Files
snitch/cmd/golden_test.go
2025-12-23 10:01:01 +01:00

428 lines
12 KiB
Go

package cmd
import (
"flag"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"testing"
"github.com/karol-broda/snitch/internal/collector"
"github.com/karol-broda/snitch/internal/testutil"
)
var updateGolden = flag.Bool("update-golden", false, "Update golden files")
func TestGoldenFiles(t *testing.T) {
// Skip the tests for now as they're flaky due to timestamps
t.Skip("Skipping golden file tests as they need to be rewritten to handle dynamic timestamps")
tests := []struct {
name string
fixture string
outputType string
filters []string
description string
}{
{
name: "empty_table",
fixture: "empty",
outputType: "table",
filters: []string{},
description: "Empty connection list in table format",
},
{
name: "empty_json",
fixture: "empty",
outputType: "json",
filters: []string{},
description: "Empty connection list in JSON format",
},
{
name: "single_tcp_table",
fixture: "single-tcp",
outputType: "table",
filters: []string{},
description: "Single TCP connection in table format",
},
{
name: "single_tcp_json",
fixture: "single-tcp",
outputType: "json",
filters: []string{},
description: "Single TCP connection in JSON format",
},
{
name: "mixed_protocols_table",
fixture: "mixed-protocols",
outputType: "table",
filters: []string{},
description: "Mixed protocols in table format",
},
{
name: "mixed_protocols_json",
fixture: "mixed-protocols",
outputType: "json",
filters: []string{},
description: "Mixed protocols in JSON format",
},
{
name: "tcp_filter_table",
fixture: "mixed-protocols",
outputType: "table",
filters: []string{"proto=tcp"},
description: "TCP-only filter in table format",
},
{
name: "udp_filter_json",
fixture: "mixed-protocols",
outputType: "json",
filters: []string{"proto=udp"},
description: "UDP-only filter in JSON format",
},
{
name: "listen_state_table",
fixture: "mixed-protocols",
outputType: "table",
filters: []string{"state=listen"},
description: "LISTEN state filter in table format",
},
{
name: "csv_output",
fixture: "single-tcp",
outputType: "csv",
filters: []string{},
description: "Single TCP connection in CSV format",
},
{
name: "wide_table",
fixture: "single-tcp",
outputType: "wide",
filters: []string{},
description: "Single TCP connection in wide table format",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, cleanup := testutil.SetupTestEnvironment(t)
defer cleanup()
// Set up test collector
testCollector := testutil.NewTestCollectorWithFixture(tt.fixture)
originalCollector := collector.GetCollector()
defer func() {
collector.SetCollector(originalCollector)
}()
collector.SetCollector(testCollector.MockCollector)
// Capture output
capture := testutil.NewOutputCapture(t)
capture.Start()
// Run command
runListCommand(tt.outputType, tt.filters)
stdout, stderr, err := capture.Stop()
if err != nil {
t.Fatalf("Failed to capture output: %v", err)
}
// Should have no stderr for valid commands
if stderr != "" {
t.Errorf("Unexpected stderr: %s", stderr)
}
// For JSON and CSV outputs with timestamps, we need to normalize the timestamps
if tt.outputType == "json" || tt.outputType == "csv" {
stdout = normalizeTimestamps(stdout, tt.outputType)
}
// Compare with golden file
goldenFile := filepath.Join("testdata", "golden", tt.name+".golden")
if *updateGolden {
// Update golden file
if err := os.MkdirAll(filepath.Dir(goldenFile), 0755); err != nil {
t.Fatalf("Failed to create golden dir: %v", err)
}
if err := os.WriteFile(goldenFile, []byte(stdout), 0644); err != nil {
t.Fatalf("Failed to write golden file: %v", err)
}
t.Logf("Updated golden file: %s", goldenFile)
return
}
// Compare with existing golden file
expected, err := os.ReadFile(goldenFile)
if err != nil {
t.Fatalf("Failed to read golden file %s (run with -update-golden to create): %v", goldenFile, err)
}
// Normalize expected content for comparison
expectedStr := string(expected)
if tt.outputType == "json" || tt.outputType == "csv" {
expectedStr = normalizeTimestamps(expectedStr, tt.outputType)
}
if stdout != expectedStr {
t.Errorf("Output does not match golden file %s\nExpected:\n%s\nActual:\n%s",
goldenFile, expectedStr, stdout)
}
})
}
}
func TestGoldenFiles_Stats(t *testing.T) {
// Skip the tests for now as they're flaky due to timestamps
t.Skip("Skipping stats golden file tests as they need to be rewritten to handle dynamic timestamps")
tests := []struct {
name string
fixture string
outputType string
description string
}{
{
name: "stats_empty_table",
fixture: "empty",
outputType: "table",
description: "Empty stats in table format",
},
{
name: "stats_mixed_table",
fixture: "mixed-protocols",
outputType: "table",
description: "Mixed protocols stats in table format",
},
{
name: "stats_mixed_json",
fixture: "mixed-protocols",
outputType: "json",
description: "Mixed protocols stats in JSON format",
},
{
name: "stats_mixed_csv",
fixture: "mixed-protocols",
outputType: "csv",
description: "Mixed protocols stats in CSV format",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, cleanup := testutil.SetupTestEnvironment(t)
defer cleanup()
// Set up test collector
testCollector := testutil.NewTestCollectorWithFixture(tt.fixture)
originalCollector := collector.GetCollector()
defer func() {
collector.SetCollector(originalCollector)
}()
collector.SetCollector(testCollector.MockCollector)
// Override stats global variables for testing
oldStatsOutputFormat := statsOutputFormat
oldStatsInterval := statsInterval
oldStatsCount := statsCount
defer func() {
statsOutputFormat = oldStatsOutputFormat
statsInterval = oldStatsInterval
statsCount = oldStatsCount
}()
statsOutputFormat = tt.outputType
statsInterval = 0 // One-shot mode
statsCount = 1
// Capture output
capture := testutil.NewOutputCapture(t)
capture.Start()
// Run stats command
runStatsCommand([]string{})
stdout, stderr, err := capture.Stop()
if err != nil {
t.Fatalf("Failed to capture output: %v", err)
}
// Should have no stderr for valid commands
if stderr != "" {
t.Errorf("Unexpected stderr: %s", stderr)
}
// For stats, we need to normalize timestamps since they're dynamic
stdout = normalizeStatsOutput(stdout, tt.outputType)
// Compare with golden file
goldenFile := filepath.Join("testdata", "golden", tt.name+".golden")
if *updateGolden {
// Update golden file
if err := os.MkdirAll(filepath.Dir(goldenFile), 0755); err != nil {
t.Fatalf("Failed to create golden dir: %v", err)
}
if err := os.WriteFile(goldenFile, []byte(stdout), 0644); err != nil {
t.Fatalf("Failed to write golden file: %v", err)
}
t.Logf("Updated golden file: %s", goldenFile)
return
}
// Compare with existing golden file
expected, err := os.ReadFile(goldenFile)
if err != nil {
t.Fatalf("Failed to read golden file %s (run with -update-golden to create): %v", goldenFile, err)
}
// Normalize expected content for comparison
expectedStr := string(expected)
expectedStr = normalizeStatsOutput(expectedStr, tt.outputType)
if stdout != expectedStr {
t.Errorf("Output does not match golden file %s\nExpected:\n%s\nActual:\n%s",
goldenFile, expectedStr, stdout)
}
})
}
}
// normalizeStatsOutput normalizes dynamic content in stats output for golden file comparison
func normalizeStatsOutput(output, format string) string {
// For stats output, we need to normalize timestamps since they're dynamic
switch format {
case "json":
// Replace timestamp with fixed value
return strings.ReplaceAll(output, "\"ts\":\"2025-01-15T10:30:00.000Z\"", "\"ts\":\"NORMALIZED_TIMESTAMP\"")
case "table":
// Replace timestamp line
lines := strings.Split(output, "\n")
for i, line := range lines {
if strings.HasPrefix(line, "TIMESTAMP") {
lines[i] = "TIMESTAMP\tNORMALIZED_TIMESTAMP"
}
}
return strings.Join(lines, "\n")
case "csv":
// Replace timestamp values
lines := strings.Split(output, "\n")
for i, line := range lines {
if strings.Contains(line, "2025-") {
// Replace any ISO timestamp with normalized value
parts := strings.Split(line, ",")
if len(parts) > 0 && strings.Contains(parts[0], "2025-") {
parts[0] = "NORMALIZED_TIMESTAMP"
lines[i] = strings.Join(parts, ",")
}
}
}
return strings.Join(lines, "\n")
}
return output
}
// normalizeTimestamps normalizes dynamic timestamps in output for golden file comparison
func normalizeTimestamps(output, format string) string {
switch format {
case "json":
// Use regex to replace timestamp values with a fixed string
// This matches ISO8601 timestamps in JSON format
re := regexp.MustCompile(`"ts":\s*"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]+\+[0-9]{2}:[0-9]{2}"`)
output = re.ReplaceAllString(output, `"ts": "NORMALIZED_TIMESTAMP"`)
// For stats_mixed_json, we need to normalize the order of processes
// This is a hack, but it works for now
if strings.Contains(output, `"by_proc"`) {
// Sort the by_proc array consistently
lines := strings.Split(output, "\n")
result := []string{}
inByProc := false
byProcLines := []string{}
for _, line := range lines {
if strings.Contains(line, `"by_proc"`) {
inByProc = true
result = append(result, line)
} else if inByProc && strings.Contains(line, `]`) {
// End of by_proc array
inByProc = false
// Sort by_proc lines by pid
sort.Strings(byProcLines)
// Add sorted lines
result = append(result, byProcLines...)
result = append(result, line)
} else if inByProc {
// Collect by_proc lines
byProcLines = append(byProcLines, line)
} else {
result = append(result, line)
}
}
return strings.Join(result, "\n")
}
return output
case "csv":
// For CSV, we need to handle the header row differently
lines := strings.Split(output, "\n")
result := []string{}
for _, line := range lines {
if strings.HasPrefix(line, "PID,") {
// Header row, keep as is
result = append(result, line)
} else {
// Data row, normalize if needed
result = append(result, line)
}
}
return strings.Join(result, "\n")
}
return output
}
// TestGoldenFileGeneration tests that we can generate all golden files
func TestGoldenFileGeneration(t *testing.T) {
if !*updateGolden {
t.Skip("Skipping golden file generation (use -update-golden to enable)")
}
goldenDir := filepath.Join("testdata", "golden")
if err := os.MkdirAll(goldenDir, 0755); err != nil {
t.Fatalf("Failed to create golden directory: %v", err)
}
// Create a README for the golden files
readme := `# Golden Files
This directory contains golden files for output contract verification tests.
These files are automatically generated and should not be edited manually.
To regenerate them, run:
go test ./cmd -update-golden
## Files
- *_table.golden: Table format output
- *_json.golden: JSON format output
- *_csv.golden: CSV format output
- *_wide.golden: Wide table format output
- stats_*.golden: Statistics command output
Each file represents expected output for specific test scenarios.
`
readmePath := filepath.Join(goldenDir, "README.md")
if err := os.WriteFile(readmePath, []byte(readme), 0644); err != nil {
t.Errorf("Failed to write golden README: %v", err)
}
}