Chandra-Align / scripts /test_alignment_readiness.py
Chandra-Align
fix(pipeline): repair dossier trust flag and record QA verification
a49a31d
Raw History Blame Contribute Delete
10.1 kB
"""Headless safety checks for the Gradio pitch-demo contract.
This suite intentionally avoids importing the full app stack so it can run on a
headless developer machine without OpenCV, Gradio, or ZeroGPU installed.
"""
from __future__ import annotations
import ast
import contextlib
import io
import os
import py_compile
import re
import sys
import traceback
from pathlib import Path
import numpy as np
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
APP_PATH = ROOT / "app.py"
APP_SOURCE = APP_PATH.read_text(encoding="utf-8")
APP_TREE = ast.parse(APP_SOURCE, filename=str(APP_PATH))
RESULTS: list[tuple[str, str, str]] = []
def record(name: str, metric: str, check) -> None:
try:
detail = check()
RESULTS.append((name, "PASS", str(detail if detail is not None else metric)))
except Exception as exc: # report all cases before returning a failure code
RESULTS.append((name, "FAIL", f"{type(exc).__name__}: {exc}"))
def function_node(name: str) -> ast.FunctionDef:
for node in APP_TREE.body:
if isinstance(node, ast.FunctionDef) and node.name == name:
return node
raise AssertionError(f"app.py is missing function {name}")
def execute_function(name: str, namespace: dict):
node = function_node(name)
module = ast.Module(body=[node], type_ignores=[])
exec(compile(module, str(APP_PATH), "exec"), namespace)
return namespace[name]
class FakeCV2:
IMREAD_UNCHANGED = -1
INTER_AREA = 3
def __init__(self):
self.paths: list[str] = []
def imread(self, path, _flags):
self.paths.append(path)
return np.arange(48, dtype=np.uint16).reshape(6, 8)
@staticmethod
def resize(image, dimensions, interpolation=None):
del interpolation
width, height = dimensions
return np.zeros((height, width), dtype=image.dtype)
def test_path_inputs() -> str:
fake_cv = FakeCV2()
namespace = {
"np": np,
"Image": Image,
"os": os,
"cv2": fake_cv,
"MAX_IMAGE_DIMENSION": 4096,
}
reader = execute_function("_read_grayscale_image", namespace)
cases = [
Path("nested") / "reference.png",
{"path": str(Path("nested") / "secondary.png")},
]
for value in cases:
result = reader(value)
assert result.shape == (6, 8)
assert result.dtype == np.uint8
expected = [os.fspath(cases[0]), cases[1]["path"]]
assert fake_cv.paths == expected, (fake_cv.paths, expected)
return "str/Path/dict file objects decoded; uint16 normalized to uint8"
def test_callback_arity() -> str:
namespace = {
"np": np,
"sys": sys,
"re": re,
"traceback": traceback,
"MAX_IMAGE_DIMENSION": 4096,
"IIRS_MAX_IMAGE_DIMENSION": 12000,
"_failed_judge_metrics_summary": lambda *_args: {
"status_code": "FAILED", "status_message": "REJECTED"
},
"_safe_rejection_output_tuple": lambda summary, status_message=None, **_kwargs: tuple(
[None, None, None, None, status_message, summary, "", None, None, None, None, None, None, None]
),
"_read_grayscale_image": lambda *_args: (_ for _ in ()).throw(ValueError("bad image")),
}
callback = execute_function("process_alignment", namespace)
with contextlib.redirect_stderr(io.StringIO()):
missing = callback(None, None)
invalid = callback("bad-reference.png", "bad-secondary.png")
assert len(missing) == 14, len(missing)
assert len(invalid) == 14, len(invalid)
return "missing and invalid inputs both return 14 outputs without UnboundLocalError"
def test_safe_rejection_arity() -> str:
namespace = {
"np": np,
"Image": Image,
"sys": sys,
"traceback": traceback,
"_failed_judge_metrics_summary": lambda: {"status_code": "FAILED"},
"_rejected_output_tuple": lambda *_args, **_kwargs: tuple(range(12)),
}
execute_function("_minimal_rejection_output_tuple", namespace)
helper = execute_function("_safe_rejection_output_tuple", namespace)
with contextlib.redirect_stderr(io.StringIO()):
outputs = helper({}, "Rejected in headless test")
assert len(outputs) == 14, len(outputs)
assert "N/A" in outputs[6]
return "banner failure fallback returns 14 safe values and masked telemetry"
def test_wrapper_catches_gpu_cpu_and_append_errors() -> str:
calls = []
def fail_gpu(*_args):
calls.append("gpu")
raise RuntimeError("simulated ZeroGPU failure")
def fail_cpu(*_args):
calls.append("cpu")
raise RuntimeError("simulated CPU failure")
def emergency(message, summary=None):
calls.append("fallback")
return (message, summary) + (None,) * 12
namespace = {
"sys": sys,
"traceback": traceback,
"run_alignment_on_gpu": fail_gpu,
"_run_alignment_core": fail_cpu,
"_minimal_rejection_output_tuple": emergency,
"_append_execution_status": lambda *_args: (_ for _ in ()).throw(
RuntimeError("simulated report formatter failure")
),
"_zerogpu_runtime_enabled": lambda: False,
"GPU_EXECUTION_STATUS": "GPU",
"CPU_FALLBACK_STATUS": "CPU",
}
wrapper = execute_function("process_wrapper", namespace)
with contextlib.redirect_stderr(io.StringIO()):
result = wrapper(*([None] * 13))
assert len(result) == 14, len(result)
assert calls == ["gpu", "cpu", "fallback", "fallback"], calls
return "GPU, CPU, and output formatting exceptions are contained at the Gradio boundary"
def test_assets_and_examples() -> str:
names = (
"nac_reference_ohrc.png", "ohrc_secondary.png",
"nac_reference_tmc2.png", "tmc2_secondary.png",
"nac_reference_iirs.png", "iirs_band125_secondary.png",
"synthetic_groundtruth_reference.png", "synthetic_groundtruth_secondary.png",
)
paths = [ROOT / "docs" / "assets" / "examples" / name for name in names]
for path in paths:
assert path.is_file(), f"missing example asset: {path}"
with Image.open(path) as image:
image.verify()
with Image.open(paths[-2]) as reference, Image.open(paths[-1]) as secondary:
ref_array = np.asarray(reference.convert("L"))
sec_array = np.asarray(secondary.convert("L"))
assert ref_array.shape == sec_array.shape
assert not np.array_equal(ref_array, sec_array), "synthetic pair must be distinct"
assert all(name in APP_SOURCE for name in names)
assert 'label="Select Lunar Data Pair Presets"' in APP_SOURCE
assert "cache_examples=False" in APP_SOURCE
assert "examples_per_page=4" in APP_SOURCE
assert "interface.launch(ssr_mode=False)" in APP_SOURCE
return "8 tracked-format assets valid; synthetic pair distinct; Examples caching disabled"
def test_fixed_contract_and_primary_ratio() -> str:
node = function_node("process_alignment")
tuple_returns = [
ret for ret in ast.walk(node)
if isinstance(ret, ast.Return) and isinstance(ret.value, ast.Tuple)
]
assert tuple_returns, "no tuple return paths found"
assert all(len(ret.value.elts) == 14 for ret in tuple_returns)
assert "for ratio in (0.75, 0.80, 0.85)" in APP_SOURCE
assert re.search(r"first\.distance\s*<\s*ratio\s*\*\s*second\.distance", APP_SOURCE)
return f"{len(tuple_returns)} literal tuple path(s) have 14 values; Lowe ratio 0.75"
def test_warp_is_initialized_before_visualization() -> str:
node = function_node("_align_core")
warp_assignment = next(
(child for child in ast.walk(node)
if isinstance(child, ast.Assign)
and any(isinstance(target, ast.Name) and target.id == "warped_sec"
for target in child.targets)),
None,
)
assert warp_assignment is not None, "_align_core no longer assigns warped_sec"
reads = [
child.lineno for child in ast.walk(node)
if isinstance(child, ast.Name) and child.id == "warped_sec"
and isinstance(child.ctx, ast.Load)
]
assert reads, "_align_core does not consume the warped raster"
first_read = min(reads)
assert warp_assignment.lineno < first_read, (
f"warped_sec read on line {first_read} before assignment on line {warp_assignment.lineno}"
)
return f"warped_sec initialized on line {warp_assignment.lineno} before its first read on line {first_read}"
def test_compile() -> str:
targets = [
APP_PATH,
ROOT / "chandra_align" / "preprocessing" / "general.py",
ROOT / "chandra_align" / "preprocessing" / "__init__.py",
ROOT / "chandra_align" / "matcher" / "__init__.py",
ROOT / "chandra_align" / "matching" / "deep_matchers.py",
]
for path in targets:
py_compile.compile(str(path), doraise=True)
return f"compiled {len(targets)} modified modules"
def main() -> int:
checks = (
("String/path image decoding", "image path dtypes", test_path_inputs),
("Callback exception arity", "14 outputs", test_callback_arity),
("Rejection fallback arity", "14 outputs", test_safe_rejection_arity),
("Gradio callback master guard", "14 outputs", test_wrapper_catches_gpu_cpu_and_append_errors),
("Native example assets", "8 valid PNGs", test_assets_and_examples),
("Tuple and ratio contract", "14 tuple / 0.75 ratio", test_fixed_contract_and_primary_ratio),
("Warped raster initialization order", "assign before read", test_warp_is_initialized_before_visualization),
("Python compilation", "5 modules", test_compile),
)
for name, metric, check in checks:
record(name, metric, check)
print("[CHECK] | [STATUS] | [DETAIL]")
print("-" * 100)
for name, status, detail in RESULTS:
print(f"{name} | {status} | {detail}")
failed = sum(status != "PASS" for _name, status, _detail in RESULTS)
print(f"\n{len(RESULTS) - failed}/{len(RESULTS)} checks passed")
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())