Spaces:
Running
Running
| """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] | |
| 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) | |