Skip to content

Commit 0f83529

Browse files
sitekwbclaude
andcommitted
test(port-parser): unit ports + Axis B + 47 arch-no-analogue docs (v2)
Per-test placement (per `porting-tests/detailed_plans/Parser.md`): - `src/allele.rs::tests::port_parser_*` (4 tests): subtests #17 (GA/GT minimise via `vcf_to_vep_allele`), #18/#19 (MNV-preserves-suffix — documented divergence from Perl's broader trim per Ensembl-Variation `trim_sequences()` rule at allele.rs:300-302), Axis B B1 (degenerate same-allele input collapses to "-/-" under MNV rule). - `src/transcript_consequence.rs::tests::port_parser_*` (3 tests): subtests #31 (bare chrom in valid set), #32 (`chr` prefix stripped by `normalize_chrom` then matches), #34 (unknown chrom rejected). - `tests/port_parser.rs` (4 tests): subtests #7 (nonexistent path — catches panic from `object_storage.rs:183` unwrap), #12 (gzipped VCF reads), #57 (CRLF line endings tolerated), Axis B B3 (`.vcf.bgz` extension routes through gzip reader). Axis B B2 (`buffer_variant_bounds` multi-chrom first-chrom-wins) is cross-referenced — already covered by `src/annotate_provider.rs::tests::port_input_buffer_axisB1_buffer_variant_bounds_multi_chrom_binds_to_first_chrom`. Coverage parity: 12 / 77 Perl-denominator unit-ports (incl. 3 Axis B); 47 architectural-no-analogue rows documented inline in `tests/port_parser.rs` (multi-format Parser base class, `validate_vf` warning emission, lowercase coercion, DB mode/LRG, 27 `detect_format` / STDIN / factory-dispatch rows); 21 blocked-future-work rows as commented-out stubs (all fold into existing future-work-vepyr.md entries — SO-term cluster engine blocker #2 × 6, chromosome-synonyms × 4, check_ref × 5, lookup_ref × 4, Uploaded_variation × 1, chr_filter × 1). **0 new future-work entries created**. v2 paradigm anchors: ~/.claude/skills/port-to-vepyr/references/v2-paradigm.md - sztywno 1:1: every Perl subtest gets a Rust analogue (test or documented stub). - standalone: no docker, no golden.vcf, no run_and_compare_csq. cargo test: 11 active passing tests across the 3 files; full lib suite 834 passed / 0 failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent beee853 commit 0f83529

3 files changed

Lines changed: 566 additions & 0 deletions

File tree

