|
| 1 | +//! A well-formed breakend has no END or SVLEN (VCF 4.2 §5.4: it is a point), so the |
| 2 | +//! coverage pass must not warn that one is missing. A symbolic SV that does need a span and |
| 3 | +//! lacks it must still warn (#497). |
| 4 | +//! |
| 5 | +//! The breakend case asserts the DEBUG line as well as the missing WARN: without it, a |
| 6 | +//! breakend that never reached the coverage pass would pass the test for the wrong reason. |
| 7 | +//! Both tests read the written log: the terminal logger sends WARN to stdout, not stderr. |
| 8 | +
|
| 9 | +mod common; |
| 10 | +use common::{GenReadsConfig, eidolon, fresh_workdir, h1n1_reference}; |
| 11 | +use std::io::Write as _; |
| 12 | + |
| 13 | +const MISSING_SPAN_WARN: &str = "has no END/SVLEN"; |
| 14 | +const BND_DEBUG: &str = "Breakend at 1-based POS 600: a point event"; |
| 15 | + |
| 16 | +fn write_vcf(path: &std::path::Path, records: &[&str]) { |
| 17 | + let mut f = std::fs::File::create(path).unwrap(); |
| 18 | + writeln!(f, "##fileformat=VCFv4.2").unwrap(); |
| 19 | + writeln!( |
| 20 | + f, |
| 21 | + "##FORMAT=<ID=GT,Number=1,Type=String,Description=\"Genotype\">" |
| 22 | + ) |
| 23 | + .unwrap(); |
| 24 | + writeln!( |
| 25 | + f, |
| 26 | + "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tS" |
| 27 | + ) |
| 28 | + .unwrap(); |
| 29 | + for r in records { |
| 30 | + writeln!(f, "{r}").unwrap(); |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +/// Runs gen-reads on `records` at debug log level and returns the written log. |
| 35 | +fn run(records: &[&str]) -> String { |
| 36 | + let (_dir, work) = fresh_workdir(); |
| 37 | + let vcf = work.join("in.vcf"); |
| 38 | + write_vcf(&vcf, records); |
| 39 | + let mut config = GenReadsConfig::new(h1n1_reference(), work.clone(), "run"); |
| 40 | + config.coverage = 5; |
| 41 | + config.produce_fastq = true; |
| 42 | + config.input_vcf = Some(vcf); |
| 43 | + config.sv_rate_scale = Some(0.0); // only the supplied records, no de novo SVs |
| 44 | + let yaml = config.write_yaml(); |
| 45 | + let log = work.join("run.log"); |
| 46 | + let out = eidolon() |
| 47 | + .args(["--log-level", "debug", "--log-dest"]) |
| 48 | + .arg(&log) |
| 49 | + .args(["gen-reads", "-c"]) |
| 50 | + .arg(yaml.path()) |
| 51 | + .output() |
| 52 | + .unwrap(); |
| 53 | + assert!( |
| 54 | + out.status.success(), |
| 55 | + "gen-reads failed: {}", |
| 56 | + String::from_utf8_lossy(&out.stderr) |
| 57 | + ); |
| 58 | + std::fs::read_to_string(&log).unwrap() |
| 59 | +} |
| 60 | + |
| 61 | +#[test] |
| 62 | +fn a_breakend_without_end_or_svlen_does_not_warn() { |
| 63 | + // REF bases are the actual reference at those positions. |
| 64 | + let log = run(&[ |
| 65 | + "H1N1_HA\t600\t.\tT\tT[H1N1_PB2:900[\t60\tPASS\tSVTYPE=BND\tGT\t1/1", |
| 66 | + "H1N1_PB2\t900\t.\tG\t]H1N1_HA:600]G\t60\tPASS\tSVTYPE=BND\tGT\t1/1", |
| 67 | + ]); |
| 68 | + assert!( |
| 69 | + log.contains(BND_DEBUG), |
| 70 | + "the breakend must reach the coverage pass and be skipped there:\n{log}" |
| 71 | + ); |
| 72 | + assert!( |
| 73 | + !log.contains(MISSING_SPAN_WARN), |
| 74 | + "a breakend is not missing a span:\n{log}" |
| 75 | + ); |
| 76 | +} |
| 77 | + |
| 78 | +#[test] |
| 79 | +fn a_deletion_without_end_or_svlen_still_warns() { |
| 80 | + let log = run(&["H1N1_HA\t600\t.\tT\t<DEL>\t60\tPASS\tSVTYPE=DEL\tGT\t1/1"]); |
| 81 | + assert!( |
| 82 | + log.contains(&format!( |
| 83 | + "Symbolic SV at 1-based POS 600 {MISSING_SPAN_WARN}" |
| 84 | + )), |
| 85 | + "a <DEL> with no span is under-specified and must say so:\n{log}" |
| 86 | + ); |
| 87 | +} |
0 commit comments