89 lines
4.8 KiB
Python
89 lines
4.8 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
CONTRACTS = {
|
|
"wgs_qc": ("wgs_sample_config", "clean_reads_r1", "clean_reads_r2", "fastp_html", "fastp_json"),
|
|
"wgs_index": ("reference_fasta", "reference_fai", "reference_dict", "bwa_mem2_index_component"),
|
|
"wgs_align": ("clean_reads_r1", "clean_reads_r2", "reference_fasta", "reference_fai", "reference_dict", "bwa_mem2_index_component", "sorted_bam", "sorted_bam_index", "align_log"),
|
|
"wgs_markdup": ("sorted_bam", "sorted_bam_index", "markdup_bam", "markdup_bam_index", "markdup_metrics", "markdup_log"),
|
|
"wgs_bam_qc": ("markdup_bam", "markdup_bam_index", "reference_fai", "bam_flagstat", "bam_depth_summary", "bam_qc_summary", "bam_qc_report"),
|
|
"wgs_haplotype_calling": ("markdup_bam", "markdup_bam_index", "bam_qc_summary", "reference_fasta", "reference_fai", "reference_dict", "gvcf", "gvcf_index", "haplotype_log"),
|
|
"wgs_gvcf_combine": ("gvcf", "gvcf_index", "reference_fasta", "reference_fai", "reference_dict", "combined_gvcf", "combined_gvcf_index", "combine_log"),
|
|
"wgs_joint_genotyping": ("combined_gvcf", "combined_gvcf_index", "reference_fasta", "reference_fai", "reference_dict", "joint_vcf", "joint_vcf_index", "joint_summary", "joint_log"),
|
|
"wgs_variant_select": ("joint_vcf", "joint_vcf_index", "selected_snp_vcf", "selected_snp_vcf_index", "selected_indel_vcf", "selected_indel_vcf_index", "variant_select_report"),
|
|
"wgs_variant_filter": ("selected_snp_vcf", "selected_snp_vcf_index", "selected_indel_vcf", "selected_indel_vcf_index", "filtered_snp_vcf", "filtered_snp_vcf_index", "pass_snp_vcf", "pass_snp_vcf_index", "filtered_indel_vcf", "filtered_indel_vcf_index", "pass_indel_vcf", "pass_indel_vcf_index", "variant_filter_report"),
|
|
"wgs_impute": ("pass_snp_vcf", "pass_snp_vcf_index", "imputed_snp_vcf", "imputed_snp_vcf_index", "impute_report", "impute_log"),
|
|
"wgs_maf_missing_filter": ("imputed_snp_vcf", "imputed_snp_vcf_index", "final_snp_vcf", "final_snp_vcf_index", "maf_missing_report", "maf_missing_stats"),
|
|
}
|
|
CANONICAL_FILENAME_TOKENS = {
|
|
"wgs_qc": ".clean.R1.fastq.gz",
|
|
"wgs_index": ".reference.fasta.fai",
|
|
"wgs_align": ".sorted.bam",
|
|
"wgs_markdup": ".markdup.bam",
|
|
"wgs_bam_qc": ".flagstat.txt",
|
|
"wgs_haplotype_calling": ".g.vcf.gz",
|
|
"wgs_gvcf_combine": ".combined.g.vcf.gz",
|
|
"wgs_joint_genotyping": ".joint.vcf.gz",
|
|
"wgs_variant_select": ".selected.snp.vcf.gz",
|
|
"wgs_variant_filter": ".filtered.snp.vcf.gz",
|
|
"wgs_impute": ".imputed.snp.vcf.gz",
|
|
"wgs_maf_missing_filter": ".final.snp.vcf.gz",
|
|
}
|
|
|
|
|
|
def _section(text: str, heading: str) -> str:
|
|
return text.split(f"## {heading}\n", 1)[1].split("\n## ", 1)[0]
|
|
|
|
|
|
@pytest.mark.parametrize("stage,roles", CONTRACTS.items())
|
|
def test_wgs_contract_locks_roles_filenames_indices_identities_and_gates(stage: str, roles: tuple[str, ...]) -> None:
|
|
contract = PROJECT_ROOT / "gfa_core" / "schemas" / "stages" / f"{stage}_contract_v1.md"
|
|
text = contract.read_text(encoding="utf-8")
|
|
for phrase in (
|
|
f"Stage: `{stage}`",
|
|
"## Consumes",
|
|
"## Provides",
|
|
"## Canonical filenames",
|
|
"## Required indices",
|
|
"## Scientific identities",
|
|
"## Parameter snapshot",
|
|
"status=success",
|
|
"artifacts={}",
|
|
):
|
|
assert phrase in text
|
|
for role in roles:
|
|
assert f"`{role}`" in text
|
|
assert CANONICAL_FILENAME_TOKENS[stage] in _section(text, "Canonical filenames")
|
|
failure = _section(text, "Success and failure evidence")
|
|
assert ".attempts/<run_id>" in failure
|
|
assert "atomically publish" in failure
|
|
assert "old success" in failure
|
|
assert "artifacts={}" in failure
|
|
|
|
|
|
def test_wgs_index_contract_locks_exact_bwa_mem2_components() -> None:
|
|
text = (PROJECT_ROOT / "gfa_core/schemas/stages/wgs_index_contract_v1.md").read_text(encoding="utf-8")
|
|
indices = _section(text, "Required indices")
|
|
for component in (".0123", ".amb", ".ann", ".bwt.2bit.64", ".pac"):
|
|
assert component in indices
|
|
|
|
|
|
def test_wgs_haplotype_contract_requires_successful_bam_qc_gate() -> None:
|
|
text = (PROJECT_ROOT / "gfa_core/schemas/stages/wgs_haplotype_calling_contract_v1.md").read_text(encoding="utf-8")
|
|
assert "successful `wgs_bam_qc`" in _section(text, "Consumes")
|
|
|
|
|
|
@pytest.mark.parametrize("stage", ["wgs_variant_select", "wgs_variant_filter"])
|
|
def test_variant_contracts_lock_confirmed_gatk_to_bcftools_fallback(stage: str) -> None:
|
|
text = (PROJECT_ROOT / "gfa_core" / "schemas" / "stages" / f"{stage}_contract_v1.md").read_text(encoding="utf-8")
|
|
assert "method=gatk" in _section(text, "Parameter snapshot")
|
|
failure = _section(text, "Success and failure evidence")
|
|
assert "Only after GATK failure" in failure
|
|
assert "user confirmation" in failure
|
|
assert "method=bcftools" in failure
|