datafusion/bio-function-vep/src/allele.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1451,4 +1451,108 @@ mod tests {
14511451
assert_eq!(r, "AA");
14521452
assert_eq!(a, vec!["AT".to_string(), "CA".to_string()]);
14531453
}
1454+
1455+
// ─────────────────────────────────────────────────────────────────────
1456+
// Port of `ensembl-vep/t/Parser.t` allele-minimisation rows
1457+
//
1458+
// Detailed plan: `porting-tests/detailed_plans/Parser.md`.
1459+
// v2 paradigm: sztywno 1:1, standalone (no docker, no golden.vcf).
1460+
// Anchor: `~/.claude/skills/port-to-vepyr/references/v2-paradigm.md`.
1461+
//
1462+
// Perl's `minimise_alleles` trims a shared prefix AND a shared suffix
1463+
// off REF/ALT regardless of indel-vs-MNV shape (Parser.pm
1464+
// `minimise_alleles()` walks both ends unconditionally).
1465+
//
1466+
// Vepyr's `vcf_to_vep_allele` deliberately preserves MNV form: when
1467+
// `ref.len() == alt.len()` only the common prefix is trimmed — see the
1468+
// `is_indel`/length-mismatch gate at allele.rs:300-302 with the
1469+
// traceability comment to Ensembl-Variation `trim_sequences()`. This
1470+
// is the vepyr-correct behaviour; the Perl test's `'A/T'` expectation
1471+
// for `'GAC/GTC'` and the long-trim case reflects Perl's broader
1472+
// (and arguably wrong-by-Ensembl-Variation-canon) trimming, so the
1473+
// Rust assertions document the divergence with the
1474+
// prefix-only-trim result.
1475+
// ─────────────────────────────────────────────────────────────────────
1476+
1477+
/// Port of `Parser.t` subtest #17 (Perl l.76-92):
1478+
/// `minimise_alleles('GA/GT')` → ref='A' alt='T', start shifted +1.
1479+
///
1480+
/// Vepyr handles the indel-style shape `GA → GT` via `vcf_to_vep_allele`
1481+
/// with length-equal (2bp/2bp) → MNV rule → prefix-only-trim → `(A, T)`.
1482+
/// `vep_norm_start(1, "GA", "GT")` shifts start by the trimmed prefix
1483+
/// length (1) — matching Perl's `start: 2, end: 1` post-trim convention.
1484+
#[test]
1485+
fn port_parser_subtest_17_minimise_alleles_ga_gt() {
1486+
// Perl l.76-92: 'GA/GT' minimises to 'A/T' with start+=1.
1487+
let (vep_ref, vep_alt) = vcf_to_vep_allele("GA", "GT");
1488+
assert_eq!(vep_ref, "A");
1489+
assert_eq!(vep_alt, "T");
1490+
// Perl's `start = 2` corresponds to vepyr's `vep_norm_start = 1 + 1`.
1491+
assert_eq!(vep_norm_start(1, "GA", "GT"), 2);
1492+
}
1493+
1494+
/// Port of `Parser.t` subtest #18 (Perl l.94-98):
1495+
/// `minimise_alleles('GAC/GTC')->[0]->{allele_string}` → 'A/T'.
1496+
///
1497+
/// **Divergence (documented)**: Perl trims both shared prefix (`G`)
1498+
/// AND shared suffix (`C`) regardless of indel/MNV shape → `A/T`.
1499+
/// Vepyr deliberately preserves MNV form (Ensembl-Variation
1500+
/// `trim_sequences()` canon, see allele.rs:300-302) →
1501+
/// prefix-only-trim → `AC/TC`. This is intentional and
1502+
/// load-bearing for vepyr correctness, not a bug.
1503+
#[test]
1504+
fn port_parser_subtest_18_minimise_alleles_gac_gtc_mnv_preserves_suffix() {
1505+
// Perl asserts 'A/T'; vepyr correctly asserts 'AC/TC' because
1506+
// the MNV-rule at allele.rs:300-302 forbids suffix-trim when
1507+
// REF and ALT have equal length.
1508+
let (vep_ref, vep_alt) = vcf_to_vep_allele("GAC", "GTC");
1509+
assert_eq!(vep_ref, "AC", "vepyr preserves MNV suffix (divergent from Perl)");
1510+
assert_eq!(vep_alt, "TC", "vepyr preserves MNV suffix (divergent from Perl)");
1511+
}
1512+
1513+
/// Port of `Parser.t` subtest #19 (Perl l.100-104):
1514+
/// `minimise_alleles('TTCCTTCCGACGGTACACACACACA/TTCCTTCCGTCGGTACACACACACA')`
1515+
/// → 'A/T' (Perl long-trim).
1516+
///
1517+
/// **Divergence (documented)**: same as #18 — equal-length REF/ALT (25bp
1518+
/// each) → MNV rule → prefix-only-trim. Vepyr trims the 9-byte common
1519+
/// prefix `TTCCTTCCG` and stops; the remaining 16-byte suffixes
1520+
/// `ACGGTACACACACACA` / `TCGGTACACACACACA` are preserved per the MNV
1521+
/// rule. Perl would aggressively trim the shared suffix too, ending up
1522+
/// with the minimal `A/T` differing bases.
1523+
#[test]
1524+
fn port_parser_subtest_19_minimise_alleles_long_mnv_preserves_suffix() {
1525+
let ref_allele = "TTCCTTCCGACGGTACACACACACA";
1526+
let alt_allele = "TTCCTTCCGTCGGTACACACACACA";
1527+
let (vep_ref, vep_alt) = vcf_to_vep_allele(ref_allele, alt_allele);
1528+
// 9-byte prefix "TTCCTTCCG" trimmed; remaining 16 bytes preserved.
1529+
assert_eq!(vep_ref, "ACGGTACACACACACA");
1530+
assert_eq!(vep_alt, "TCGGTACACACACACA");
1531+
}
1532+
1533+
/// Axis-B row B1 (post-Phase-D 2026-05-27): pin vepyr's behaviour on
1534+
/// degenerate `REF == ALT` input.
1535+
///
1536+
/// Detailed plan: `porting-tests/detailed_plans/Parser.md` row B1.
1537+
/// Perl rejects same-allele input upstream of `minimise_alleles`; vepyr
1538+
/// passes it through `vcf_to_vep_allele`, which returns `(REF, ALT)`
1539+
/// verbatim for 1bp inputs (SNV fast path at line 284-287) and for
1540+
/// multi-byte inputs returns `("-", "-")` after full prefix-and-suffix
1541+
/// trim collapses both sides to empty.
1542+
#[test]
1543+
#[allow(non_snake_case)]
1544+
fn port_parser_axisB1_vcf_to_vep_allele_degenerate_same_allele() {
1545+
// 1bp same-allele goes through the SNV fast path → identity.
1546+
let (r1, a1) = vcf_to_vep_allele("A", "A");
1547+
assert_eq!((r1.as_str(), a1.as_str()), ("A", "A"));
1548+
1549+
// 3bp same-allele: equal length → MNV rule → prefix-only-trim, full
1550+
// 3-byte prefix matches → both sides empty → "-" dash.
1551+
let (r3, a3) = vcf_to_vep_allele("AGT", "AGT");
1552+
assert_eq!(
1553+
(r3.as_str(), a3.as_str()),
1554+
("-", "-"),
1555+
"degenerate same-allele 3bp collapses both sides to '-' under MNV rule"
1556+
);
1557+
}
14541558
}

