repositories / bugabinga.net
bugabinga.net
personal infrastructure for bugabinga!
owned by admin
services/luci/internal/logs/logs_test.go
Rawpackage 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")
}
}