use grangers::Grangers;
use std::path::Path;
fn gtf() -> Grangers {
Grangers::from_gtf(Path::new("tests/data/strand_phase.gtf"), true)
.expect("the GTF fixture should parse")
}
fn gff() -> Grangers {
Grangers::from_gff(Path::new("tests/data/strand_phase.gff3"), true)
.expect("the GFF3 fixture should parse")
}
fn col_opt(gr: &Grangers, name: &str) -> Vec<Option<String>> {
gr.df()
.column(name)
.unwrap_or_else(|e| panic!("column {name} should exist: {e}"))
.str()
.expect("column should be of string type")
.into_iter()
.map(|v| v.map(|s| s.to_string()))
.collect()
}
#[test]
fn gtf_row_count_and_columns() {
let gr = gtf();
assert_eq!(gr.df().height(), 15);
for c in ["seqname", "start", "end", "strand", "phase", "feature_type"] {
assert!(
gr.df().column(c).is_ok(),
"expected the {c} column to be present"
);
}
}
#[test]
fn gtf_strand_is_decoded_to_plus_and_minus() {
let strand = col_opt(>f(), "strand");
assert!(
strand
.iter()
.all(|s| matches!(s.as_deref(), Some("+") | Some("-"))),
"every strand should decode to + or -, got {strand:?}"
);
assert_eq!(strand[0].as_deref(), Some("+"));
assert_eq!(strand[8].as_deref(), Some("-"));
assert_eq!(
strand.iter().filter(|s| s.as_deref() == Some("+")).count(),
8
);
assert_eq!(
strand.iter().filter(|s| s.as_deref() == Some("-")).count(),
7
);
}
#[test]
fn gtf_phase_is_decoded_exactly() {
let gr = gtf();
let phase = col_opt(&gr, "phase");
let ftype = col_opt(&gr, "feature_type");
let cds: Vec<Option<String>> = ftype
.iter()
.zip(phase.iter())
.filter(|(f, _)| f.as_deref() == Some("CDS"))
.map(|(_, p)| p.clone())
.collect();
assert_eq!(
cds,
vec![
Some("0".to_string()),
Some("1".to_string()),
Some("2".to_string())
]
);
assert!(ftype
.iter()
.zip(phase.iter())
.filter(|(f, _)| f.as_deref() != Some("CDS"))
.all(|(_, p)| p.is_none()));
}
#[test]
fn gtf_coordinates_and_attributes_survive_parsing() {
let gr = gtf();
let start = gr.df().column("start").unwrap().i64().unwrap();
let end = gr.df().column("end").unwrap().i64().unwrap();
assert_eq!(start.get(0), Some(100));
assert_eq!(end.get(0), Some(2000));
let gene_ids = col_opt(&gr, "gene_id");
assert_eq!(gene_ids[0].as_deref(), Some("g1"));
assert_eq!(gene_ids[14].as_deref(), Some("g3"));
let names = col_opt(&gr, "gene_name");
assert_eq!(names[0].as_deref(), Some("GENE1"));
}
#[test]
fn gff_parses_and_decodes_the_same_way() {
let gr = gff();
assert_eq!(gr.df().height(), 14);
let ftype = col_opt(&gr, "feature_type");
let phase = col_opt(&gr, "phase");
let cds: Vec<Option<String>> = ftype
.iter()
.zip(phase.iter())
.filter(|(f, _)| f.as_deref() == Some("CDS"))
.map(|(_, p)| p.clone())
.collect();
assert_eq!(
cds,
vec![
Some("0".to_string()),
Some("1".to_string()),
Some("2".to_string())
]
);
}
#[test]
fn gff_coerces_unstranded_and_unknown_strand_to_plus() {
let gr = gff();
let strand = col_opt(&gr, "strand");
assert!(
strand
.iter()
.all(|s| matches!(s.as_deref(), Some("+") | Some("-"))),
"no strand should be null or exotic, got {strand:?}"
);
for (i, s) in strand.iter().enumerate().skip(10) {
assert_eq!(
s.as_deref(),
Some("+"),
"row {i} (a . or ? strand record) should be coerced to +"
);
}
}
#[test]
fn exons_and_introns_round_trip_through_the_polars_pipeline() {
let gr = gtf();
let exons = gr.exons(None, true).expect("exons should be derivable");
assert_eq!(exons.df().height(), 6, "the fixture has six exon records");
let introns = gr
.introns(None, None, None, true)
.expect("introns should be derivable");
assert_eq!(introns.df().height(), 3);
let start = introns.df().column("start").unwrap().i64().unwrap();
let end = introns.df().column("end").unwrap().i64().unwrap();
let mut spans: Vec<(i64, i64)> = (0..introns.df().height())
.map(|i| (start.get(i).unwrap(), end.get(i).unwrap()))
.collect();
spans.sort();
assert_eq!(spans, vec![(301, 799), (701, 1199), (1001, 1799)]);
}