package logs import ( "errors" "io" "os" "testing" "github.com/klauspost/compress/zstd" ) func TestCreateLiveCreatesParentAndFinalizeMissingLiveFails(t *testing.T) { store := Store{DataDir: t.TempDir()} file, err := store.CreateLive("nested/run") if err != nil { t.Fatalf("CreateLive() error = %v", err) } _ = file.Close() if _, err := os.Stat(store.DataDir + "/live/nested/run.log"); err != nil { t.Fatalf("live log missing: %v", err) } if _, err := store.Finalize("missing"); err == nil { t.Fatal("missing live log finalized") } } func TestCreateLiveRejectsBadDataDir(t *testing.T) { root := t.TempDir() bad := root + "/file" if err := os.WriteFile(bad, []byte("x"), 0o644); err != nil { t.Fatal(err) } if _, err := (Store{DataDir: bad}).CreateLive("run"); err == nil { t.Fatal("CreateLive accepted file data dir") } } func TestFinalizeRejectsBadPaths(t *testing.T) { root := t.TempDir() if err := os.WriteFile(root+"/file", []byte("x"), 0o644); err != nil { t.Fatal(err) } if _, err := (Store{DataDir: root + "/file"}).Finalize("run"); err == nil { t.Fatal("Finalize accepted file data dir") } store := Store{DataDir: root} if err := os.MkdirAll(root+"/live", 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(root+"/live/run.log", []byte("x"), 0o644); err != nil { t.Fatal(err) } if err := os.MkdirAll(root+"/logs/run.log.zst", 0o755); err != nil { t.Fatal(err) } if _, err := store.Finalize("run"); err == nil { t.Fatal("Finalize wrote over directory") } store = Store{DataDir: t.TempDir()} if err := os.MkdirAll(store.DataDir+"/live/dir.log", 0o755); err != nil { t.Fatal(err) } if _, err := store.Finalize("dir"); err == nil { t.Fatal("Finalize copied directory") } } func TestFinalizeInjectedFailures(t *testing.T) { boom := errors.New("boom") for _, tc := range []struct { name string set func() }{ {"encoder", func() { newLogEncoder = func(io.Writer) (logEncoder, error) { return nil, boom } }}, {"copy", func() { copyLog = func(io.Writer, io.Reader) (int64, error) { return 0, boom } }}, {"close", func() { newLogEncoder = func(io.Writer) (logEncoder, error) { return failLogEncoder{closeErr: boom}, nil } }}, {"rename", func() { renameLog = func(string, string) error { return boom } }}, {"remove", func() { removeLog = func(string) error { return boom } }}, } { t.Run(tc.name, func(t *testing.T) { oldEncoder := newLogEncoder oldCopy := copyLog oldRename := renameLog oldRemove := removeLog defer func() { newLogEncoder = oldEncoder; copyLog = oldCopy; renameLog = oldRename; removeLog = oldRemove }() tc.set() store := Store{DataDir: t.TempDir()} file, err := store.CreateLive("run") if err != nil { t.Fatal(err) } if _, err := file.WriteString("x"); err != nil { t.Fatal(err) } if err := file.Close(); err != nil { t.Fatal(err) } if _, err := store.Finalize("run"); !errors.Is(err, boom) { t.Fatalf("err = %v", err) } if data, err := os.ReadFile(store.DataDir + "/live/run.log"); err != nil || string(data) != "x" { t.Fatalf("live=%q err=%v", data, err) } if tc.name != "remove" { if _, err := os.Stat(store.DataDir + "/logs/run.log.zst"); !os.IsNotExist(err) { t.Fatalf("partial completed log visible: %v", err) } } }) } } type failLogEncoder struct{ closeErr error } func (f failLogEncoder) Write(p []byte) (int, error) { return len(p), nil } func (f failLogEncoder) Close() error { return f.closeErr } func TestFinalizeMakesCompletedLogVisibleOnlyAfterCompression(t *testing.T) { store := Store{DataDir: t.TempDir()} file, err := store.CreateLive("run") if err != nil { t.Fatal(err) } if _, err := file.WriteString("complete"); err != nil { t.Fatal(err) } if err := file.Close(); err != nil { t.Fatal(err) } oldCopy := copyLog started := make(chan struct{}) release := make(chan struct{}) copyLog = func(dst io.Writer, src io.Reader) (int64, error) { close(started) <-release return oldCopy(dst, src) } defer func() { copyLog = oldCopy }() result := make(chan error, 1) go func() { _, err := store.Finalize("run") result <- err }() <-started if _, err := os.Stat(store.DataDir + "/logs/run.log.zst"); !os.IsNotExist(err) { t.Fatalf("partial completed log visible: %v", err) } if _, err := os.Stat(store.DataDir + "/live/run.log"); err != nil { t.Fatalf("live log vanished before final visibility: %v", err) } close(release) if err := <-result; err != nil { t.Fatal(err) } } func TestFinalizeCompressesAndRemovesLiveLog(t *testing.T) { store := Store{DataDir: t.TempDir()} file, err := store.CreateLive("run1") if err != nil { t.Fatalf("CreateLive() error = %v", err) } if _, err := file.WriteString("hello\n"); err != nil { t.Fatal(err) } if err := file.Close(); err != nil { t.Fatal(err) } path, err := store.Finalize("run1") if err != nil { t.Fatalf("Finalize() error = %v", err) } compressed, err := os.Open(path) if err != nil { t.Fatal(err) } decoder, err := zstd.NewReader(compressed) if err != nil { t.Fatal(err) } data, err := io.ReadAll(decoder) decoder.Close() compressed.Close() if err != nil { t.Fatal(err) } if string(data) != "hello\n" { t.Fatalf("decompressed = %q", data) } if _, err := os.Stat(store.DataDir + "/live/run1.log"); !os.IsNotExist(err) { t.Fatalf("live log survived: %v", err) } } func TestOpenReadsLiveAndFinalLogs(t *testing.T) { store := Store{DataDir: t.TempDir()} file, err := store.CreateLive("live") if err != nil { t.Fatal(err) } if _, err := file.WriteString("live text"); err != nil { t.Fatal(err) } if err := file.Close(); err != nil { t.Fatal(err) } reader, err := store.Open("live") if err != nil { t.Fatal(err) } data, err := io.ReadAll(reader) reader.Close() if err != nil || string(data) != "live text" { t.Fatalf("data=%q err=%v", data, err) } file, err = store.CreateLive("final") if err != nil { t.Fatal(err) } _, _ = file.WriteString("final text") _ = file.Close() if _, err := store.Finalize("final"); err != nil { t.Fatal(err) } reader, err = store.Open("final") if err != nil { t.Fatal(err) } data, err = io.ReadAll(reader) reader.Close() if err != nil || string(data) != "final text" { t.Fatalf("data=%q err=%v", data, err) } if _, err := store.Open("missing"); !errors.Is(err, os.ErrNotExist) { t.Fatalf("missing err=%v", err) } } func TestTailIsBoundedAndUTF8Safe(t *testing.T) { store := Store{DataDir: t.TempDir()} writeLive := func(name string, data []byte) { file, err := store.CreateLive(name) if err != nil { t.Fatal(err) } if _, err := file.Write(data); err != nil { t.Fatal(err) } if err := file.Close(); err != nil { t.Fatal(err) } } writeLive("ascii", []byte("0123456789")) tail, err := store.Tail("ascii", 5) if err != nil || tail.Text != "56789" || !tail.Truncated { t.Fatalf("tail=%#v err=%v", tail, err) } writeLive("rune", []byte("éabc")) tail, err = store.Tail("rune", 4) if err != nil || tail.Text != "abc" || !tail.Truncated { t.Fatalf("rune=%#v err=%v", tail, err) } writeLive("invalid", []byte{0xff, 'a'}) tail, err = store.Tail("invalid", 8) if err != nil || tail.Text != "�a" || tail.Truncated { t.Fatalf("invalid=%#v err=%v", tail, err) } } func TestTailAndSpoolStripANSIFromLegacyLogs(t *testing.T) { store := Store{DataDir: t.TempDir()} file, err := store.CreateLive("legacy") if err != nil { t.Fatal(err) } _, _ = file.WriteString("plain \x1b[31mred\x1b[0m\n") _ = file.Close() tail, err := store.Tail("legacy", 1024) if err != nil || tail.Text != "plain red\n" { t.Fatalf("tail=%#v err=%v", tail, err) } spool, err := store.Spool("legacy") if err != nil { t.Fatal(err) } data, err := io.ReadAll(spool) _ = spool.Close() if err != nil || string(data) != "plain red\n" { t.Fatalf("spool=%q err=%v", data, err) } } func TestOpenAndSpoolRejectCorruptFinalLog(t *testing.T) { store := Store{DataDir: t.TempDir()} if err := os.MkdirAll(store.DataDir+"/logs", 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(store.DataDir+"/logs/bad.log.zst", []byte("bad"), 0o644); err != nil { t.Fatal(err) } if _, err := store.Spool("bad"); err == nil { t.Fatal("corrupt log spooled") } }