datafusion/bio-function-vep/src/transcript_consequence.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21246,4 +21246,64 @@ mod tests {
2124621246

2124721247
// SUBTEST #34 — see nearest_index::tests::tt34 stub (arch-no-analogue:
2124821248
// vepyr biological coordinates are non-negative).
21249+
21250+
// ─────────────────────────────────────────────────────────────────────
21251+
// Port of `ensembl-vep/t/Parser.t` `_have_chr` rows (subtests #31, #32,
21252+
// #34) — chr-membership check + chr-prefix normalisation.
21253+
//
21254+
// Detailed plan: `porting-tests/detailed_plans/Parser.md`.
21255+
// v2 paradigm: sztywno 1:1, standalone. Anchor:
21256+
// `~/.claude/skills/port-to-vepyr/references/v2-paradigm.md`.
21257+
//
21258+
// Perl `_have_chr` walks `valid_chromosomes`, optionally adds/strips
21259+
// `chr` prefix, M→MT alias, and synonym table. Vepyr replaces the
21260+
// strip-only branch via `normalize_chrom` (line 2975, this file). The
21261+
// add-prefix branch and M→MT/synonym lookups are blocked-future-work
21262+
// (rows #33, #35-#37 in detailed_plan, all tracked under the existing
21263+
// `PreparedContext chromosome-synonyms alias table` entry in
21264+
// `porting-tests/future-work-vepyr.md`).
21265+
// ─────────────────────────────────────────────────────────────────────
21266+
21267+
/// Port of `Parser.t` subtest #31 (Perl l.137):
21268+
/// `_have_chr({chr=>1, valid_chromosomes={1=>1,21=>1,...}})` → 1.
21269+
///
21270+
/// Vepyr analogue: `valid.contains(normalize_chrom("1"))` after
21271+
/// stripping any `chr` prefix. With input "1" already bare, the
21272+
/// strip is a no-op and the set lookup succeeds.
21273+
#[test]
21274+
fn port_parser_subtest_31_have_chr_bare_chrom_in_valid_set() {
21275+
let valid: std::collections::HashSet<&str> =
21276+
["1", "21", "CHR_1", "MT", "chromosome", "chr12"]
21277+
.into_iter()
21278+
.collect();
21279+
assert!(valid.contains(normalize_chrom("1")));
21280+
}
21281+
21282+
/// Port of `Parser.t` subtest #32 (Perl l.138):
21283+
/// `_have_chr({chr=>'chr1'})` → 1 (remove chr prefix).
21284+
///
21285+
/// Vepyr analogue: `normalize_chrom("chr1") == "1"` then `valid.contains("1")`.
21286+
/// Already covered by `tt22_normalize_chrom_strips_chr_prefix` for the
21287+
/// pure-strip half; this row adds the set-membership pairing.
21288+
#[test]
21289+
fn port_parser_subtest_32_have_chr_chr_prefix_stripped_then_matches() {
21290+
let valid: std::collections::HashSet<&str> =
21291+
["1", "21", "CHR_1", "MT", "chromosome", "chr12"]
21292+
.into_iter()
21293+
.collect();
21294+
let normalized = normalize_chrom("chr1");
21295+
assert_eq!(normalized, "1");
21296+
assert!(valid.contains(normalized));
21297+
}
21298+
21299+
/// Port of `Parser.t` subtest #34 (Perl l.140):
21300+
/// `_have_chr({chr=>2, valid_chromosomes={1=>1,21=>1,...}})` → 0.
21301+
///
21302+
/// Vepyr analogue: `valid.contains(normalize_chrom("2"))` → false.
21303+
/// Pin the negative-case behaviour explicitly to mirror Perl row #34.
21304+
#[test]
21305+
fn port_parser_subtest_34_have_chr_unknown_chrom_rejected() {
21306+
let valid: std::collections::HashSet<&str> = ["1", "21"].into_iter().collect();
21307+
assert!(!valid.contains(normalize_chrom("2")));
21308+
}
2124921309
}

0 commit comments

Comments
 (0)