from __future__ import annotations import csv import gzip import importlib.util import json import tempfile import unittest from pathlib import Path from types import SimpleNamespace from unittest import mock import numpy as np SCRIPT = Path(__file__).resolve().parents[2] / "python" / "spectra_reml.py" SPEC = importlib.util.spec_from_file_location("spectra_reml", SCRIPT) assert SPEC is not None and SPEC.loader is not None cli = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(cli) class SpectraRemlCliTests(unittest.TestCase): def fixture(self, root: Path, with_contrasts: bool = False) -> tuple[Path, dict]: n, p0 = 3, 2 np.zeros(n * (n + 1) // 2, dtype=" None: root.mkdir(parents=True, exist_ok=True) first_contrast_offset = 0 if contrast_count else -1 second_contrast_offset = contrast_count if contrast_count else -1 (root / "block_000000.summary.tsv").write_text( "\t".join(cli.SUMMARY_HEADER) + "\n" "0\ttrait_a\tconverged\t2\t0\t0\t0\t1\t1\t0.5\t-1\t4\t8\t1e-8" "\tsatterthwaite\tok\t0\t{}\t0\tnan\tnan\tnan\t\t\n" "1\ttrait_b\tconverged_boundary\t3\t1\t2\t3\t0\t1\t0\t-2\t3\t6\t1e-9" "\tordinary-least-squares\tboundary_ols\t2\t{}\t1\t8\t2\t0.2\t\t\n".format( first_contrast_offset, second_contrast_offset ), encoding="utf-8", ) np.asarray([1, 2, 3, 4, 5], dtype=" None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) _, manifest = self.fixture(root) args = cli.build_parser().parse_args( ["run", "--manifest", str(root / "manifest.json"), "--engine", "spectra_reml"] ) command = cli.build_engine_command(Path("spectra_reml"), manifest, args) for option in ( "--phenotypes", "--extra-covariates", "--extra-offsets", "--extra-indices", "--n-phenotype-rows", "--n-extra-covariate-rows", "--line-search-expansion", "--zoom-safeguard", "--n-contrasts", ): self.assertEqual(command.count(option), 1) def test_contrast_manifest_and_engine_command(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) _, manifest = self.fixture(root, with_contrasts=True) self.assertEqual(manifest["dimensions"]["contrast_count"], 2) args = cli.build_parser().parse_args( [ "run", "--manifest", str(root / "manifest.json"), "--engine", "spectra_reml", "--fixed-effect-test", "kenward-roger", ] ) command = cli.build_engine_command(Path("spectra_reml"), manifest, args) self.assertEqual(command.count("--contrast-matrix"), 1) self.assertEqual(command.count("--contrast-metadata"), 1) self.assertEqual(command[command.index("--n-contrasts") + 1], "2") self.assertEqual(command[command.index("--fixed-effect-test") + 1], "kenward-roger") def test_new_contract_rejects_missing_contrast_fields(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) manifest_path, _ = self.fixture(root) manifest = cli.read_json(manifest_path) del manifest["dimensions"]["contrast_count"] del manifest["paths"]["contrast_matrix_f64"] del manifest["paths"]["contrast_metadata_tsv"] cli.atomic_json(manifest_path, manifest) with self.assertRaises(RuntimeError): cli.validate_manifest(manifest_path) def test_both_fixed_effect_methods_are_forwarded(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) _, manifest = self.fixture(root, with_contrasts=True) for method in ("satterthwaite", "kenward-roger"): args = cli.build_parser().parse_args( [ "run", "--manifest", str(root / "manifest.json"), "--engine", "spectra_reml", "--fixed-effect-test", method, ] ) command = cli.build_engine_command(Path("spectra_reml"), manifest, args) self.assertEqual( command[command.index("--fixed-effect-test") + 1], method ) def test_contrast_finalize_contract(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) manifest_path, manifest = self.fixture(root, with_contrasts=True) engine = root / "spectra_reml" engine.write_bytes(b"synthetic engine") args = cli.build_parser().parse_args( [ "run", "--manifest", str(manifest_path), "--engine", str(engine), "--resume", "--fixed-effect-test", "satterthwaite", ] ) with mock.patch.object( cli.subprocess, "run", return_value=SimpleNamespace(returncode=0) ): cli.run_engine(manifest, args) output = Path(manifest["paths"]["output_directory"]) self.write_blocks(output, contrast_count=2) result = root / "contrast_results.tsv.gz" cli.finalize(manifest, result) with gzip.open(result, "rt", encoding="utf-8", newline="") as handle: rows = list(csv.DictReader(handle, delimiter="\t")) self.assertEqual(json.loads(rows[0]["contrast_estimate_json"]), [1.0, 2.0]) self.assertEqual(json.loads(rows[1]["contrast_estimate_json"]), [3.0, 4.0]) self.assertEqual(json.loads(rows[0]["contrast_numerator_df_json"]), [1.0, 1.0]) def test_signature_recovery_and_finalize(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) manifest_path, manifest = self.fixture(root) engine = root / "spectra_reml" engine.write_bytes(b"synthetic engine") args = cli.build_parser().parse_args( ["run", "--manifest", str(manifest_path), "--engine", str(engine), "--resume"] ) with mock.patch.object(cli.subprocess, "run", return_value=SimpleNamespace(returncode=0)): cli.run_engine(manifest, args) output = Path(manifest["paths"]["output_directory"]) signature = cli.read_signature(output / "run.signature.json") self.assertEqual(signature["format"], cli.RUN_SIGNATURE_FORMAT) self.write_blocks(output) changed = cli.build_parser().parse_args( [ "run", "--manifest", str(manifest_path), "--engine", str(engine), "--resume", "--max-iterations", "101", ] ) with self.assertRaisesRegex(RuntimeError, "signature differs"): cli.run_engine(manifest, changed) result = root / "results.tsv.gz" cli.finalize(manifest, result) with gzip.open(result, "rt", encoding="utf-8", newline="") as handle: rows = list(csv.DictReader(handle, delimiter="\t")) self.assertEqual([row["task_id"] for row in rows], ["trait_a", "trait_b"]) self.assertEqual(json.loads(rows[0]["beta_json"]), [1.0, 2.0]) self.assertEqual(len(json.loads(rows[1]["covariance_packed_lower_json"])), 6) self.assertEqual(len(json.loads(rows[1]["fixed_effect_p_value_json"])), 3) altered = dict(manifest) altered["created_utc"] = "changed" with self.assertRaisesRegex(RuntimeError, "does not belong"): cli.finalize(altered, root / "unsafe.tsv") def test_force_invalidates_before_cleanup_and_dry_run_is_read_only(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) manifest_path, manifest = self.fixture(root) engine = root / "spectra_reml" engine.write_bytes(b"engine") output = Path(manifest["paths"]["output_directory"]) self.write_blocks(output) resume_args = cli.build_parser().parse_args( ["run", "--manifest", str(manifest_path), "--engine", str(engine), "--resume"] ) cli.atomic_json(output / "run.signature.json", cli.make_signature(manifest, engine, resume_args)) before = sorted(path.name for path in output.iterdir()) dry = cli.build_parser().parse_args( ["run", "--manifest", str(manifest_path), "--engine", "missing", "--force", "--dry-run"] ) cli.run_engine(manifest, dry) self.assertEqual(before, sorted(path.name for path in output.iterdir())) force = cli.build_parser().parse_args( ["run", "--manifest", str(manifest_path), "--engine", str(engine), "--force"] ) with mock.patch.object(cli, "block_files", side_effect=RuntimeError("cleanup interrupted")): with self.assertRaisesRegex(RuntimeError, "cleanup interrupted"): cli.run_engine(manifest, force) self.assertFalse((output / "run.signature.json").exists()) if __name__ == "__main__": unittest.main()