Files
obitools4/pkg/obialign/locatepattern_test.go
T

124 lines
4.0 KiB
Go
Raw Normal View History

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)
}
}()
}
}