"""Genome off-target search — pure-function + injected-index coverage. NO NETWORK. We never hit NCBI/Ensembl here: the kmer-index search logic is exercised by injecting a synthetic in-memory index, and the index *builder* is exercised against a tiny temp FASTA on disk. """ import pytest from dee.core import offtarget as OT def _rc(s: str) -> str: return s.translate(str.maketrans("ACGT", "TGCA"))[::-1] @pytest.fixture def restore_kmer_cache(): """Snapshot + restore the module-level cache so injected test indexes never leak into other tests.""" snapshot = dict(OT._KMER_CACHE) try: yield finally: OT._KMER_CACHE.clear() OT._KMER_CACHE.update(snapshot) def test_revcomp(): assert OT._revcomp("ACGT") == "ACGT" assert OT._revcomp("ATGC") == "GCAT" def test_cfd_score_delegates_to_crispr_matrix(): assert OT._cfd_score("A" * 20, "A" * 20, "TGG") == 1.0 assert OT._cfd_score("A" * 20, "A" * 20, "AAA") == 0.0 def test_organism_readiness_and_status(): assert OT.is_organism_ready("ecoli") is True assert OT.is_organism_ready("human") is True assert OT.is_organism_ready("frog") is False assert OT.index_status("") == "n/a" assert OT.index_status("frog") == "unavailable" def test_index_status_building_vs_ready(restore_kmer_cache): OT._KMER_CACHE.pop("ecoli", None) assert OT.index_status("ecoli") == "building" OT._KMER_CACHE["ecoli"] = OT.KmerIndex("ecoli", 1, 0, {}) assert OT.index_status("ecoli") == "ready" TARGET = "ACGTACGTACGTACGTACGT" # 20 nt; seed (last 8) = "ACGTACGT" def _inject_index(spacer, pam="TGG", chrom="chrTest", pos=100, strand="+"): seed = spacer[-OT._SEED_LEN:] return OT.KmerIndex( organism="ecoli", n_chroms=1, n_sites=1, by_seed={seed: [(spacer, pam, chrom, pos, strand)]}, ) def test_perfect_match_offtarget_scores_one(restore_kmer_cache): OT._KMER_CACHE["ecoli"] = _inject_index(TARGET) hits = OT.find_genomic_offtargets(TARGET, "ecoli") assert len(hits) == 1 assert hits[0].cfd == pytest.approx(1.0) assert hits[0].n_mismatches == 0 assert hits[0].position_1 == 100 def test_single_distal_mismatch_still_found(restore_kmer_cache): OT._KMER_CACHE["ecoli"] = _inject_index(TARGET) guide = "C" + TARGET[1:] # mismatch at position 1 (distal) hits = OT.find_genomic_offtargets(guide, "ecoli") assert len(hits) == 1 assert hits[0].n_mismatches == 1 assert 0.0 < hits[0].cfd < 1.0 def test_many_seed_mismatches_not_found(restore_kmer_cache): OT._KMER_CACHE["ecoli"] = _inject_index(TARGET) # Change 3 bases inside the seed (last 8) → seed bucket miss # (search only covers ≤1 seed mismatch). guide = TARGET[:12] + _rc(TARGET[12:15]) + TARGET[15:] hits = OT.find_genomic_offtargets(guide, "ecoli") assert hits == [] def test_non_20nt_guide_returns_empty(restore_kmer_cache): OT._KMER_CACHE["ecoli"] = _inject_index(TARGET) assert OT.find_genomic_offtargets("ACGT", "ecoli") == [] def test_unsupported_organism_returns_empty(): assert OT.find_genomic_offtargets(TARGET, "frog") == [] def test_build_kmer_index_from_temp_fasta(tmp_path, restore_kmer_cache): spacer = "ACGTACGTACGTACGTACGT" seq = spacer + "AGG" + "TTTTTTTT" # forward NGG ("AGG") right after a 20-nt spacer fa = tmp_path / "tiny.fa" fa.write_text(">chr1\n" + seq + "\n") idx = OT._build_kmer_index("test", str(fa)) assert idx.n_sites >= 1 seed = spacer[-OT._SEED_LEN:] assert seed in idx.by_seed fwd = [e for bucket in idx.by_seed.values() for e in bucket if e[4] == "+"] assert any(e[0] == spacer and e[1] == "AGG" and e[3] == 1 for e in fwd)