Files
obitools4/pkg/obitools/obiprimers/embedded_test.go
T
Eric Coissac 574221df42 Refactor taxonomy handling and improve performance
This update introduces logic for handling embedded taxonomy, allowing users to specify taxonomy or extract it from sequences, while optimizing performance through reduced memory usage. It also adds batch reordering capabilities and implements comprehensive testing to ensure correct precedence rules and robust error handling for missing taxonomic paths.
2026-11-03 22:44:44 +11:00

142 lines
4.6 KiB
Go

package obiprimers
import (
"os"
"path/filepath"
"strings"
"testing"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiprimer"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitax"
)
// Three sequences below two families (5 and 6) of the same root, annotated like
// obiannotate --taxonomic-path does.
const embeddedFasta = `>s1 {"taxid":"taxon:10 [Alpha beta]@species","taxonomic_path":["taxon:1 [root]@no rank","taxon:5 [Famone]@family","taxon:10 [Alpha beta]@species"]}
acgtacgtacgtacgtacgtacgtacgtacgtacgtacgt
>s2 {"taxid":"taxon:11 [Alpha gamma]@species","taxonomic_path":["taxon:1 [root]@no rank","taxon:5 [Famone]@family","taxon:11 [Alpha gamma]@species"]}
acgtacgtacgtacgtacgtacgtacgtacgtacgtacgt
>s3 {"taxid":"taxon:20 [Delta eps]@species","taxonomic_path":["taxon:1 [root]@no rank","taxon:6 [Famtwo]@family","taxon:20 [Delta eps]@species"]}
acgtacgtacgtacgtacgtacgtacgtacgtacgtacgt
`
func writeTemp(t *testing.T, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "seqs.fasta")
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
return path
}
func drain(it obiiter.IBioSequence) (n int) {
for it.Next() {
batch := it.Get()
n += len(batch.Slice())
}
return
}
func TestEmbeddedTaxonomyFromFiles(t *testing.T) {
path := writeTemp(t, embeddedFasta)
sequences, taxonomy, err := readSequencesAndTaxonomy([]string{path}, nil)
if err != nil {
t.Fatal(err)
}
if taxonomy == nil || taxonomy.Len() != 6 { // root, 2 families, 3 species
t.Fatalf("expected an embedded taxonomy of 6 taxa, got %v", taxonomy)
}
// the sequences are read in full on the second pass
restrict, err := obiprimer.BelongingTo(taxonomy, []string{"5"})
if err != nil {
t.Fatalf("a taxid of the embedded taxonomy must be usable with -r: %v", err)
}
corpus, err := obiprimer.LoadCorpus(sequences, obiprimer.Selection{Restrict: restrict})
if err != nil {
t.Fatal(err)
}
if c := corpus.Counts(); c.Examples != 2 || c.Ignored != 1 {
t.Errorf("restricting to family 5: %+v", c)
}
taxa, err := corpus.AssignTaxa(taxonomy, "species")
if err != nil {
t.Fatal(err)
}
if taxa.ExampleTaxa != 2 {
t.Errorf("%d example species, want 2", taxa.ExampleTaxa)
}
}
func TestEmbeddedTaxonomyFromMemory(t *testing.T) { // the standard input case
var slice obiseq.BioSequenceSlice
for _, s := range []struct{ id, taxid string }{{"a", "10"}, {"b", "20"}} {
seq := obiseq.NewBioSequence(s.id, []byte("acgtacgtacgt"), "")
seq.SetAttribute("taxonomic_path", []string{
"taxon:1 [root]@no rank", "taxon:" + s.taxid + " [Sp" + s.taxid + "]@species"})
slice = append(slice, seq)
}
sequences, taxonomy, err := embeddedTaxonomyInMemory(obiiter.IBatchOver("stdin", slice, 10))
if err != nil {
t.Fatal(err)
}
if taxonomy.Len() != 3 {
t.Errorf("%d taxa, want 3", taxonomy.Len())
}
if n := drain(sequences); n != 2 {
t.Errorf("the sequences must be given back intact: %d of 2", n)
}
}
func TestMissingEmbeddedTaxonomyGivesAdvice(t *testing.T) {
path := writeTemp(t, ">s1 {\"taxid\":\"taxon:10 [Alpha beta]@species\"}\nacgtacgtacgtacgt\n")
_, _, err := readSequencesAndTaxonomy([]string{path}, nil)
if err == nil {
t.Fatal("sequences without a taxonomic path and no -t must be refused")
}
for _, want := range []string{"-t", "obiannotate --taxonomic-path"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("the error should mention %q: %v", want, err)
}
}
var slice obiseq.BioSequenceSlice
slice = append(slice, obiseq.NewBioSequence("a", []byte("acgt"), ""))
if _, _, err := embeddedTaxonomyInMemory(obiiter.IBatchOver("stdin", slice, 10)); err == nil ||
!strings.Contains(err.Error(), "obiannotate --taxonomic-path") {
t.Errorf("the in-memory case must give the same advice: %v", err)
}
}
func TestGivenTaxonomyHasPrecedence(t *testing.T) {
path := writeTemp(t, embeddedFasta)
_, embedded, err := readSequencesAndTaxonomy([]string{path}, nil)
if err != nil {
t.Fatal(err)
}
// With a taxonomy given (-t), the embedded paths are ignored, even if present.
other := obitax.NewTaxonomy("other", "other", obitax.DefaultTaxidAlphabet)
sequences, used, err := readSequencesAndTaxonomy([]string{path}, other)
if err != nil {
t.Fatal(err)
}
if used != other || used == embedded {
t.Error("the given taxonomy must be returned as is")
}
if n := drain(sequences); n != 3 {
t.Errorf("%d sequences read, want 3", n)
}
}
func TestPrimerPairsNeedsATaxonomy(t *testing.T) {
if _, err := CLIPrimerPairs(obiiter.NilIBioSequence, nil); err == nil {
t.Error("a nil taxonomy must be refused")
}
}