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)}])