package logx import ( "bytes" "fmt" "os" "strings" "testing" ) func TestCappedStaysUnderMax(t *testing.T) { f, err := os.CreateTemp("", "logx-capped-*") if err != nil { t.Fatal(err) } defer os.Remove(f.Name()) const max = int64(64) w := Capped(f, max) for i := 0; i < 50; i++ { fmt.Fprintf(w, "line %02d padding\n", i) // 16 bytes each } fi, err := f.Stat() if err != nil { t.Fatal(err) } if fi.Size() > max { t.Errorf("file grew to %d bytes, cap is %d", fi.Size(), max) } data, _ := os.ReadFile(f.Name()) out := string(data) if !strings.Contains(out, "line 49") { t.Errorf("newest line must survive trims: %q", out) } if strings.Contains(out, "line 00") { t.Errorf("oldest lines must be trimmed away first: %q", out) } } func TestCappedTrimsOversizedExistingFile(t *testing.T) { f, err := os.CreateTemp("", "logx-capped-*") if err != nil { t.Fatal(err) } defer os.Remove(f.Name()) // Simulate a file left behind by an older, uncapped build. for i := 0; i < 10; i++ { f.WriteString(strings.Repeat("x", 40) + "\n") // 410 bytes total } w := Capped(f, 64) fmt.Fprint(w, "fresh line\n") data, _ := os.ReadFile(f.Name()) out := string(data) if !strings.Contains(out, "fresh line") { t.Fatalf("new write must land in the file: %q", out) } if strings.Contains(out, "xxxx") { t.Errorf("old oversized content must be trimmed away: %q", out) } } func TestCountVerbs(t *testing.T) { cases := map[string]int{ "": 0, "no verbs": 0, "%% literal": 0, "a %": 0, // bare trailing percent is not a verb "%s": 1, "%s %d %v": 3, "-%+ 0.2f%x*": 2, "%s %% %d": 2, } for format, want := range cases { if got := countVerbs(format); got != want { t.Errorf("countVerbs(%q) = %d, want %d", format, got, want) } } } func TestFMismatchWritesDiagnostic(t *testing.T) { var buf bytes.Buffer AddSink(&buf) F("[t]", "login ok: user=%s region=%d", "nathan") // one verb short out := buf.String() if !strings.Contains(out, "(MISSING)") { t.Fatalf("expected rendered mismatch marker in output, got %q", out) } if !strings.Contains(out, "LOG-FORMAT-MISMATCH") { t.Fatalf("expected diagnostic line, got %q", out) } if !strings.Contains(out, "logx_test.go:") { t.Errorf("diagnostic should name the call site: %q", out) } buf.Reset() F("[t]", "balanced %s and %d", "a", 1) // matched: no diagnostic if strings.Contains(buf.String(), "LOG-FORMAT-MISMATCH") { t.Fatalf("matched format must not emit a diagnostic, got %q", buf.String()) } buf.Reset() F("[t]", "extra arg dropped %s", "a", 2) // more args than verbs if !strings.Contains(buf.String(), "LOG-FORMAT-MISMATCH") { t.Fatalf("expected diagnostic for extra args, got %q", buf.String()) } } // Regression: F once built its value slice with the prefix only and never // appended the caller's args, so every line rendered "%!v(MISSING)" while the // verb/arg balance check (which sees the args at the call site) stayed quiet. func TestFRenderedArgs(t *testing.T) { var buf bytes.Buffer AddSink(&buf) F("[ffxiv-launcher]", "starting ffxiv-tiny-launcher (%s/%s)", "windows", "amd64") out := buf.String() if !strings.Contains(out, "(windows/amd64)") { t.Fatalf("args must render in the line, got %q", out) } if strings.Contains(out, "(MISSING)") { t.Fatalf("balanced call rendered a missing-arg marker: %q", out) } buf.Reset() F("[t]", "login ok: user=%s region=%d maxex=%v", "nathan", 1, true) if !strings.Contains(buf.String(), "user=nathan region=1 maxex=true") { t.Fatalf("args must render in the line, got %q", buf.String()) } }