From ed3a5621f426fa3a475193699746c0e0b02b62f0 Mon Sep 17 00:00:00 2001 From: Markus Graf Date: Thu, 26 Mar 2026 12:06:04 +0100 Subject: [PATCH] test(02-03): add failing tests for MP3 encoder and RunSynthesis - TestMP3Valid: synthetic snapshots through full pipeline, ffprobe validation - TestZeroPacketError: empty slice and zero-count snapshots return error, no file created - TestEncodeMP3DirectBytes: direct frame encoding validated by ffprobe --- encode/mp3_test.go | 182 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 encode/mp3_test.go diff --git a/encode/mp3_test.go b/encode/mp3_test.go new file mode 100644 index 0000000..2c18454 --- /dev/null +++ b/encode/mp3_test.go @@ -0,0 +1,182 @@ +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); 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) + 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) + 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)) + } +}