package main import ( "fmt" "os" "path/filepath" "strings" "sync" "testing" "time" ) func mustParser(t *testing.T, format, expr string) *fileParser { t.Helper() p, err := newFileParser(format, expr) if err != nil { t.Fatal(err) } return p } func TestParseLogTime(t *testing.T) { now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) utc := func(s string) time.Time { t, _ := time.Parse(time.RFC3339Nano, s); return t } tests := []struct { in string want time.Time }{ {"2026-10-05T10:00:00.123Z", utc("2026-10-05T10:00:00.123Z")}, {"2026-10-05T10:00:00+02:00", utc("2026-10-05T08:00:00Z")}, {"2026-10-05 10:00:00,250 +0200", utc("2026-10-05T08:00:00.25Z")}, {"2026/10/05 10:00:00Z", utc("2026-10-05T10:00:00Z")}, {"05/Oct/2026:10:00:00 +0200", utc("2026-10-05T08:00:00Z")}, {"1759572000", time.Unix(1759572000, 0)}, {"1759572000123", time.UnixMilli(1759572000123)}, } for _, tt := range tests { got, ok := parseLogTime(tt.in, now) if !ok || !got.Equal(tt.want) { t.Errorf("%q: got %v %v, want %v", tt.in, got, ok, tt.want) } } // Syslog timestamps: no year, the last one before tomorrow. if got, ok := parseLogTime("Dec 31 23:00:00", now); !ok || got.Year() != 2025 { t.Errorf("Dec 31: %v %v", got, ok) } if got, ok := parseLogTime("Oct 5 11:00:00", now); !ok || got.Year() != 2026 || got.Day() != 5 { t.Errorf("Oct 5: %v %v", got, ok) } for _, bad := range []string{"", "hello", "200", "12345"} { if _, ok := parseLogTime(bad, now); ok { t.Errorf("%q parsed", bad) } } } func TestParseText(t *testing.T) { now := time.Now() p := mustParser(t, fmtText, "") r := p.parse("2026-10-05 10:00:00.5 ERROR db: connection refused", now) if !r.dated || r.msg != "ERROR db: connection refused" || r.sev != 3 { t.Errorf("got %+v", r) } r = p.parse("[2026-10-05T10:00:00Z] - started", now) if !r.dated || r.msg != "started" || r.sev != 6 { t.Errorf("got %+v", r) } r = p.parse("\x1b[33mWARN\x1b[0m no timestamp here", now) if r.dated || r.msg != "WARN no timestamp here" || r.sev != 4 || !r.t.Equal(now) { t.Errorf("got %+v", r) } } func TestParseJSON(t *testing.T) { now := time.Now() p := mustParser(t, fmtJSON, "") r := p.parse(`{"time":"2026-10-05T10:00:00Z","level":"warn","msg":"slow query","host":"db.example","req":{"id":7,"tags":["a"]},"ok":true}`, now) if !r.dated || r.sev != 4 || r.msg != "slow query" { t.Errorf("got %+v", r) } for k, want := range map[string]string{"field_host": "db.example", "req.id": "7", "req.tags": `["a"]`, "ok": "true"} { if r.fields[k] != want { t.Errorf("%s = %q, want %q", k, r.fields[k], want) } } // pino: numeric level, epoch in ms. r = p.parse(`{"level":50,"time":1759572000123,"msg":"boom"}`, now) if r.sev != 3 || !r.t.Equal(time.UnixMilli(1759572000123)) { t.Errorf("got %+v", r) } // No message key: the whole line; not JSON: text. line := `{"event":"login","user":"bob"}` if r = p.parse(line, now); r.msg != line || r.fields["user"] != "bob" { t.Errorf("got %+v", r) } if r = p.parse("plain ERROR line", now); r.msg != "plain ERROR line" || r.sev != 3 { t.Errorf("got %+v", r) } } func TestParseLogfmt(t *testing.T) { p := mustParser(t, fmtLogfmt, "") r := p.parse(`time="2026-10-05T10:00:00Z" level=error msg="Cannot connect: \"docker\" down" providerName=docker flag`, time.Now()) if !r.dated || r.sev != 3 || r.msg != `Cannot connect: "docker" down` || r.fields["providerName"] != "docker" { t.Errorf("got %+v", r) } } func TestParseRegex(t *testing.T) { if _, err := newFileParser(fmtRegex, "("); err == nil { t.Error("invalid regex accepted") } if _, err := newFileParser(fmtRegex, ""); err == nil { t.Error("empty regex accepted") } p := mustParser(t, fmtRegex, `^(?P