use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use crate::hgvs::variant::Accession;
use crate::reference::provider::GenomicPlacement;
use crate::reference::transcript::Transcript;
use crate::{FerroError, HgvsVariant, ReferenceProvider};
#[derive(Debug, Default)]
struct Audit {
failures: Mutex<Vec<String>>,
successes: AtomicUsize,
}
#[derive(Debug)]
pub struct AuditedProvider<P> {
inner: P,
audit: Arc<Audit>,
}
impl<P: Clone> Clone for AuditedProvider<P> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
audit: Arc::clone(&self.audit),
}
}
}
impl<P> AuditedProvider<P> {
pub fn new(inner: P) -> Self {
Self {
inner,
audit: Arc::new(Audit::default()),
}
}
pub fn inner(&self) -> &P {
&self.inner
}
pub fn failures(&self) -> Vec<String> {
self.audit.failures.lock().expect("audit lock").clone()
}
pub fn successful_reads(&self) -> usize {
self.audit.successes.load(Ordering::Relaxed)
}
pub fn reset(&self) {
self.audit.failures.lock().expect("audit lock").clear();
self.audit.successes.store(0, Ordering::Relaxed);
}
fn record<T>(
&self,
what: impl FnOnce() -> String,
outcome: Result<T, FerroError>,
) -> Result<T, FerroError> {
match &outcome {
Ok(_) => {
self.audit.successes.fetch_add(1, Ordering::Relaxed);
}
Err(e) => self
.audit
.failures
.lock()
.expect("audit lock")
.push(format!("{}: {e}", what())),
}
outcome
}
}
impl<P: ReferenceProvider> ReferenceProvider for AuditedProvider<P> {
fn get_transcript(&self, id: &str) -> Result<Arc<Transcript>, FerroError> {
let outcome = self.inner.get_transcript(id);
self.record(|| format!("get_transcript({id})"), outcome)
}
fn get_transcript_for_variant(
&self,
variant: &HgvsVariant,
) -> Result<Arc<Transcript>, FerroError> {
let outcome = self.inner.get_transcript_for_variant(variant);
self.record(|| format!("get_transcript_for_variant({variant})"), outcome)
}
fn get_transcript_for_accession(
&self,
accession: &Accession,
) -> Result<Arc<Transcript>, FerroError> {
let outcome = self.inner.get_transcript_for_accession(accession);
self.record(
|| format!("get_transcript_for_accession({})", accession.full()),
outcome,
)
}
fn get_sequence(&self, id: &str, start: u64, end: u64) -> Result<String, FerroError> {
let outcome = self.inner.get_sequence(id, start, end);
self.record(|| format!("get_sequence({id}, {start}, {end})"), outcome)
}
fn get_genomic_sequence(
&self,
contig: &str,
start: u64,
end: u64,
) -> Result<String, FerroError> {
let outcome = self.inner.get_genomic_sequence(contig, start, end);
self.record(
|| format!("get_genomic_sequence({contig}, {start}, {end})"),
outcome,
)
}
fn genomic_placement(&self, parent: &Accession) -> Option<GenomicPlacement> {
self.inner.genomic_placement(parent)
}
fn genomic_placement_on_build(
&self,
parent: &Accession,
build: Option<&str>,
) -> Option<GenomicPlacement> {
self.inner.genomic_placement_on_build(parent, build)
}
fn infer_genome_build(&self, accession: &Accession) -> Option<&'static str> {
self.inner.infer_genome_build(accession)
}
fn resolve_legacy_gene_selector(
&self,
selector: &str,
ng_parent: Option<&Accession>,
) -> Option<String> {
self.inner.resolve_legacy_gene_selector(selector, ng_parent)
}
fn sole_hosted_transcript(&self, ng_parent: &Accession) -> Option<String> {
self.inner.sole_hosted_transcript(ng_parent)
}
fn has_transcript(&self, id: &str) -> bool {
self.inner.has_transcript(id)
}
fn has_transcript_version_exact(&self, id: &str) -> bool {
self.inner.has_transcript_version_exact(id)
}
fn has_genomic_data(&self) -> bool {
self.inner.has_genomic_data()
}
fn get_protein_sequence(
&self,
accession: &str,
start: u64,
end: u64,
) -> Result<String, FerroError> {
self.inner.get_protein_sequence(accession, start, end)
}
fn get_protein_length(&self, accession: &str) -> Result<u64, FerroError> {
self.inner.get_protein_length(accession)
}
fn has_protein_data(&self) -> bool {
self.inner.has_protein_data()
}
fn get_sequence_length(&self, id: &str) -> Result<u64, FerroError> {
self.inner.get_sequence_length(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reference::mock::MockProvider;
use crate::reference::transcript::{Exon, GenomeBuild, ManeStatus, Strand};
fn provider() -> AuditedProvider<MockProvider> {
let mut inner = MockProvider::new();
inner.add_transcript(Transcript::new(
"NM_TEST.1".to_string(),
Some("TEST".to_string()),
Strand::Plus,
"ACGTACGTACGT".to_string(),
Some(1),
Some(12),
vec![Exon::new(1, 1, 12)],
None,
None,
None,
GenomeBuild::default(),
ManeStatus::default(),
None,
None,
));
AuditedProvider::new(inner)
}
#[test]
fn a_served_read_is_counted_and_not_reported() {
let audited = provider();
assert_eq!(audited.get_sequence("NM_TEST.1", 0, 4).unwrap(), "ACGT");
assert_eq!(audited.successful_reads(), 1);
assert!(audited.failures().is_empty());
}
#[test]
fn a_failed_read_is_reported_and_not_counted_as_served() {
let audited = provider();
assert!(audited.get_transcript("NM_ABSENT.1").is_err());
assert_eq!(audited.successful_reads(), 0);
let failures = audited.failures();
assert_eq!(failures.len(), 1);
assert!(
failures[0].starts_with("get_transcript(NM_ABSENT.1): "),
"a failure must name the call that made it: {:?}",
failures[0]
);
}
#[test]
fn a_clone_shares_the_tally() {
let audited = provider();
let clone = audited.clone();
let _ = clone.get_sequence("NM_TEST.1", 0, 4);
let _ = clone.get_sequence("NM_ABSENT.1", 0, 4);
assert_eq!(audited.successful_reads(), 1);
assert_eq!(audited.failures().len(), 1);
}
#[test]
fn reset_clears_both_halves_of_the_tally() {
let audited = provider();
let _ = audited.get_sequence("NM_TEST.1", 0, 4);
let _ = audited.get_sequence("NM_ABSENT.1", 0, 4);
audited.reset();
assert_eq!(audited.successful_reads(), 0);
assert!(audited.failures().is_empty());
}
#[test]
fn a_protein_length_probe_is_not_reported_as_a_missing_window() {
let audited = provider();
assert!(!audited.has_protein_data());
assert_eq!(audited.get_protein_length("NP_TEST.1").ok(), Some(0));
assert!(
audited.failures().is_empty(),
"protein probes must not be reported as failed reference reads: {:?}",
audited.failures()
);
}
#[test]
fn an_absent_sequence_length_is_not_reported_as_a_missing_window() {
let audited = provider();
assert!(audited.get_sequence_length("NC_ABSENT.1").is_err());
assert!(audited.failures().is_empty());
}
#[test]
fn existence_probes_are_forwarded_rather_than_defaulted() {
let audited = provider();
assert!(audited.has_transcript("NM_TEST.1"));
assert!(!audited.has_transcript("NM_ABSENT.1"));
assert!(audited.has_genomic_data() == audited.inner().has_genomic_data());
assert!(audited.failures().is_empty());
}
}