igv-rs 0.7.0

Interactive terminal genome viewer for FASTA / VCF / BAM / GFF / BED / bigWig / BEDPE. Includes a bundled-igv.js browser companion.
Documentation
use std::sync::Arc;

use async_trait::async_trait;
use igv_core::region::Region;
use igv_core::render::RenderMode;
use igv_core::source::link::{
    FetchLinkOpts, LinkRecord, LinkScope, LinkSource, VisibleLink,
};
use igv_core::source::{FetchOpts, RefMeta};
use igv_rs::app::loader::{LoadRequest, LoadResult, Loader};

#[derive(Debug)]
struct StubLink {
    name: String,
    out: Vec<VisibleLink>,
    count: usize,
}

#[async_trait]
impl LinkSource for StubLink {
    async fn query(
        &self,
        _region: &Region,
        _opts: &FetchLinkOpts,
    ) -> igv_core::error::Result<Vec<VisibleLink>> {
        Ok(self.out.clone())
    }
    fn display_name(&self) -> &str {
        &self.name
    }
    fn record_count(&self) -> usize {
        self.count
    }
}

#[derive(Debug)]
struct StubFasta;
#[async_trait]
impl igv_core::source::FastaSource for StubFasta {
    async fn references(&self) -> igv_core::error::Result<Vec<RefMeta>> {
        Ok(vec![RefMeta { name: "chr1".into(), length: 1_000_000 }])
    }
    async fn fetch(&self, _r: &Region) -> igv_core::error::Result<Vec<u8>> {
        Ok(Vec::new())
    }
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn dispatch_emits_link_results_per_track() {
    let r = LinkRecord {
        chrom_a: Arc::from("chr1"),
        start_a: 100,
        end_a: 200,
        chrom_b: Arc::from("chr1"),
        start_b: 300,
        end_b: 400,
        name: Some("loop".into()),
        score: Some(1.0),
        strand_a: igv_core::source::annotation::Strand::Forward,
        strand_b: igv_core::source::annotation::Strand::Reverse,
    };
    let v = vec![VisibleLink { record: r, scope: LinkScope::BothIn }];
    let link_a: Arc<dyn LinkSource> = Arc::new(StubLink {
        name: "a".into(),
        out: v.clone(),
        count: 1,
    });
    let link_b: Arc<dyn LinkSource> = Arc::new(StubLink {
        name: "b".into(),
        out: vec![],
        count: 0,
    });

    let (tx, mut rx) = tokio::sync::mpsc::channel::<LoadResult>(16);
    let mut loader = Loader::new(
        Arc::new(StubFasta),
        None,
        vec![],
        vec![],
        vec![],
        vec![link_a, link_b],
        tx,
    );

    loader.dispatch(LoadRequest {
        generation: 1,
        region: Region::new("chr1", 1, 1000).unwrap(),
        fetch_opts: FetchOpts::default(),
        signal_max_bins: 100,
        link_min_score: None,
        render_mode: RenderMode::DetailedReads,
    });

    let mut got_a = false;
    let mut got_b = false;
    while let Some(msg) =
        tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv())
            .await
            .ok()
            .flatten()
    {
        if let LoadResult::Link { generation, track_index, visible, total_record_count } = msg {
            assert_eq!(generation, 1);
            match track_index {
                0 => {
                    assert_eq!(visible.len(), 1);
                    assert_eq!(total_record_count, 1);
                    got_a = true;
                }
                1 => {
                    assert!(visible.is_empty());
                    assert_eq!(total_record_count, 0);
                    got_b = true;
                }
                _ => panic!("unexpected track_index {track_index}"),
            }
        }
        if got_a && got_b {
            break;
        }
    }
    assert!(got_a && got_b, "missing link result(s)");
}