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

117 lines
4.1 KiB
Python

from __future__ import annotations
import pytest
from gfa_core.wgs_scientific_identity import (
WgsIdentityError,
build_wgs_analysis_id,
build_wgs_reference_id,
build_wgs_sample_set_id,
sha256_file,
)
def test_analysis_identity_changes_with_method():
args = {
"stage": "wgs_variant_filter",
"upstream_sha256s": ["a" * 64],
"parameters": {"qd": 2.0},
"tool_versions": {"gatk": "4.6", "bcftools": "1.20"},
}
assert build_wgs_analysis_id(**args, method="gatk") != build_wgs_analysis_id(
**args, method="bcftools"
)
def test_wgs_sample_set_identity_uses_fastq_contents(tmp_path):
r1 = tmp_path / "S1_R1.fastq.gz"
r2 = tmp_path / "S1_R2.fastq.gz"
r1.write_bytes(b"r1-v1")
r2.write_bytes(b"r2-v1")
samples = build_wgs_sample_set_id([{"sample_id": "S1", "r1": str(r1), "r2": str(r2)}])
assert samples.startswith("wgs-samples:")
assert len(sha256_file(r1)) == 64
r1.write_bytes(b"r1-v2")
assert samples != build_wgs_sample_set_id([{"sample_id": "S1", "r1": str(r1), "r2": str(r2)}])
def test_wgs_identity_prefixes_are_canonical_and_validate_sha256s():
reference = build_wgs_reference_id("a" * 64)
assert reference.startswith("wgs-reference:")
with pytest.raises(WgsIdentityError, match="SHA256"):
build_wgs_analysis_id(
stage="wgs_qc",
upstream_sha256s=["not-a-sha"],
parameters={},
tool_versions={},
method="fastqc",
)
def test_wgs_sample_set_identity_rejects_parser_bypassing_invalid_records(tmp_path):
r1 = tmp_path / "S1_R1.fastq"
r2 = tmp_path / "S1_R2.fastq"
r1.write_bytes(b"r1")
r2.write_bytes(b"r2")
valid = {"sample_id": "S1", "r1": str(r1), "r2": str(r2)}
invalid_records = [
{**valid, "sample_id": "unsafe/id"},
{**valid, "r2": str(r1)},
{**valid, "r2": ""},
[valid, valid],
]
for invalid in invalid_records:
with pytest.raises(WgsIdentityError):
build_wgs_sample_set_id(invalid if isinstance(invalid, list) else [invalid])
with pytest.raises(WgsIdentityError, match="unique"):
build_wgs_sample_set_id([valid, valid])
def test_wgs_analysis_identity_canonicalizes_upstream_hash_order_and_requires_tool_evidence():
args = {
"stage": "wgs_variant_filter",
"parameters": {"qd": 2.0},
"tool_versions": {"gatk": "4.6"},
"method": "gatk",
}
assert build_wgs_analysis_id(**args, upstream_sha256s=["a" * 64, "b" * 64]) == build_wgs_analysis_id(
**args, upstream_sha256s=["b" * 64, "a" * 64]
)
for tool_versions in ({}, {"": "4.6"}, {"gatk": ""}):
with pytest.raises(WgsIdentityError, match="tool"):
build_wgs_analysis_id(**{**args, "tool_versions": tool_versions}, upstream_sha256s=["a" * 64])
@pytest.mark.parametrize("invalid_name", ["S1_R1.txt", "S1_R1.empty.fastq", "reads", "missing.fastq"])
def test_wgs_sample_set_identity_rejects_noncanonical_fastq_paths(tmp_path, invalid_name):
r2 = tmp_path / "S1_R2.fastq"
r2.write_bytes(b"r2")
r1 = tmp_path / invalid_name
if invalid_name.endswith(".txt"):
r1.write_bytes(b"r1")
elif invalid_name.endswith(".empty.fastq"):
r1.write_bytes(b"")
elif invalid_name == "reads":
r1.mkdir()
with pytest.raises(WgsIdentityError):
build_wgs_sample_set_id([{"sample_id": "S1", "r1": str(r1), "r2": str(r2)}])
def test_wgs_sample_set_identity_rejects_lexical_alias_mates(tmp_path):
r1 = tmp_path / "S1_R1.fastq"
r1.write_bytes(b"r1")
alias_dir = tmp_path / "alias"
alias_dir.mkdir()
lexical_alias = alias_dir / ".." / "S1_R1.fastq"
with pytest.raises(WgsIdentityError, match="different"):
build_wgs_sample_set_id([{"sample_id": "S1", "r1": str(r1), "r2": str(lexical_alias)}])
def test_wgs_sample_set_identity_rejects_symlink_alias_mates(tmp_path):
r1 = tmp_path / "S1_R1.fastq"
r1.write_bytes(b"r1")
symlink = tmp_path / "S1_R2.fastq"
symlink.symlink_to(r1)
with pytest.raises(WgsIdentityError, match="different"):
build_wgs_sample_set_id([{"sample_id": "S1", "r1": str(r1), "r2": str(symlink)}])