129 lines
4.6 KiB
Bash
129 lines
4.6 KiB
Bash
#!/usr/bin/env bash
|
|||
|
|
# smoke_test_index.sh — end-to-end smoke test of `obikmer index` + `obikmer query`
|
||
|
|
#
|
||
|
|
# The unit/integration test suite never exercises obikmer's real,
|
||
|
|
# file-driven CLI path (every test builds indexes by calling library APIs
|
||
|
|
# directly — see DevDocMD/implementation/partition_layer_cache.md, "(3)
|
||
|
|
# done"/"(6) done": a green `cargo test --workspace` has twice missed a
|
||
|
|
# real bug in this exact path). This script is the fast, repeatable
|
||
|
|
# substitute for hand-rolling that check each time.
|
||
|
|
#
|
||
|
|
# What it does:
|
||
|
|
# 1. builds `obikmer` (debug, via `cargo run`)
|
||
|
|
# 2. generates a small deterministic random FASTA
|
||
|
|
# 3. runs `obikmer index` on it
|
||
|
|
# 4. picks a real k-mer from the source sequence, avoiding low-complexity
|
||
|
|
# substrings (a homopolymer run tripped up an earlier manual run of
|
||
|
|
# this check — it gets rejected by query's own entropy filter, which
|
||
|
|
# looks like a bug but isn't one)
|
||
|
|
# 5. runs `obikmer query` and checks the k-mer round-trips
|
||
|
|
# 6. prints total-kmers-indexed and a clear PASS/FAIL, exit code matches
|
||
|
|
#
|
||
|
|
# Usage:
|
||
|
|
# scripts/smoke_test_index.sh [-k KMER_SIZE] [-m MINIMIZER_SIZE] [-p PARTITIONS] [--keep]
|
||
|
|
#
|
||
|
|
# --keep leaves the temp directory in place (path printed) instead of
|
||
|
|
# deleting it on exit, for manual inspection of a failure.
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
K=11
|
||
|
|
M=5
|
||
|
|
PARTITIONS=4
|
||
|
|
KEEP=0
|
||
|
|
|
||
|
|
while [ $# -gt 0 ]; do
|
||
|
|
case "$1" in
|
||
|
|
-k) K="$2"; shift 2 ;;
|
||
|
|
-m) M="$2"; shift 2 ;;
|
||
|
|
-p) PARTITIONS="$2"; shift 2 ;;
|
||
|
|
--keep) KEEP=1; shift ;;
|
||
|
|
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
||
|
|
esac
|
||
|
|
done
|
||
|
|
|
||
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
|
|
WORK="$(mktemp -d -t obikmer_smoke.XXXXXX)"
|
||
|
|
|
||
|
|
cleanup() {
|
||
|
|
if [ "$KEEP" -eq 1 ]; then
|
||
|
|
echo "kept: $WORK"
|
||
|
|
else
|
||
|
|
rm -rf "$WORK"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
trap cleanup EXIT
|
||
|
|
|
||
|
|
fail() {
|
||
|
|
echo "FAIL: $1" >&2
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
|
||
|
|
# ── 1. generate a small deterministic random FASTA ─────────────────────────
|
||
|
|
python3 - "$WORK/test.fasta" "$K" <<'EOF'
|
||
|
|
import random, sys
|
||
|
|
path, k = sys.argv[1], int(sys.argv[2])
|
||
|
|
random.seed(1234)
|
||
|
|
bases = "ACGT"
|
||
|
|
with open(path, "w") as f:
|
||
|
|
for i in range(3):
|
||
|
|
seq = "".join(random.choice(bases) for _ in range(300))
|
||
|
|
f.write(f">seq{i}\n{seq}\n")
|
||
|
|
EOF
|
||
|
|
|
||
|
|
# ── 2. build + run index ────────────────────────────────────────────────────
|
||
|
|
cd "$REPO_ROOT/src"
|
||
|
|
INDEX_LOG="$WORK/index.log"
|
||
|
|
if ! cargo run -q -p obikmer --bin obikmer -- \
|
||
|
|
index -k "$K" -m "$M" --theta 0 -p "$PARTITIONS" \
|
||
|
|
-o "$WORK/out.idx" "$WORK/test.fasta" > "$INDEX_LOG" 2>&1
|
||
|
|
then
|
||
|
|
cat "$INDEX_LOG" >&2
|
||
|
|
fail "obikmer index exited non-zero"
|
||
|
|
fi
|
||
|
|
|
||
|
|
N_KMERS="$(grep -o '[0-9]* total kmers indexed' "$INDEX_LOG" | grep -o '^[0-9]*' || true)"
|
||
|
|
[ -n "$N_KMERS" ] || { cat "$INDEX_LOG" >&2; fail "could not find 'N total kmers indexed' in index log"; }
|
||
|
|
[ "$N_KMERS" -gt 0 ] || fail "index reports 0 kmers indexed"
|
||
|
|
|
||
|
|
# ── 3+4. try candidate k-mers spread across the source sequence until one
|
||
|
|
# round-trips. `query` applies its own entropy filter to the query
|
||
|
|
# sequence (not exposed as a CLI flag, undocumented threshold) — a
|
||
|
|
# window that fails it reports kmer_count:0 even though the k-mer
|
||
|
|
# is genuinely in the index (not a bug, just a bad test fixture).
|
||
|
|
# Rather than reverse-engineer the filter's formula, just ask the
|
||
|
|
# real binary and move to the next candidate on a miss.
|
||
|
|
readarray -t CANDIDATES < <(python3 - "$WORK/test.fasta" "$K" <<'EOF'
|
||
|
|
import sys
|
||
|
|
path, k = sys.argv[1], int(sys.argv[2])
|
||
|
|
with open(path) as f:
|
||
|
|
seq = "".join(l.strip() for l in f if not l.startswith(">"))
|
||
|
|
for start in range(0, len(seq) - k, 17):
|
||
|
|
print(seq[start:start+k])
|
||
|
|
EOF
|
||
|
|
)
|
||
|
|
[ "${#CANDIDATES[@]}" -gt 0 ] || fail "could not extract any candidate k-mer from the source FASTA"
|
||
|
|
|
||
|
|
QUERY_LOG="$WORK/query.log"
|
||
|
|
FOUND=0
|
||
|
|
for QUERY_KMER in "${CANDIDATES[@]}"; do
|
||
|
|
printf ">q1\n%s\n" "$QUERY_KMER" > "$WORK/query.fasta"
|
||
|
|
if ! cargo run -q -p obikmer --bin obikmer -- \
|
||
|
|
query "$WORK/out.idx" "$WORK/query.fasta" > "$QUERY_LOG" 2>&1
|
||
|
|
then
|
||
|
|
cat "$QUERY_LOG" >&2
|
||
|
|
fail "obikmer query exited non-zero"
|
||
|
|
fi
|
||
|
|
if grep -q '"kmer_count":1' "$QUERY_LOG"; then
|
||
|
|
FOUND=1
|
||
|
|
break
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
|
||
|
|
if [ "$FOUND" -ne 1 ]; then
|
||
|
|
cat "$QUERY_LOG" >&2
|
||
|
|
fail "no candidate k-mer round-tripped (tried ${#CANDIDATES[@]}) — likely a real regression, not a low-complexity fixture"
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo "PASS: index+query round-trip OK — $N_KMERS kmers indexed, query k-mer '$QUERY_KMER' found"
|