File size: 10,138 Bytes
a49a31d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
"""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())