from __future__ import annotations import hashlib import json import os from pathlib import Path from threading import Barrier, Thread import zipfile import pysam import pytest SAMPLES = "wgs-samples:" + "a" * 64 REFERENCE = "wgs-reference:" + "b" * 64 FILTER_ANALYSIS = "wgs-analysis:" + "c" * 64 SITE_KEYS = [ ("chr1", 10, "A", "C"), ("chr1", 20, "G", "T"), ] def _record(workspace: Path, path: Path, role: str) -> dict: return { "artifact": role, "role": role, "path": str(path.relative_to(workspace)), "full_path": str(path.resolve()), "size": path.stat().st_size, "size_bytes": path.stat().st_size, "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), } def _write_bgzip_vcf( path: Path, *, samples: tuple[str, ...] = ("102", "108", "112"), site_keys: list[tuple[str, int, str, str]] | None = None, ) -> None: plain = path.with_suffix("").with_suffix("") with plain.open("w", encoding="utf-8") as handle: handle.write("##fileformat=VCFv4.2\n") handle.write("##contig=\n") handle.write( '##FORMAT=\n' ) handle.write( "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\t" + "\t".join(samples) + "\n" ) for chrom, pos, ref, alt in site_keys or SITE_KEYS: handle.write( f"{chrom}\t{pos}\t.\t{ref}\t{alt}\t50\tPASS\t.\tGT\t" + "\t".join("./." if index == 1 else "0/1" for index, _ in enumerate(samples)) + "\n" ) pysam.tabix_compress(str(plain), str(path), force=True) pysam.tabix_index(str(path), preset="vcf", force=True) plain.unlink() def _filter_authority( workspace: Path, *, method: str = "gatk", samples: tuple[str, ...] = ("102", "108", "112"), ) -> Path: bundle = ( workspace / "final" / ".wgs_variant_filter_runs" / "Cohort" / method / ("8" * 32) ) bundle.mkdir(parents=True) vcf = bundle / "Cohort.wgs_variant_filter.cohort.pass.snp.vcf.gz" _write_bgzip_vcf(vcf, samples=samples) index = Path(f"{vcf}.tbi") report = bundle / "Cohort.wgs_variant_filter.cohort.report.md" report.write_text(f"method: {method}\n", encoding="utf-8") authority = ( workspace / "final" / f"Cohort.wgs_variant_filter.{method}.manifest.json" ) authority.write_text( json.dumps( { "manifest_version": "1.0", "stage": "wgs_variant_filter", "status": "success", "prefix": "Cohort", "job_id": "job-1", "run_id": "8" * 32, "workspace": str(workspace.resolve()), "wgs_sample_set_id": SAMPLES, "wgs_reference_id": REFERENCE, "wgs_analysis_id": FILTER_ANALYSIS, "method": method, "parameters": {"method": method}, "metrics": { "input_sample_ids": list(samples), "output_sample_ids": list(samples), }, "artifacts": { "pass_snp_vcf": _record( workspace, vcf, "pass_snp_vcf" ), "pass_snp_vcf_index": _record( workspace, index, "pass_snp_vcf_index" ), "variant_filter_report": _record( workspace, report, "variant_filter_report" ), }, } ), encoding="utf-8", ) return authority def _java(tmp_path: Path, version: str = "23-test") -> Path: java = tmp_path / "java" java.write_text( """#!/home/xia-lab/miniconda3/envs/test4py311/bin/python import gzip, json, os, pathlib, sys, tempfile import pysam args = sys.argv[1:] if args == ['-version']: print('openjdk version "JAVA_VERSION"', file=sys.stderr) raise SystemExit(0) calls = os.environ.get('WGS_IMPUTE_CALLS') if calls: with open(calls, 'a', encoding='utf-8') as handle: handle.write(json.dumps(args) + '\\n') if os.environ.get('FAIL_POINT') == 'beagle': raise SystemExit(23) def value(key): return next(item.split('=', 1)[1] for item in args if item.startswith(key + '=')) source = pathlib.Path(value('gt')) out = pathlib.Path(value('out')) with gzip.open(source, 'rt', encoding='utf-8') as handle: lines = list(handle) mode = os.environ.get('BEAGLE_OUTPUT', '') if mode == 'empty': lines = [] elif mode == 'invalid': lines = ['not-a-vcf\\n'] elif mode == 'sample-mismatch': lines = [line.replace('\\t112\\n', '\\t999\\n') if line.startswith('#CHROM') else line for line in lines] elif mode == 'site-drop': records = [line for line in lines if not line.startswith('#')] lines = [line for line in lines if line.startswith('#')] + records[:1] elif mode == 'site-reorder': records = [line for line in lines if not line.startswith('#')] lines = [line for line in lines if line.startswith('#')] + list(reversed(records)) plain = pathlib.Path(str(out) + '.vcf') plain.parent.mkdir(parents=True, exist_ok=True) with plain.open('w', encoding='utf-8') as handle: for line in lines: if not line.startswith('#') and mode not in ('empty', 'invalid'): fields = line.rstrip().split('\\t') fields[9:] = [gt.replace('/', '|').replace('.', '0') for gt in fields[9:]] line = '\\t'.join(fields) + '\\n' handle.write(line) pysam.tabix_compress(str(plain), str(out) + '.vcf.gz', force=True) plain.unlink() pathlib.Path(str(out) + '.log').write_text('beagle test log\\n', encoding='utf-8') """.replace("JAVA_VERSION", version), encoding="utf-8", ) java.chmod(0o755) return java def _class_jar(tmp_path: Path, class_major: int) -> Path: jar = tmp_path / f"beagle-java-{class_major - 44}.jar" class_header = ( b"\xca\xfe\xba\xbe" + (0).to_bytes(2, "big") + class_major.to_bytes(2, "big") ) with zipfile.ZipFile(jar, "w") as archive: archive.writestr("main/Main.class", class_header) return jar def _argv(workspace: Path, authority: Path, java: Path) -> list[str]: return [ "--workspace", str(workspace), "--job-id", "job-1", "--prefix", "Cohort", "--filter-manifest", str(authority), "--java", str(java), "--beagle-jar", str(Path("/tmp/beagle.29Oct24.c8e.jar")), ] def _authority_path(workspace: Path) -> Path: return workspace / "final" / "Cohort.wgs_impute.manifest.json" def _failed(workspace: Path) -> dict: paths = sorted((workspace / "final").glob("*wgs_impute*failed*.json")) if paths: path = paths[-1] else: path = _authority_path(workspace) payload = json.loads(path.read_text(encoding="utf-8")) assert payload["status"] == "failed" return payload def test_cli_success_locks_parameters_lineage_roles_samples_sites_and_index( tmp_path: Path, monkeypatch ) -> None: from apps.cli.run_wgs_impute import main workspace = tmp_path / "workspace" authority = _filter_authority(workspace, method="bcftools") java = _java(tmp_path) calls = tmp_path / "calls.jsonl" monkeypatch.setenv("WGS_IMPUTE_CALLS", str(calls)) assert main(_argv(workspace, authority, java)) == 0 manifest = json.loads(_authority_path(workspace).read_text()) assert set(manifest["artifacts"]) == { "imputed_snp_vcf", "imputed_snp_vcf_index", "impute_report", "impute_log", } assert manifest["upstream_wgs_analysis_ids"] == { "wgs_variant_filter": FILTER_ANALYSIS } assert manifest["upstream_method"] == "bcftools" assert manifest["wgs_sample_set_id"] == SAMPLES assert manifest["wgs_reference_id"] == REFERENCE assert manifest["metrics"]["input_sample_ids"] == ["102", "108", "112"] assert manifest["metrics"]["output_sample_ids"] == ["102", "108", "112"] assert manifest["metrics"]["input_site_keys"] == [ ["chr1", 10, "A", "C"], ["chr1", 20, "G", "T"], ] assert manifest["metrics"]["output_site_keys"] == manifest["metrics"]["input_site_keys"] assert manifest["parameters"] == { "method": "beagle", "java": str(java), "beagle_jar": "/tmp/beagle.29Oct24.c8e.jar", "memory": "8g", "window": 40.0, "overlap": 2.0, "ne": 100000, "iterations": 12, "phase_states": 280, "imp_states": 1600, "cluster": 0.005, } command = json.loads(calls.read_text().splitlines()[0]) assert command == [ "-Xmx8g", "-jar", "/tmp/beagle.29Oct24.c8e.jar", f"gt={json.loads(authority.read_text())['artifacts']['pass_snp_vcf']['full_path']}", "out=" + str(workspace / ".attempts" / manifest["run_id"] / "beagle"), "window=40.0", "overlap=2.0", "ne=100000", "iterations=12", "phase-states=280", "imp-states=1600", "cluster=0.005", ] output = Path(manifest["artifacts"]["imputed_snp_vcf"]["full_path"]) with pysam.VariantFile(str(output)) as handle: assert [record.pos for record in handle.fetch("chr1", 0, 1000)] == [10, 20] def test_cli_rejects_java_older_than_beagle_class_version( tmp_path: Path ) -> None: from apps.cli.run_wgs_impute import main workspace = tmp_path / "workspace" java = _java(tmp_path, version="11.0.13") argv = _argv(workspace, _filter_authority(workspace), java) argv[-1] = str(_class_jar(tmp_path, class_major=65)) assert main(argv) == 1 error = _failed(workspace)["errors"][0] assert "requires Java 21" in error assert "configured Java is 11" in error @pytest.mark.parametrize("method", ["gatk", "bcftools"]) def test_cli_accepts_either_legal_stage10_method( tmp_path: Path, method: str ) -> None: from apps.cli.run_wgs_impute import main workspace = tmp_path / method assert main( _argv(workspace, _filter_authority(workspace, method=method), _java(tmp_path)) ) == 0 @pytest.mark.parametrize( ("failure", "message"), [ ("beagle", "Beagle failed with exit code 23"), ("sample-mismatch", "sample header mismatch"), ("site-drop", "site keys are not conserved"), ("site-reorder", "building of index"), ("empty", "invalid"), ("invalid", "invalid"), ], ) def test_cli_rejects_tool_and_postflight_failures_with_empty_artifacts( tmp_path: Path, monkeypatch, failure: str, message: str ) -> None: from apps.cli.run_wgs_impute import main workspace = tmp_path / "workspace" upstream = _filter_authority(workspace) if failure == "beagle": monkeypatch.setenv("FAIL_POINT", failure) else: monkeypatch.setenv("BEAGLE_OUTPUT", failure) assert main(_argv(workspace, upstream, _java(tmp_path))) == 1 payload = _failed(workspace) assert payload["artifacts"] == {} assert message.lower() in payload["errors"][0].lower() def test_cli_index_failure_is_not_overwritten_by_later_validation( tmp_path: Path, monkeypatch ) -> None: from apps.cli import run_wgs_impute workspace = tmp_path / "workspace" upstream = _filter_authority(workspace) def fail_index(*_args, **_kwargs): raise OSError("controlled index failure") monkeypatch.setattr(run_wgs_impute, "_tabix_index", fail_index) assert run_wgs_impute.main(_argv(workspace, upstream, _java(tmp_path))) == 1 payload = _failed(workspace) assert payload["artifacts"] == {} assert "controlled index failure" in payload["errors"][0] def test_failed_rerun_preserves_old_authority_and_bundle( tmp_path: Path, monkeypatch ) -> None: from apps.cli.run_wgs_impute import main workspace = tmp_path / "workspace" upstream = _filter_authority(workspace) argv = _argv(workspace, upstream, _java(tmp_path)) assert main(argv) == 0 authority = _authority_path(workspace) before = authority.read_bytes() manifest = json.loads(before) bundle_hashes = { role: hashlib.sha256(Path(item["full_path"]).read_bytes()).hexdigest() for role, item in manifest["artifacts"].items() } monkeypatch.setenv("FAIL_POINT", "beagle") assert main(argv) == 1 assert authority.read_bytes() == before assert { role: hashlib.sha256(Path(item["full_path"]).read_bytes()).hexdigest() for role, item in manifest["artifacts"].items() } == bundle_hashes def test_concurrent_success_publishes_one_complete_authority( tmp_path: Path, monkeypatch ) -> None: from apps.cli.run_wgs_impute import main workspace = tmp_path / "workspace" argv = _argv(workspace, _filter_authority(workspace), _java(tmp_path)) barrier = Barrier(2) original = __import__( "apps.cli.run_wgs_impute", fromlist=["_run_beagle"] )._run_beagle def synchronized(*args, **kwargs): barrier.wait() return original(*args, **kwargs) monkeypatch.setattr("apps.cli.run_wgs_impute._run_beagle", synchronized) results: list[int] = [] threads = [Thread(target=lambda: results.append(main(argv))) for _ in range(2)] for thread in threads: thread.start() for thread in threads: thread.join(timeout=20) assert sorted(results) in ([0, 0], [0, 1]) manifest = json.loads(_authority_path(workspace).read_text()) assert manifest["status"] == "success" assert all(Path(item["full_path"]).is_file() for item in manifest["artifacts"].values())