Files
PlantAI-dev/tests/unit/test_wgs_qc_backend.py
T
2026-08-03 18:42:47 +08:00

111 lines
6.0 KiB
Python

import json
from pathlib import Path
from apps.web import pipeline_backend
def _valid_manifest() -> dict:
return {
"stage": "wgs_qc", "status": "success", "prefix": "Cohort", "job_id": "qc-job",
"wgs_sample_set_id": "wgs-samples:" + "a" * 64,
"wgs_analysis_id": "wgs-analysis:" + "b" * 64,
"artifacts": {
"wgs_sample_config": {"role": "wgs_sample_config"},
**{f"{role}.S1": {"role": role, "sample_id": "S1"}
for role in ("clean_reads_r1", "clean_reads_r2", "fastp_html", "fastp_json")},
},
}
def test_wgs_manifest_validator_rejects_wrong_config_role_and_incomplete_sample_roles() -> None:
wrong_role = _valid_manifest()
wrong_role["artifacts"]["wgs_sample_config"]["role"] = "input_config"
assert not pipeline_backend._valid_wgs_qc_manifest(wrong_role, job_id="qc-job", prefix="Cohort", sample_ids={"S1"})
incomplete = _valid_manifest()
del incomplete["artifacts"]["fastp_json.S1"]
assert not pipeline_backend._valid_wgs_qc_manifest(incomplete, job_id="qc-job", prefix="Cohort", sample_ids={"S1"})
def test_wgs_qc_backend_maps_api_config_to_canonical_cli_and_requires_fresh_manifest(monkeypatch, tmp_path: Path) -> None:
input_dir = tmp_path / "input"
workspace = tmp_path / "workspace"
input_dir.mkdir()
(input_dir / "S1.R1.fastq").write_text("@r\nA\n+\nI\n", encoding="utf-8")
(input_dir / "S1.R2.fastq").write_text("@r\nA\n+\nI\n", encoding="utf-8")
config = input_dir / "wgs_samples.yaml"
config.write_text("samples:\n S1:\n r1: S1.R1.fastq\n r2: S1.R2.fastq\n", encoding="utf-8")
final_dir = workspace / "final"
final_dir.mkdir(parents=True)
manifest_path = final_dir / "Cohort.wgs_qc.manifest.json"
manifest_path.write_text(json.dumps({"stage": "wgs_qc", "status": "success", "prefix": "Cohort", "artifacts": {}}), encoding="utf-8")
captured = {}
def fake_run(cmd, log_file, cwd):
captured["cmd"] = list(cmd)
manifest_path.write_text(json.dumps({
"stage": "wgs_qc", "status": "success", "prefix": "Cohort",
"artifacts": {"wgs_sample_config": {"role": "wgs_sample_config", "path": "final/config.yaml"}, **{f"{role}.S1": {"role": role, "sample_id": "S1"} for role in ("clean_reads_r1", "clean_reads_r2", "fastp_html", "fastp_json")}},
"job_id": "qc-job",
"wgs_sample_set_id": "wgs-samples:" + "a" * 64,
"wgs_analysis_id": "wgs-analysis:" + "a" * 64,
}), encoding="utf-8")
return 0
monkeypatch.setattr(pipeline_backend, "run_cmd_logged", fake_run)
message, returned_manifest = pipeline_backend.run_stage(
stage="wgs_qc",
job=pipeline_backend.JobInfo("qc-job", str(input_dir), str(workspace), str(workspace / "logs"), "now"),
prefix="Cohort",
wgs_sample_config=str(config),
wgs_fastp="/opt/fastp",
wgs_threads=3,
wgs_max_jobs=2,
)
assert "completed" in message.lower()
assert returned_manifest == manifest_path
assert captured["cmd"][:3] == [pipeline_backend.sys.executable, "-m", "apps.cli.run_wgs_qc"]
assert captured["cmd"][captured["cmd"].index("--wgs-samples") + 1] == str(config)
assert captured["cmd"][captured["cmd"].index("--fastp") + 1] == "/opt/fastp"
def test_wgs_qc_backend_rejects_missing_or_multiple_sample_configs(tmp_path: Path) -> None:
job = pipeline_backend.JobInfo("qc-job", str(tmp_path / "input"), str(tmp_path / "workspace"), str(tmp_path / "logs"), "now")
message, _ = pipeline_backend.run_stage(stage="wgs_qc", job=job, prefix="Cohort")
assert "wgs_samples.yaml" in message
input_dir = Path(job.input_dir); workspace = Path(job.workspace_dir)
input_dir.mkdir(); workspace.mkdir()
(input_dir / "wgs_samples.yaml").write_text("samples: {}\n", encoding="utf-8")
(workspace / "wgs_samples.yaml").write_text("samples: {}\n", encoding="utf-8")
message, _ = pipeline_backend.run_stage(stage="wgs_qc", job=job, prefix="Cohort")
assert "exactly one" in message
def test_wgs_qc_backend_rejects_fresh_but_identityless_manifest(monkeypatch, tmp_path: Path) -> None:
input_dir = tmp_path / "input"; workspace = tmp_path / "workspace"; input_dir.mkdir()
for mate in ("R1", "R2"):
(input_dir / f"S1.{mate}.fastq").write_text("@r\nA\n+\nI\n", encoding="utf-8")
config = input_dir / "wgs_samples.yaml"
config.write_text("samples:\n S1:\n r1: S1.R1.fastq\n r2: S1.R2.fastq\n", encoding="utf-8")
manifest = workspace / "final" / "Cohort.wgs_qc.manifest.json"; manifest.parent.mkdir(parents=True)
def fake_run(*_args, **_kwargs):
manifest.write_text(json.dumps({"stage": "wgs_qc", "status": "success", "prefix": "Cohort", "job_id": "qc-job", "artifacts": {}}), encoding="utf-8")
return 0
monkeypatch.setattr(pipeline_backend, "run_cmd_logged", fake_run)
message, _ = pipeline_backend.run_stage(stage="wgs_qc", job=pipeline_backend.JobInfo("qc-job", str(input_dir), str(workspace), str(workspace / "logs"), "now"), prefix="Cohort", wgs_sample_config=str(config))
assert "failed" in message.lower()
def test_wgs_qc_backend_rejects_stale_success_manifest(monkeypatch, tmp_path: Path) -> None:
input_dir = tmp_path / "input"; workspace = tmp_path / "workspace"; input_dir.mkdir()
for mate in ("R1", "R2"):
(input_dir / f"S1.{mate}.fastq").write_text("@r\nA\n+\nI\n", encoding="utf-8")
config = input_dir / "wgs_samples.yaml"
config.write_text("samples:\n S1:\n r1: S1.R1.fastq\n r2: S1.R2.fastq\n", encoding="utf-8")
manifest = workspace / "final" / "Cohort.wgs_qc.manifest.json"; manifest.parent.mkdir(parents=True)
manifest.write_text(json.dumps(_valid_manifest()), encoding="utf-8")
monkeypatch.setattr(pipeline_backend, "run_cmd_logged", lambda *_args, **_kwargs: 0)
message, returned = pipeline_backend.run_stage(stage="wgs_qc", job=pipeline_backend.JobInfo("qc-job", str(input_dir), str(workspace), str(workspace / "logs"), "now"), prefix="Cohort", wgs_sample_config=str(config))
assert "failed" in message.lower()
assert returned == manifest