- install and Reset retire the channels they replace: a retired daily writer appends a late record and closes the file again, so no record is lost and no descriptor stays open; diary.Close keeps reopening the installed writers on the next record - ApplyGlobals holds the setup lock for its whole run (lock-free installLocked and setVerbosityLocked), so concurrent calls rebuild the channels once (WR-06)
124 lines
3.3 KiB
Go
124 lines
3.3 KiB
Go
package diary
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
|
)
|
|
|
|
// installDefault builds the default configuration (console and daily) on
|
|
// root and installs it, as Setup does.
|
|
func installDefault(t *testing.T, root string, color bonfire.ColorMode) *channels {
|
|
t.Helper()
|
|
opts := Options{
|
|
Root: root,
|
|
Stderr: io.Discard,
|
|
Environ: []string{},
|
|
Color: color,
|
|
Now: func() time.Time { return time.Date(2026, 10, 21, 12, 0, 0, 0, time.Local) },
|
|
}.normalize()
|
|
conf := DefaultConfig()
|
|
ch, err := buildChannels(conf, opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
install(conf, ch, opts)
|
|
return ch
|
|
}
|
|
|
|
// dailyOf returns the daily writer of built channels.
|
|
func dailyOf(t *testing.T, ch *channels) *dailyWriter {
|
|
t.Helper()
|
|
for _, c := range ch.closers {
|
|
if d, ok := c.(*dailyWriter); ok {
|
|
return d
|
|
}
|
|
}
|
|
t.Fatal("no daily writer in the channels")
|
|
return nil
|
|
}
|
|
|
|
func (d *dailyWriter) hasFile() bool {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
return d.file != nil
|
|
}
|
|
|
|
// TestRetiredDailyWriterKeepsNoFile (WR-06): a daily writer replaced by a
|
|
// new setup is retired: a late record through it (from a goroutine that
|
|
// still holds the old state) is appended to the day's file and the file is
|
|
// closed again. diary.Close keeps letting the installed writers reopen.
|
|
func TestRetiredDailyWriterKeepsNoFile(t *testing.T) {
|
|
t.Cleanup(Reset)
|
|
root := t.TempDir()
|
|
file := filepath.Join(root, "storage", "logs", "summer-2026-10-21.log")
|
|
|
|
old := dailyOf(t, installDefault(t, root, bonfire.ColorNever))
|
|
if _, err := old.Write([]byte("first\n")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
installed := dailyOf(t, installDefault(t, root, bonfire.ColorNever))
|
|
if old.hasFile() {
|
|
t.Fatal("the replaced writer still holds its file")
|
|
}
|
|
if _, err := old.Write([]byte("late\n")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if old.hasFile() {
|
|
t.Error("a late record through the replaced writer left its file open")
|
|
}
|
|
|
|
if _, err := installed.Write([]byte("current\n")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := installed.Write([]byte("reopened\n")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !installed.hasFile() {
|
|
t.Error("the installed writer did not reopen its file after diary.Close")
|
|
}
|
|
raw, err := os.ReadFile(file)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := string(raw); got != "first\nlate\ncurrent\nreopened\n" {
|
|
t.Errorf("daily file = %q", got)
|
|
}
|
|
}
|
|
|
|
// TestApplyGlobalsRebuildsOnce (WR-06): concurrent ApplyGlobals calls with
|
|
// a new color mode rebuild the channels once, and every call's verbosity
|
|
// is applied.
|
|
func TestApplyGlobalsRebuildsOnce(t *testing.T) {
|
|
t.Cleanup(Reset)
|
|
installDefault(t, t.TempDir(), bonfire.ColorAuto)
|
|
before := gens.Load()
|
|
var start, done sync.WaitGroup
|
|
start.Add(1)
|
|
for range 50 {
|
|
done.Add(1)
|
|
go func() {
|
|
defer done.Done()
|
|
start.Wait()
|
|
ApplyGlobals(bonfire.Globals{Color: bonfire.ColorNever, Verbosity: 2})
|
|
}()
|
|
}
|
|
start.Done()
|
|
done.Wait()
|
|
if n := gens.Load() - before; n != 1 {
|
|
t.Errorf("the channels were rebuilt %d times, want 1", n)
|
|
}
|
|
st := current.Load()
|
|
if st.opts.Color != bonfire.ColorNever || st.verbosity != 2 || Verbosity() != 2 {
|
|
t.Errorf("state after ApplyGlobals: color %v, verbosity %d (Verbosity %d)", st.opts.Color, st.verbosity, Verbosity())
|
|
}
|
|
}
|