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)) } }