From b9ec05aebc01abd8ea32c197a5ecaef8a3e378cf Mon Sep 17 00:00:00 2001 From: Markus Graf Date: Thu, 26 Mar 2026 20:56:03 +0100 Subject: [PATCH] test(06-01): add failing tests for config package TOML load, merge, validate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 9 table-driven tests covering CFG-01 through CFG-05 - TestLoadPartialOverrideFrequency, TestLoadPartialOverrideWaveform, TestLoadBothOverrides - TestLoadUnknownKey, TestLoadNoConfig, TestLoadExplicitMissing - TestLoadUnknownClass, TestLoadInvalidWaveform, TestLoadAllDefaultsPresent - Stub config.Load returns nil,nil — all tests fail (RED) --- config/config.go | 9 +++ config/config_test.go | 180 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 config/config.go create mode 100644 config/config_test.go diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..8e2041e --- /dev/null +++ b/config/config.go @@ -0,0 +1,9 @@ +package config + +import "github.com/netsynth/netsynth/classify" +import "github.com/netsynth/netsynth/synth" + +// Load is a stub — implementation pending GREEN phase. +func Load(configPath string) (map[classify.TrafficClass]synth.FreqConfig, error) { + return nil, nil +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..1a3e078 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,180 @@ +package config_test + +import ( + "os" + "strings" + "testing" + + "github.com/netsynth/netsynth/classify" + "github.com/netsynth/netsynth/config" + "github.com/netsynth/netsynth/synth" +) + +// writeTOML creates a temp TOML file with the given content and returns its path. +func writeTOML(t *testing.T, content string) string { + t.Helper() + f, err := os.CreateTemp(t.TempDir(), "*.toml") + if err != nil { + t.Fatalf("CreateTemp: %v", err) + } + if _, err := f.WriteString(content); err != nil { + t.Fatalf("WriteString: %v", err) + } + if err := f.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + return f.Name() +} + +// TestLoadPartialOverrideFrequency: setting only frequency for ICMP overrides BaseHz, +// leaves WaveformType unchanged (WaveformCustom), and leaves other classes unchanged. +func TestLoadPartialOverrideFrequency(t *testing.T) { + path := writeTOML(t, "[sounds.ICMP]\nfrequency = 100.0\n") + + cfgs, err := config.Load(path) + if err != nil { + t.Fatalf("Load: %v", err) + } + + if cfgs[classify.ClassICMP].BaseHz != 100.0 { + t.Errorf("ICMP BaseHz: got %v, want 100.0", cfgs[classify.ClassICMP].BaseHz) + } + if cfgs[classify.ClassICMP].WaveformType != synth.WaveformCustom { + t.Errorf("ICMP WaveformType: got %v, want WaveformCustom (0)", cfgs[classify.ClassICMP].WaveformType) + } + // DNS should be unchanged + want := synth.ClassFreqConfigs[classify.ClassDNS].BaseHz + if cfgs[classify.ClassDNS].BaseHz != want { + t.Errorf("DNS BaseHz: got %v, want %v (default)", cfgs[classify.ClassDNS].BaseHz, want) + } +} + +// TestLoadPartialOverrideWaveform: setting only waveform for ICMP changes WaveformType, +// leaves BaseHz unchanged, and regenerates Harmonics. +func TestLoadPartialOverrideWaveform(t *testing.T) { + path := writeTOML(t, "[sounds.ICMP]\nwaveform = \"square\"\n") + + cfgs, err := config.Load(path) + if err != nil { + t.Fatalf("Load: %v", err) + } + + if cfgs[classify.ClassICMP].WaveformType != synth.WaveformSquare { + t.Errorf("ICMP WaveformType: got %v, want WaveformSquare", cfgs[classify.ClassICMP].WaveformType) + } + // BaseHz should be unchanged (default is 65.0) + if cfgs[classify.ClassICMP].BaseHz != 65.0 { + t.Errorf("ICMP BaseHz: got %v, want 65.0 (default)", cfgs[classify.ClassICMP].BaseHz) + } + // Harmonics should be regenerated (non-empty) + if len(cfgs[classify.ClassICMP].Harmonics) == 0 { + t.Error("ICMP Harmonics: got empty slice, expected regenerated harmonics for WaveformSquare") + } +} + +// TestLoadBothOverrides: setting both frequency and waveform applies both. +func TestLoadBothOverrides(t *testing.T) { + path := writeTOML(t, "[sounds.ICMP]\nfrequency = 100.0\nwaveform = \"square\"\n") + + cfgs, err := config.Load(path) + if err != nil { + t.Fatalf("Load: %v", err) + } + + if cfgs[classify.ClassICMP].BaseHz != 100.0 { + t.Errorf("ICMP BaseHz: got %v, want 100.0", cfgs[classify.ClassICMP].BaseHz) + } + if cfgs[classify.ClassICMP].WaveformType != synth.WaveformSquare { + t.Errorf("ICMP WaveformType: got %v, want WaveformSquare", cfgs[classify.ClassICMP].WaveformType) + } +} + +// TestLoadUnknownKey: a typo'd field name produces an error naming the bad key. +func TestLoadUnknownKey(t *testing.T) { + path := writeTOML(t, "[sounds.ICMP]\nfrequncy = 440\n") + + _, err := config.Load(path) + if err == nil { + t.Fatal("expected error for unknown key 'frequncy', got nil") + } + if !strings.Contains(err.Error(), "frequncy") { + t.Errorf("error should name the bad key 'frequncy', got: %v", err) + } +} + +// TestLoadNoConfig: Load("") in a directory with no netsynth.toml returns defaults with no error. +func TestLoadNoConfig(t *testing.T) { + // Chdir to a temp dir that has no netsynth.toml + t.Chdir(t.TempDir()) + + cfgs, err := config.Load("") + if err != nil { + t.Fatalf("Load with no config: %v", err) + } + if len(cfgs) != 14 { + t.Errorf("result map size: got %d, want 14", len(cfgs)) + } + // ICMP should be at its default BaseHz (65.0) + if cfgs[classify.ClassICMP].BaseHz != 65.0 { + t.Errorf("ICMP BaseHz: got %v, want 65.0 (default)", cfgs[classify.ClassICMP].BaseHz) + } +} + +// TestLoadExplicitMissing: an explicit path that doesn't exist returns an error containing "not found". +func TestLoadExplicitMissing(t *testing.T) { + _, err := config.Load("/nonexistent/path/config.toml") + if err == nil { + t.Fatal("expected error for missing explicit file, got nil") + } + if !strings.Contains(err.Error(), "not found") { + t.Errorf("error should contain 'not found', got: %v", err) + } +} + +// TestLoadUnknownClass: unknown class name produces no error (warning only), result has 14 entries. +func TestLoadUnknownClass(t *testing.T) { + path := writeTOML(t, "[sounds.BOGUS]\nfrequency = 100.0\n") + + cfgs, err := config.Load(path) + if err != nil { + t.Fatalf("Load with unknown class: %v", err) + } + if len(cfgs) != 14 { + t.Errorf("result map size: got %d, want 14 (BOGUS should not appear)", len(cfgs)) + } + // Confirm BOGUS is NOT in the map + if _, ok := cfgs["BOGUS"]; ok { + t.Error("BOGUS class should not be present in result map") + } +} + +// TestLoadInvalidWaveform: an invalid waveform string produces an error containing "invalid waveform". +func TestLoadInvalidWaveform(t *testing.T) { + path := writeTOML(t, "[sounds.ICMP]\nwaveform = \"invalid\"\n") + + _, err := config.Load(path) + if err == nil { + t.Fatal("expected error for invalid waveform, got nil") + } + if !strings.Contains(err.Error(), "invalid waveform") { + t.Errorf("error should contain 'invalid waveform', got: %v", err) + } +} + +// TestLoadAllDefaultsPresent: regardless of overrides, all 14 default classes are in the result map. +func TestLoadAllDefaultsPresent(t *testing.T) { + path := writeTOML(t, "[sounds.ICMP]\nfrequency = 200.0\n") + + cfgs, err := config.Load(path) + if err != nil { + t.Fatalf("Load: %v", err) + } + if len(cfgs) != 14 { + t.Errorf("result map size: got %d, want 14", len(cfgs)) + } + for _, class := range classify.AllClasses() { + if _, ok := cfgs[class]; !ok { + t.Errorf("class %q missing from result map", class) + } + } +}