Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion .github/workflows/guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,29 @@ jobs:
grep -E 'planted_failure.*FAILED' "$log" || { cat "$log"; echo "ERROR: the run failed, but not on the planted test"; exit 1; }
echo "Control: the planted test failed, as it must."

- name: Traits controls (diplotype order, REF check and missing phase rows)
env:
CARGO_BUILD_JOBS: 3
run: |
src=crates/og-steps/src/traits.rs
saved="${RUNNER_TEMP}/traits.rs"
log="${RUNNER_TEMP}/traits-controls.log"
cp -f "$src" "$saved"
trap 'cp -f "$saved" "$src"' EXIT
perl -0pi -e 's/(fn allowed\b.*?)(\n out\n)/$1\n out.reverse();$2/s; s/if c\[2\]\.chars\(\)\.next\(\) != s\.row\["ref"\]\.chars\(\)\.next\(\)/if c[2].is_empty()/; s/(fn phase_vcf\b.*?)( let mut out = AtomicFile::create\(path\))/$1 sites.clear();\n$2/s' "$src"
cmp -s "$saved" "$src" && { echo "ERROR: no traits control was planted"; exit 1; }
if cargo test --locked -p og-steps traits::tests --no-fail-fast > "$log" 2>&1; then
cat "$log"
echo "ERROR: traits accepted the planted faults"
exit 1
fi
cat "$log"
for test in apoe_grid_keeps_haplotype_order_and_missing_calls stream_prefers_first_snv_but_checks_every_ref_and_bounds_memory phase_requires_one_block_and_only_multiple_hets_are_exported; do
grep -E "traits::tests::${test} .*FAILED" "$log" || { echo "ERROR: $test did not catch its fault"; exit 1; }
done
cp -f "$saved" "$src"
trap - EXIT

- name: cargo test
run: cargo test --workspace --locked --no-fail-fast

Expand Down Expand Up @@ -351,7 +374,7 @@ jobs:
tests/test_results_index.py \
tests/test_pgx_outside_calls.py tests/test_hla_second_caller.py tests/test_paraphase_report.py \
tests/test_copy_number.py tests/test_hba_copy_number.py \
tests/test_lpa_kiv2.py tests/test_traits_report.py tests/test_callability.py; do
tests/test_lpa_kiv2.py tests/test_traits_report.py tests/test_traits_table.py tests/test_genotype_rules_indels.py tests/test_callability.py; do
rc=0
echo "::group::${t}"
case "$t" in
Expand All @@ -363,6 +386,43 @@ jobs:
done
exit "$fail"

- name: Traits independent expectations reject changed reference labels and marker metadata
working-directory: .
env:
OG: ${{ github.workspace }}/bin/og
OG_REQUIRED: 1
run: |
python3 - <<'PY'
import contextlib, io, pathlib, runpy, tempfile
test = runpy.run_path("tests/test_traits_report.py")["main"].__globals__
original = pathlib.Path(test["RULES"]).read_text()
with tempfile.TemporaryDirectory() as work:
for rid, key, label in [("APOL1", "G0/G0", "G0/G0"), ("CAD9p21", "nonrisk/nonrisk", "no risk copy")]:
path = pathlib.Path(work) / "rules.tsv"
old = f"combination\t{rid}\t{key}\t{label}\t"
assert old in original
path.write_text(original.replace(old, f"combination\t{rid}\t{key}\tWRONG_REFERENCE\t"))
test["RULES"], test["FAILS"] = str(path), 0
log = io.StringIO()
with contextlib.redirect_stdout(log):
rc = test["main"]()
print(log.getvalue())
assert rc == 1 and test["FAILS"] == 1
assert f"[FAIL] {rid}, reference blocks only: {label}" in log.getvalue()
print(f"Control: {rid} WRONG_REFERENCE failed its independent expectation")
marker = test["tr"].MARKERS["SMN1_27134"]
rs, chrom, pos, ref, alt = marker["dbsnp"]
marker["dbsnp"] = rs, chrom, pos + 1, ref, alt
test["FAILS"] = 0
log = io.StringIO()
with contextlib.redirect_stdout(log):
test["marker_checks"]()
print(log.getvalue())
assert test["FAILS"] == 1
assert "[FAIL] marker SMN1_27134: the pinned dbSNP record agrees" in log.getvalue()
print("Control: retained SMN1 off-copy metadata failed its independent expectation")
PY

# The fake-docker job builds no og, so its cn-engine-calls case says it
# is skipped there; here bin/og is built and the case must run.
- name: The fake-docker case cn-engine-calls (steps 35, 41 and 46 through og cn) with og
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Each release's notes. From 1.0.0 on, a major or minor release has hand-written n

## Unreleased

- Step 42 reads traits and genotype rules with `og traits table`, exports WhatsHap sites with `og traits phase-sites`, and checks the catalogs with `og traits check`. The replaced Python evaluator is removed. Report columns and readings stay the same. Genotypes outside the catalog are streamed past, and write errors now report a controlled error with the path instead of a traceback. Both tables are staged before either is published. A staging failure or a directory destination leaves both previous tables intact. Each file is then replaced by a separate atomic rename. The pair is not a transaction: a failure of the second rename can leave the first table updated. Catalogs and output remain plain TSV; quotes never hide a tab or bypass validation.
- The unused Python copy-number engine is removed. Its historical catalog tests materialise a pinned test oracle and keep testing the Rust commands.

- The Rust summary's CHIP reader marks a depth table with NaN unreadable instead of sorting it into a usable sensitivity estimate. Its unindexed APOL1 G2 reader keeps only records overlapping the deletion span and stops past that span on sorted input.

- The Rust `og` binary now reads the rest of the summary's own sections: `og summary collect --only c`. It reads 32 sections of summary.json (couple, parascopy, paralogs, hla, prs, ancestry_fine, manta, delly, cnvpytor, sv_consensus, annotsv, expansions and their readings, telomere, roh, haplogroup, y_haplogroup, y_tree, y_str, consumer_export, bloodtype, lpa, vntyper, locityper, mito, traits with the APOL1 G2 call, archaic, ancient, viral, exomiser, chip, mei and mosaic). Nothing calls it yet: the reports still read the summary that `bin/collect_summary.py` writes. The sections are the same as the Python's on 15 new parity cases, compared as JSON with key order. The readers run on a thread pool, one per section. On a synthetic sample folder with every section and a 1,000,000-record VCF it takes 0.33 s and 34 MB, where the Python readers took 0.33 s and 41 to 59 MB. What differs from the Python:
Expand Down
Loading
Loading