Luigit
repositories / bugabinga.net

bugabinga.net

personal infrastructure for bugabinga!

owned by admin

services/luci/internal/logs/logs_test.go

Raw
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")
	}
}