mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-08-24 13:51:18 +00:00
Introduce explicit input validation and a `(-1, -1, -1)` sentinel return value to signal invalid or unreliable matches. Add pre-call length checks and early returns to prevent panics during indel relocation. Fix an index offset bug in coordinate calculation by preserving the original fragment position before applying alignment deltas. Update match filtering to enforce reliability checks only on successfully relocated alignments. Comprehensive tests validate edge cases, boundary constraints, and error thresholds.
124 lines
4.0 KiB
Go
124 lines
4.0 KiB
Go
package obialign
|
|
|
|
import (
|
|
"math/rand"
|
|
"testing"
|
|
)
|
|
|
|
func TestLocatePatternNormal(t *testing.T) {
|
|
// Pattern fully and exactly present in the middle of a longer sequence.
|
|
start, end, nerr := LocatePattern("id", []byte("ACGT"), []byte("TTTTACGTTTTT"))
|
|
if start != 4 || end != 8 || nerr != 0 {
|
|
t.Errorf("got start=%d end=%d nerr=%d, want start=4 end=8 nerr=0", start, end, nerr)
|
|
}
|
|
}
|
|
|
|
func TestLocatePatternOneMismatch(t *testing.T) {
|
|
start, end, nerr := LocatePattern("id", []byte("ACGT"), []byte("TTTTACTTTTTT"))
|
|
if nerr != 1 {
|
|
t.Errorf("got nerr=%d, want 1 (start=%d end=%d)", nerr, start, end)
|
|
}
|
|
}
|
|
|
|
// The real-world case that used to panic: pattern longer than the sequence
|
|
// fragment extracted for indel relocation.
|
|
func TestLocatePatternPatternLongerThanSequence(t *testing.T) {
|
|
start, end, nerr := LocatePattern("id",
|
|
[]byte("GGGCAATCCTGAGCCAAATC"),
|
|
[]byte("tcctgagccaaatcacgtt"))
|
|
|
|
if start != -1 || end != -1 || nerr != -1 {
|
|
t.Errorf("got start=%d end=%d nerr=%d, want the (-1,-1,-1) sentinel", start, end, nerr)
|
|
}
|
|
}
|
|
|
|
func TestLocatePatternSequenceLengthOne(t *testing.T) {
|
|
start, end, nerr := LocatePattern("id", []byte("AB"), []byte("A"))
|
|
|
|
if start < 0 || end < 0 || nerr < 0 {
|
|
t.Fatalf("got start=%d end=%d nerr=%d, expected a valid (non-sentinel) result", start, end, nerr)
|
|
}
|
|
if start != 0 || end != 1 || nerr != 1 {
|
|
t.Errorf("got start=%d end=%d nerr=%d, want start=0 end=1 nerr=1", start, end, nerr)
|
|
}
|
|
}
|
|
|
|
// A pattern much longer than the sequence can still yield a mathematically
|
|
// valid (in-bounds) alignment: the extra pattern length is absorbed as gaps,
|
|
// driving the error count high enough that the caller's maxerr threshold
|
|
// rejects it. The function itself must still return consistent bounds.
|
|
func TestLocatePatternPatternMuchLongerThanSequence(t *testing.T) {
|
|
start, end, nerr := LocatePattern("id", []byte("ACGTACGTACGTACGTACGT"), []byte("ACG"))
|
|
|
|
isSentinel := start == -1 && end == -1 && nerr == -1
|
|
isValid := start >= 0 && end > start && end <= 3 && nerr >= 0
|
|
|
|
if !isSentinel && !isValid {
|
|
t.Errorf("got start=%d end=%d nerr=%d, want either the sentinel or consistent in-bounds values", start, end, nerr)
|
|
}
|
|
}
|
|
|
|
func TestLocatePatternNeverReturnsOutOfBounds(t *testing.T) {
|
|
patterns := []string{"A", "AC", "ACG", "ACGT", "ACGTA", "ACGTAC", "ACGTACG", "ACGTACGT"}
|
|
sequences := []string{"A", "AC", "ACG", "ACGT", "ACGTA", "ACGTAC", "ACGTACG", "ACGTACGT"}
|
|
|
|
for _, p := range patterns {
|
|
for _, s := range sequences {
|
|
start, end, nerr := LocatePattern("id", []byte(p), []byte(s))
|
|
|
|
if start == -1 && end == -1 && nerr == -1 {
|
|
// Explicit "no reliable match" sentinel: always acceptable.
|
|
continue
|
|
}
|
|
|
|
if start < 0 || end < 0 || start >= end || end > len(s) || nerr < 0 {
|
|
t.Errorf("pattern=%q sequence=%q -> start=%d end=%d nerr=%d is out of bounds / inconsistent",
|
|
p, s, start, end, nerr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Randomized property test over a wide range of pattern/sequence length
|
|
// combinations, including pattern >= sequence, to make sure the function
|
|
// never panics and never returns anything but the sentinel or fully
|
|
// consistent, in-bounds coordinates.
|
|
func TestLocatePatternRandomizedNeverInvalid(t *testing.T) {
|
|
const bases = "ACGT"
|
|
rng := rand.New(rand.NewSource(42))
|
|
|
|
randSeq := func(n int) []byte {
|
|
b := make([]byte, n)
|
|
for i := range b {
|
|
b[i] = bases[rng.Intn(len(bases))]
|
|
}
|
|
return b
|
|
}
|
|
|
|
for trial := 0; trial < 5000; trial++ {
|
|
patLen := 1 + rng.Intn(15)
|
|
seqLen := 1 + rng.Intn(15)
|
|
|
|
pattern := randSeq(patLen)
|
|
sequence := randSeq(seqLen)
|
|
|
|
func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Fatalf("panic for pattern=%q sequence=%q: %v", pattern, sequence, r)
|
|
}
|
|
}()
|
|
|
|
start, end, nerr := LocatePattern("id", pattern, sequence)
|
|
|
|
isSentinel := start == -1 && end == -1 && nerr == -1
|
|
isValid := start >= 0 && end > start && end <= len(sequence) && nerr >= 0
|
|
|
|
if !isSentinel && !isValid {
|
|
t.Errorf("pattern=%q sequence=%q -> start=%d end=%d nerr=%d is neither the sentinel nor consistent",
|
|
pattern, sequence, start, end, nerr)
|
|
}
|
|
}()
|
|
}
|
|
}
|