mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-08-24 05:41:19 +00:00
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)
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
}
|