Files
gurix 3dfcbbeaf5 feat(06-02): add freqCfgs parameter to RunSynthesis
- Change RunSynthesis signature to accept injected config map (D-10)
- Replace hardcoded synth.ClassFreqConfigs with passed-in freqCfgs
- Update all three RunSynthesis calls in encode tests to pass synth.ClassFreqConfigs
2026-03-26 21:02:35 +01:00

183 lines
5.0 KiB
Go

package encode
import (
"math"
"os"
"os/exec"
"strings"
"testing"
"github.com/netsynth/netsynth/classify"
"github.com/netsynth/netsynth/synth"
)
// TestMP3Valid synthesizes from 3 synthetic WindowSnapshots and validates the
// output file is a valid MP3 using ffprobe.
func TestMP3Valid(t *testing.T) {
// Check ffprobe is available
if _, err := exec.LookPath("ffprobe"); err != nil {
t.Skip("ffprobe not found in PATH; skipping MP3 validation test")
}
snaps := []classify.WindowSnapshot{
{
Counts: map[classify.TrafficClass]int64{
classify.ClassICMP: 50,
classify.ClassDNS: 30,
},
TotalPackets: 80,
WindowIndex: 0,
},
{
Counts: map[classify.TrafficClass]int64{
classify.ClassHTTPS: 200,
},
TotalPackets: 200,
WindowIndex: 1,
},
{
Counts: map[classify.TrafficClass]int64{
classify.ClassSSH: 10,
classify.ClassHTTP: 80,
},
TotalPackets: 90,
WindowIndex: 2,
},
}
tmpFile, err := os.CreateTemp("", "netsynth-test-*.mp3")
if err != nil {
t.Fatalf("create temp file: %v", err)
}
tmpPath := tmpFile.Name()
tmpFile.Close()
defer os.Remove(tmpPath)
if err := RunSynthesis(snaps, tmpPath, synth.ClassFreqConfigs); err != nil {
t.Fatalf("RunSynthesis: %v", err)
}
// Verify file exists and is non-empty
info, err := os.Stat(tmpPath)
if err != nil {
t.Fatalf("stat output file: %v", err)
}
if info.Size() == 0 {
t.Fatal("output file is empty")
}
// Validate with ffprobe
out, err := exec.Command("ffprobe",
"-v", "error",
"-show_entries", "format=format_name,duration,nb_streams",
"-of", "csv=p=0",
tmpPath,
).CombinedOutput()
if err != nil {
t.Fatalf("ffprobe error: %v\noutput: %s", err, out)
}
outStr := string(out)
if !strings.Contains(outStr, "mp3") {
t.Errorf("expected format_name to contain 'mp3', got: %q", outStr)
}
// ffprobe csv=p=0 format: format_name,duration,nb_streams
// Duration should be > 0; nb_streams should be 1
// We check that the output is non-empty and contains expected fields
fields := strings.Split(strings.TrimSpace(outStr), ",")
if len(fields) < 3 {
t.Errorf("unexpected ffprobe output format: %q", outStr)
}
}
// TestZeroPacketError verifies that RunSynthesis returns an error containing
// "no packets" and does NOT create an output file when given zero-packet input.
func TestZeroPacketError(t *testing.T) {
// Test 1: empty snapshot slice
tmpPath := "/tmp/netsynth-should-not-exist-" + t.Name() + ".mp3"
defer os.Remove(tmpPath)
err := RunSynthesis([]classify.WindowSnapshot{}, tmpPath, synth.ClassFreqConfigs)
if err == nil {
t.Fatal("expected error for empty snapshot slice, got nil")
}
if !strings.Contains(err.Error(), "no packets") {
t.Errorf("expected error to contain 'no packets', got: %q", err.Error())
}
if _, statErr := os.Stat(tmpPath); !os.IsNotExist(statErr) {
t.Error("output file should NOT exist after zero-packet error")
os.Remove(tmpPath)
}
// Test 2: snapshots where all TotalPackets == 0
tmpPath2 := "/tmp/netsynth-should-not-exist-zero-" + t.Name() + ".mp3"
defer os.Remove(tmpPath2)
zeroSnaps := []classify.WindowSnapshot{
{Counts: map[classify.TrafficClass]int64{}, TotalPackets: 0, WindowIndex: 0},
{Counts: map[classify.TrafficClass]int64{}, TotalPackets: 0, WindowIndex: 1},
}
err2 := RunSynthesis(zeroSnaps, tmpPath2, synth.ClassFreqConfigs)
if err2 == nil {
t.Fatal("expected error for zero-packet snapshots, got nil")
}
if !strings.Contains(err2.Error(), "no packets") {
t.Errorf("expected error to contain 'no packets', got: %q", err2.Error())
}
if _, statErr := os.Stat(tmpPath2); !os.IsNotExist(statErr) {
t.Error("output file should NOT exist after zero-packet error")
os.Remove(tmpPath2)
}
}
// TestEncodeMP3DirectBytes encodes a 440 Hz sine wave directly via EncodeMP3
// and validates the output as a valid MP3 file.
func TestEncodeMP3DirectBytes(t *testing.T) {
if _, err := exec.LookPath("ffprobe"); err != nil {
t.Skip("ffprobe not found in PATH; skipping MP3 validation test")
}
// Generate 1 second of 440 Hz stereo sine wave
sampleRate := synth.SampleRate
numFrames := sampleRate
frames := make([][2]float64, numFrames)
for i := range frames {
v := 0.5 * math.Sin(2.0*math.Pi*440.0*float64(i)/float64(sampleRate))
frames[i] = [2]float64{v, v}
}
tmpFile, err := os.CreateTemp("", "netsynth-sine-test-*.mp3")
if err != nil {
t.Fatalf("create temp file: %v", err)
}
tmpPath := tmpFile.Name()
tmpFile.Close()
defer os.Remove(tmpPath)
if err := EncodeMP3(tmpPath, frames, sampleRate); err != nil {
t.Fatalf("EncodeMP3: %v", err)
}
info, err := os.Stat(tmpPath)
if err != nil {
t.Fatalf("stat output file: %v", err)
}
if info.Size() == 0 {
t.Fatal("output file is empty")
}
out, err := exec.Command("ffprobe",
"-v", "error",
"-show_entries", "format=format_name",
"-of", "csv=p=0",
tmpPath,
).CombinedOutput()
if err != nil {
t.Fatalf("ffprobe error: %v\noutput: %s", err, out)
}
if !strings.Contains(string(out), "mp3") {
t.Errorf("expected MP3 format, got: %q", string(out))
}
}