Files
yoloyolo/synth/bank_test.go
T
gurix ff3ec7eb78 feat(03-01): extend TrafficClass to 14 classes with hash-bucketed unknowns
- Replace ClassUnknown with ClassUnknown1-4 (unknown-1 through unknown-4)
- AllClasses() now returns 14 elements
- Add hashBucket() function routing unrecognized traffic deterministically
- Update synth/config.go: NumLayers=14, 4 dissonant unknown tone configs at 862-1047 Hz
- Add TestAllClassesCount and TestHashBucketDistribution tests
- Add TestClassFreqConfigsComplete and TestNumLayersMatchesAllClasses to config_test.go
- Update TestNewBankHas14Layers, TestMixerNoClip for 14 classes
- All classify and synth tests pass
2026-03-26 13:11:24 +01:00

161 lines
4.3 KiB
Go

package synth
import (
"math"
"testing"
"github.com/netsynth/netsynth/classify"
)
func TestNewBankHas14Layers(t *testing.T) {
b := NewBank(1.0)
if len(b.layers) != 14 {
t.Errorf("NewBank() has %d layers, want 14", len(b.layers))
}
// Verify each class has exactly one layer
for _, class := range classify.AllClasses() {
if _, ok := b.layers[class]; !ok {
t.Errorf("NewBank() missing layer for class %q", class)
}
}
}
func TestRenderWindowOutputLength(t *testing.T) {
b := NewBank(1.0)
snap := classify.WindowSnapshot{
Counts: make(map[classify.TrafficClass]int64),
TotalPackets: 0,
WindowIndex: 0,
}
frames := b.RenderWindow(snap)
if len(frames) != SamplesPerWindow {
t.Errorf("RenderWindow returned %d frames, want %d (SamplesPerWindow)", len(frames), SamplesPerWindow)
}
}
func TestRenderWindowSilentWhenNoTraffic(t *testing.T) {
b := NewBank(1.0)
// Empty counts — no class ever seen — all layers should stay at zero amplitude
snap := classify.WindowSnapshot{
Counts: make(map[classify.TrafficClass]int64),
TotalPackets: 0,
WindowIndex: 0,
}
frames := b.RenderWindow(snap)
for i, frame := range frames {
if frame[0] != 0.0 || frame[1] != 0.0 {
t.Errorf("frame[%d] = [%v, %v], want [0, 0] (silent when no traffic seen)", i, frame[0], frame[1])
break
}
}
}
func TestRenderWindowNonZeroWithTraffic(t *testing.T) {
b := NewBank(1.0)
counts := make(map[classify.TrafficClass]int64)
counts[classify.ClassICMP] = 100
snap := classify.WindowSnapshot{
Counts: counts,
TotalPackets: 100,
WindowIndex: 0,
}
frames := b.RenderWindow(snap)
// Check that at least some frames are non-zero
hasNonZero := false
for _, frame := range frames {
if frame[0] != 0.0 || frame[1] != 0.0 {
hasNonZero = true
break
}
}
if !hasNonZero {
t.Error("RenderWindow with ICMP count=100 should produce non-zero frames")
}
}
func TestMixerNoClip(t *testing.T) {
b := NewBank(0.01) // fast EMA to quickly ramp up to near-max amplitude
counts := make(map[classify.TrafficClass]int64)
// All 14 classes at max count — worst-case mixing scenario
for _, class := range classify.AllClasses() {
counts[class] = 1000
}
snap := classify.WindowSnapshot{
Counts: counts,
TotalPackets: 14000,
WindowIndex: 0,
}
// Render multiple windows to let EMA converge
for i := 0; i < 10; i++ {
frames := b.RenderWindow(snap)
for _, frame := range frames {
if frame[0] > 1.0 || frame[0] < -1.0 {
t.Errorf("left channel clipped: %v (exceeds [-1, 1])", frame[0])
return
}
if frame[1] > 1.0 || frame[1] < -1.0 {
t.Errorf("right channel clipped: %v (exceeds [-1, 1])", frame[1])
return
}
}
}
}
func TestStereoPan(t *testing.T) {
b := NewBank(0.01) // fast EMA
counts := make(map[classify.TrafficClass]int64)
// ClassDHCP has pan=-0.75 (wide-left in config.go)
counts[classify.ClassDHCP] = 1000
snap := classify.WindowSnapshot{
Counts: counts,
TotalPackets: 1000,
WindowIndex: 0,
}
// Render multiple windows to allow EMA to build up amplitude
var frames [][2]float64
for i := 0; i < 5; i++ {
frames = b.RenderWindow(snap)
}
// Compute RMS for L and R channels
var sumL2, sumR2 float64
for _, frame := range frames {
sumL2 += frame[0] * frame[0]
sumR2 += frame[1] * frame[1]
}
rmsL := math.Sqrt(sumL2 / float64(len(frames)))
rmsR := math.Sqrt(sumR2 / float64(len(frames)))
if rmsL <= rmsR {
t.Errorf("ClassDHCP (pan=-0.75) should have rmsL > rmsR; got rmsL=%v, rmsR=%v", rmsL, rmsR)
}
}
func TestMultipleWindowsEMAConvergence(t *testing.T) {
b := NewBank(1.0)
counts := make(map[classify.TrafficClass]int64)
counts[classify.ClassICMP] = 100
snap := classify.WindowSnapshot{
Counts: counts,
TotalPackets: 100,
WindowIndex: 0,
}
// Compute RMS for first and last window render
rmsFirst := windowRMS(b.RenderWindow(snap))
// Render 4 more windows with the same snapshot
var rmsLast float64
for i := 0; i < 4; i++ {
rmsLast = windowRMS(b.RenderWindow(snap))
}
if rmsLast <= rmsFirst {
t.Errorf("EMA should converge upward: rmsFirst=%v, rmsLast=%v", rmsFirst, rmsLast)
}
}
// windowRMS computes the root mean square amplitude across all stereo frames.
func windowRMS(frames [][2]float64) float64 {
var sum float64
for _, frame := range frames {
sum += frame[0]*frame[0] + frame[1]*frame[1]
}
return math.Sqrt(sum / float64(len(frames)*2))
}