"""Tests for the function-sampled@v1 demo-plot script. The script's primary contract is the canonical-bytes path (always prints SHA-256 + preview to stdout). The PNG render is an optional matplotlib-gated downstream view; tests skip gracefully when matplotlib isn't installed. """ from __future__ import annotations import json import subprocess import sys from pathlib import Path import pytest sympy = pytest.importorskip("sympy") def _run(*args: str) -> subprocess.CompletedProcess: return subprocess.run( [sys.executable, "-m", "bench.scripts.demo_plot", *args], capture_output=True, text=True, check=False, ) def test_demo_plot_emits_canonical_bytes_for_sin_x(): r = _run("--expr", "sin(x)", "--n-samples", "10", "--dv", "0.1") assert r.returncode == 0, r.stderr out = json.loads(r.stdout) assert out["expr"] == "sin(x)" assert out["pi_star_ref"] == "function-sampled@v1" assert len(out["canonical_bytes_sha256"]) == 64 assert "canonical_bytes_preview" in out assert out["grid"]["n_samples"] == 10 def test_demo_plot_canonical_bytes_deterministic(): """Same expression + grid → same SHA-256.""" r1 = _run("--expr", "x**2", "--x-min", "0", "--x-max", "4", "--n-samples", "5", "--dv", "1") r2 = _run("--expr", "x**2", "--x-min", "0", "--x-max", "4", "--n-samples", "5", "--dv", "1") assert r1.returncode == 0 and r2.returncode == 0 out1 = json.loads(r1.stdout) out2 = json.loads(r2.stdout) assert out1["canonical_bytes_sha256"] == out2["canonical_bytes_sha256"] def test_demo_plot_equivalent_expressions_collapse(): """sin(x) and 2*sin(x)/2 must produce the same SHA-256 — they sample to identical numeric values, so canonical bytes match.""" a = _run("--expr", "sin(x)", "--n-samples", "10", "--dv", "0.1") b = _run("--expr", "2*sin(x)/2", "--n-samples", "10", "--dv", "0.1") assert a.returncode == 0 and b.returncode == 0 assert ( json.loads(a.stdout)["canonical_bytes_sha256"] == json.loads(b.stdout)["canonical_bytes_sha256"] ) def test_demo_plot_invalid_expression_returns_1(): """A genuinely unparseable expression should exit 1 with a PiStarError surfaced on stderr.""" # Empty expression — sympify accepts garbage but lambdify or # range checks fail; use an obviously-bad input. r = _run("--expr", "x_min", "--x-min", "1", "--x-max", "1", "--n-samples", "5", "--dv", "1") # x_max <= x_min → PiStarError from function-sampled@v1 assert r.returncode == 1 assert "error" in r.stderr.lower() def test_demo_plot_png_optional_no_crash_when_matplotlib_missing(tmp_path): """When matplotlib isn't installed, --png prints a warning to stderr and skips the PNG; canonical bytes still print.""" png_path = tmp_path / "out.png" r = _run("--expr", "x", "--n-samples", "5", "--dv", "1", "--png", str(png_path)) assert r.returncode == 0, r.stderr out = json.loads(r.stdout) assert "canonical_bytes_sha256" in out # matplotlib may or may not be installed in this env; the # script's contract is "no crash either way." def test_demo_plot_png_renders_when_matplotlib_present(tmp_path): matplotlib = pytest.importorskip("matplotlib") png_path = tmp_path / "out.png" r = _run("--expr", "sin(x)", "--n-samples", "20", "--dv", "0.1", "--png", str(png_path)) assert r.returncode == 0, r.stderr assert png_path.is_file(), "matplotlib was importable; PNG must land" # Sanity: the file isn't empty. assert png_path.stat().st_size > 100