package config import ( "encoding/json" "log/slog" "os" "path/filepath" "slices" "strings" "testing" "go.yaml.in/yaml/v3" ) // captureStdout returns what fn prints to os.Stdout. func captureStdout(t *testing.T, fn func()) string { t.Helper() path := filepath.Join(t.TempDir(), "stdout") f, err := os.Create(filepath.Clean(path)) if err != nil { t.Fatal(err) } old := os.Stdout os.Stdout = f t.Cleanup(func() { os.Stdout = old }) fn() os.Stdout = old if err := f.Close(); err != nil { t.Fatal(err) } b, err := os.ReadFile(filepath.Clean(path)) if err != nil { t.Fatal(err) } return string(b) } func TestPrintTerminal(t *testing.T) { conf := defaultConfig(t) got := captureStdout(t, conf.PrintTerminal) if want := conf.String() + "\n"; got != want { t.Errorf("PrintTerminal printed %q; want %q", got, want) } } // String lists every key once, sorted, so the output is the same each run. func TestStringSorted(t *testing.T) { conf := defaultConfig(t) var keys []string for _, line := range strings.Split(strings.TrimSpace(conf.String()), "\n") { key, _, _ := strings.Cut(line, " = ") keys = append(keys, key) } if !slices.IsSorted(keys) || len(keys) != len(conf.Flatten()) { t.Errorf("String keys = %v; want each key once, sorted", keys) } if first := conf.String(); conf.String() != first { t.Error("String output differs between calls") } } // PrintJSON and PrintYAML print the configuration in their format. func TestPrintFormats(t *testing.T) { conf := defaultConfig(t) tests := []struct { name string print func() unmarshal func([]byte, any) error }{ {"PrintJSON", conf.PrintJSON, json.Unmarshal}, {"PrintYAML", conf.PrintYAML, yaml.Unmarshal}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { out := captureStdout(t, tt.print) var m map[string]any if err := tt.unmarshal([]byte(out), &m); err != nil { t.Fatalf("%s output doesn't parse: %v\n%s", tt.name, err, out) } level := m["logging"].(map[string]any)["level"] if level != "warn" { t.Errorf("logging.level = %v; want warn", level) } }) } } // PrintLog logs one line per key, at the level asked for (info for any // level other than debug, warn and error). func TestPrintLog(t *testing.T) { conf := defaultConfig(t) for _, tt := range []struct { at slog.Level want string }{ {slog.LevelDebug, "level=DEBUG"}, {slog.LevelInfo, "level=INFO"}, {slog.LevelWarn, "level=WARN"}, {slog.LevelError, "level=ERROR"}, {slog.LevelError + 4, "level=INFO"}, } { logs := captureLogs(t, slog.LevelDebug) conf.PrintLog(tt.at) lines := strings.Split(strings.TrimSpace(logs.String()), "\n") if len(lines) != len(conf.Flatten()) { t.Errorf( "PrintLog(%v) logged %d lines; want %d", tt.at, len(lines), len(conf.Flatten()), ) } for _, l := range lines { if !strings.Contains(l, tt.want) { t.Errorf("PrintLog(%v) line %q; want %s", tt.at, l, tt.want) } } if !strings.Contains(logs.String(), "key=logging.level value=warn") { t.Errorf("PrintLog(%v) missing logging.level:\n%s", tt.at, logs) } // Each line is msg="config value" key= value=. flat := conf.Flatten() var keys []string for _, l := range lines { _, rest, _ := strings.Cut(l, ` key=`) key, _, _ := strings.Cut(rest, " ") if _, ok := flat[key]; !ok { t.Errorf("PrintLog(%v) line %q: no key", tt.at, l) } keys = append(keys, key) } if !slices.IsSorted(keys) { t.Errorf("PrintLog(%v) keys not sorted: %v", tt.at, keys) } } }