v0.19.1 dep update, test helper package

This commit is contained in:
lixen committed 2026-09-29 01:22:14 -04:00
1 parent cb0da23070
commit 81463e305c
11 files changed
+176 -158

No files matched your search

+2 -36
View File
@@ -2,23 +2,14 @@ package main
import ( import (
"context" "context"
"os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"logwisp/internal/config" "logwisp/internal/testutil"
lconfig "github.com/lixenwraith/config" lconfig "github.com/lixenwraith/config"
"github.com/lixenwraith/log"
) )
func testLogger(t *testing.T) {
t.Helper()
logger = log.NewLogger()
t.Cleanup(func() { logger = nil })
}
func TestWatchErrorsDoNotTriggerReloadAndChangesAreCoalesced(t *testing.T) { func TestWatchErrorsDoNotTriggerReloadAndChangesAreCoalesced(t *testing.T) {
testLogger(t) testLogger(t)
for _, event := range []string{lconfig.EventFileDeleted, lconfig.EventPermissionsChanged, lconfig.EventReloadTimeout, lconfig.EventReloadError + ":invalid TOML"} { for _, event := range []string{lconfig.EventFileDeleted, lconfig.EventPermissionsChanged, lconfig.EventReloadTimeout, lconfig.EventReloadError + ":invalid TOML"} {
@@ -39,29 +30,6 @@ func TestWatchErrorsDoNotTriggerReloadAndChangesAreCoalesced(t *testing.T) {
} }
} }
func loadTestConfig(t *testing.T, path string) *config.Config {
t.Helper()
for _, entry := range os.Environ() {
key, value, _ := strings.Cut(entry, "=")
if strings.HasPrefix(key, "LOGWISP_") {
t.Setenv(key, value)
if err := os.Unsetenv(key); err != nil {
t.Fatal(err)
}
}
}
m, err := config.Load([]string{"-c", path, "--quiet", "--status_reporter=false"})
if err != nil {
t.Fatal(err)
}
t.Cleanup(m.Close)
cfg, err := m.Snapshot()
if err != nil {
t.Fatal(err)
}
return cfg
}
func TestShippedConfigurationBuildsWithCheckedPluginDecoder(t *testing.T) { func TestShippedConfigurationBuildsWithCheckedPluginDecoder(t *testing.T) {
testLogger(t) testLogger(t)
path, err := filepath.Abs(filepath.Join("..", "..", "config", "logwisp.toml")) path, err := filepath.Abs(filepath.Join("..", "..", "config", "logwisp.toml"))
@@ -87,9 +55,7 @@ func TestShippedConfigurationBuildsWithCheckedPluginDecoder(t *testing.T) {
func TestInvalidReloadLeavesStatusReporterRunning(t *testing.T) { func TestInvalidReloadLeavesStatusReporterRunning(t *testing.T) {
testLogger(t) testLogger(t)
path := filepath.Join(t.TempDir(), "config.toml") path := filepath.Join(t.TempDir(), "config.toml")
if err := os.WriteFile(path, []byte("[logging]\nlevel = \"info\"\n"), 0600); err != nil { testutil.WriteFile(t, path, "[logging]\nlevel = \"info\"\n")
t.Fatal(err)
}
for _, mode := range []string{"empty-pipelines", "duplicate-names", "fractional-plugin-integer", "plugin-overflow"} { for _, mode := range []string{"empty-pipelines", "duplicate-names", "fractional-plugin-integer", "plugin-overflow"} {
t.Run(mode, func(t *testing.T) { t.Run(mode, func(t *testing.T) {
cfg := loadTestConfig(t, path) cfg := loadTestConfig(t, path)
+32
View File
@@ -0,0 +1,32 @@
package main
import (
"testing"
"logwisp/internal/config"
"logwisp/internal/testutil"
"github.com/lixenwraith/log"
)
func testLogger(t *testing.T) {
t.Helper()
previous := logger
logger = log.NewLogger()
t.Cleanup(func() { logger = previous })
}
func loadTestConfig(t *testing.T, path string) *config.Config {
t.Helper()
testutil.ClearEnvPrefix(t, "LOGWISP_")
m, err := config.Load([]string{"-c", path, "--quiet", "--status_reporter=false"})
if err != nil {
t.Fatal(err)
}
t.Cleanup(m.Close)
cfg, err := m.Snapshot()
if err != nil {
t.Fatal(err)
}
return cfg
}
+1 -1
View File
@@ -3,7 +3,7 @@ module logwisp
go 1.27.1 go 1.27.1
require ( require (
github.com/lixenwraith/config v0.1.1-0.20260928232812-d89d4e7c8167 github.com/lixenwraith/config v0.1.1-0.20260929045030-c10752fdd22d
github.com/lixenwraith/log v0.1.1-0.20260801090951-2c40643523b3 github.com/lixenwraith/log v0.1.1-0.20260801090951-2c40643523b3
) )
+2 -10
View File
@@ -1,14 +1,6 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/lixenwraith/config v0.1.1-0.20260929045030-c10752fdd22d h1:43qJzqVtyYT3EpO9hcazcogJ/EYGLjLRj2Uqoa5b/zw=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/lixenwraith/config v0.1.1-0.20260929045030-c10752fdd22d/go.mod h1:yfJQwKS+/6Dg70GIoHeYc0eYztOPETpqe6nUIbakcyg=
github.com/lixenwraith/config v0.1.1-0.20260928232812-d89d4e7c8167 h1:WOB0DbCxGEXksprSpatdsHWeV2+koKDNc5OtNE3knJA=
github.com/lixenwraith/config v0.1.1-0.20260928232812-d89d4e7c8167/go.mod h1:vGA41x85GxmJqYTCwg7cOlHUtqBh7AK/wPTRXuFQIYI=
github.com/lixenwraith/log v0.1.1-0.20260801090951-2c40643523b3 h1:5ojkxyuOKiUBRYhqPqKJS1lrqkjckYqYMNGBB49kdY0= github.com/lixenwraith/log v0.1.1-0.20260801090951-2c40643523b3 h1:5ojkxyuOKiUBRYhqPqKJS1lrqkjckYqYMNGBB49kdY0=
github.com/lixenwraith/log v0.1.1-0.20260801090951-2c40643523b3/go.mod h1:fvGT3IxlJQLJ1is4OP8Iet8gVzTJTGcuT90FFB2WBtc= github.com/lixenwraith/log v0.1.1-0.20260801090951-2c40643523b3/go.mod h1:fvGT3IxlJQLJ1is4OP8Iet8gVzTJTGcuT90FFB2WBtc=
github.com/lixenwraith/toml v0.0.0-20260928180512-b59d068ff6a4 h1:9qZU0iBIsqintDh39vieio0ygLiRrPmCEo6Ra45EQyY= github.com/lixenwraith/toml v0.0.0-20260928180512-b59d068ff6a4 h1:9qZU0iBIsqintDh39vieio0ygLiRrPmCEo6Ra45EQyY=
github.com/lixenwraith/toml v0.0.0-20260928180512-b59d068ff6a4/go.mod h1:itcT/Vt5mwmX9KVGCMsWZr6vbU/JgZ24wV/I0J5cMt0= github.com/lixenwraith/toml v0.0.0-20260928180512-b59d068ff6a4/go.mod h1:itcT/Vt5mwmX9KVGCMsWZr6vbU/JgZ24wV/I0J5cMt0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+10 -32
View File
@@ -2,39 +2,17 @@ package config
import ( import (
"errors" "errors"
"os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"time" "time"
"logwisp/internal/testutil"
lconfig "github.com/lixenwraith/config" lconfig "github.com/lixenwraith/config"
) )
func isolateConfig(t *testing.T) {
t.Helper()
for _, entry := range os.Environ() {
key, value, _ := strings.Cut(entry, "=")
if strings.HasPrefix(key, "LOGWISP_") {
t.Setenv(key, value)
if err := os.Unsetenv(key); err != nil {
t.Fatal(err)
}
}
}
dir := t.TempDir()
t.Setenv("HOME", dir)
t.Chdir(dir)
}
func writeConfig(t *testing.T, path, contents string) {
t.Helper()
if err := os.WriteFile(path, []byte(contents), 0600); err != nil {
t.Fatal(err)
}
}
func TestResolveConfigArguments(t *testing.T) { func TestResolveConfigArguments(t *testing.T) {
isolateConfig(t) isolateConfig(t)
for _, args := range [][]string{{"-c", "chosen.toml"}, {"--config", "chosen.toml"}, {"--config=chosen.toml"}, {"-c=chosen.toml"}, {"-c", "old.toml", "--config=chosen.toml"}} { for _, args := range [][]string{{"-c", "chosen.toml"}, {"--config", "chosen.toml"}, {"--config=chosen.toml"}, {"-c=chosen.toml"}, {"-c", "old.toml", "--config=chosen.toml"}} {
@@ -63,7 +41,7 @@ func TestResolveConfigArguments(t *testing.T) {
func TestLoadPrecedenceAndDetachedSnapshots(t *testing.T) { func TestLoadPrecedenceAndDetachedSnapshots(t *testing.T) {
isolateConfig(t) isolateConfig(t)
writeConfig(t, "selected.toml", "config_file = \"ignored.toml\"\n[logging]\nlevel = \"error\"\n") testutil.WriteFile(t, "selected.toml", "config_file = \"ignored.toml\"\n[logging]\nlevel = \"error\"\n")
t.Setenv("LOGGING_LEVEL", "invalid-bare-variable") t.Setenv("LOGGING_LEVEL", "invalid-bare-variable")
t.Setenv("LOGWISP_LOGGING_LEVEL", "debug") t.Setenv("LOGWISP_LOGGING_LEVEL", "debug")
m, err := Load([]string{"--config", "selected.toml", "--logging.level=warn", "--quiet"}) m, err := Load([]string{"--config", "selected.toml", "--logging.level=warn", "--quiet"})
@@ -116,7 +94,7 @@ func TestMissingFilesDoNotHideInvalidOverrides(t *testing.T) {
func TestReloadReadsDiskValidatesAndPreservesOldSnapshots(t *testing.T) { func TestReloadReadsDiskValidatesAndPreservesOldSnapshots(t *testing.T) {
isolateConfig(t) isolateConfig(t)
writeConfig(t, "reload.toml", "status_reporter = true\n") testutil.WriteFile(t, "reload.toml", "status_reporter = true\n")
m, err := Load([]string{"-c", "reload.toml"}) m, err := Load([]string{"-c", "reload.toml"})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -126,24 +104,24 @@ func TestReloadReadsDiskValidatesAndPreservesOldSnapshots(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
writeConfig(t, "reload.toml", "status_reporter = false\n") testutil.WriteFile(t, "reload.toml", "status_reporter = false\n")
next, err := m.Reload() next, err := m.Reload()
if err != nil || next.StatusReporter || !old.StatusReporter || next.ConfigFile != old.ConfigFile { if err != nil || next.StatusReporter || !old.StatusReporter || next.ConfigFile != old.ConfigFile {
t.Fatalf("disk reload/snapshot ownership: %+v %v", next, err) t.Fatalf("disk reload/snapshot ownership: %+v %v", next, err)
} }
// Builder validators do not run on LoadFile; Snapshot must run them again. // Builder validators do not run on LoadFile; Snapshot must run them again.
writeConfig(t, "reload.toml", "pipelines = []\n") testutil.WriteFile(t, "reload.toml", "pipelines = []\n")
if _, err := m.Reload(); err == nil || !strings.Contains(err.Error(), "no pipelines") { if _, err := m.Reload(); err == nil || !strings.Contains(err.Error(), "no pipelines") {
t.Fatalf("semantically invalid reload accepted: %v", err) t.Fatalf("semantically invalid reload accepted: %v", err)
} }
if len(next.Pipelines) == 0 || !old.StatusReporter { if len(next.Pipelines) == 0 || !old.StatusReporter {
t.Fatal("invalid edit changed a published snapshot") t.Fatal("invalid edit changed a published snapshot")
} }
writeConfig(t, "reload.toml", "[logging.file]\nmax_size_mb = 1.5\n") testutil.WriteFile(t, "reload.toml", "[logging.file]\nmax_size_mb = 1.5\n")
if _, err := m.Reload(); err == nil { if _, err := m.Reload(); err == nil {
t.Fatal("fractional integer accepted") t.Fatal("fractional integer accepted")
} }
writeConfig(t, "reload.toml", "# remove the status override\n") testutil.WriteFile(t, "reload.toml", "# remove the status override\n")
recovered, err := m.Reload() recovered, err := m.Reload()
if err != nil || !recovered.StatusReporter { if err != nil || !recovered.StatusReporter {
t.Fatalf("override removal/recovery: %+v %v", recovered, err) t.Fatalf("override removal/recovery: %+v %v", recovered, err)
@@ -152,7 +130,7 @@ func TestReloadReadsDiskValidatesAndPreservesOldSnapshots(t *testing.T) {
func TestWatchStartsOnSubscriptionAndCloses(t *testing.T) { func TestWatchStartsOnSubscriptionAndCloses(t *testing.T) {
isolateConfig(t) isolateConfig(t)
writeConfig(t, "watched.toml", "auto_reload = true\n") testutil.WriteFile(t, "watched.toml", "auto_reload = true\n")
m, err := Load([]string{"-c", "watched.toml"}) m, err := Load([]string{"-c", "watched.toml"})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -178,7 +156,7 @@ func TestWatchStartsOnSubscriptionAndCloses(t *testing.T) {
func TestNestedPluginOptionsKeepDefaultsAndNumericTypes(t *testing.T) { func TestNestedPluginOptionsKeepDefaultsAndNumericTypes(t *testing.T) {
isolateConfig(t) isolateConfig(t)
writeConfig(t, "plugins.toml", ` testutil.WriteFile(t, "plugins.toml", `
[[pipelines]] [[pipelines]]
name = "nested" name = "nested"
[[pipelines.plugin_sources]] [[pipelines.plugin_sources]]
+15
View File
@@ -0,0 +1,15 @@
package config
import (
"testing"
"logwisp/internal/testutil"
)
func isolateConfig(t *testing.T) {
t.Helper()
testutil.ClearEnvPrefix(t, "LOGWISP_")
dir := t.TempDir()
t.Setenv("HOME", dir)
t.Chdir(dir)
}
+5
View File
@@ -202,7 +202,12 @@ func (h *HTTPSink) Start(ctx context.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("http sink bind %s: %w", h.addr, err) return fmt.Errorf("http sink bind %s: %w", h.addr, err)
} }
return h.serve(ctx, ln)
}
// serve owns an already-bound listener, allowing tests to reserve an ephemeral
// port and exercise the same routing and worker lifecycle as Start.
func (h *HTTPSink) serve(ctx context.Context, ln net.Listener) error {
mux := http.NewServeMux() mux := http.NewServeMux()
// Method-scoped patterns: mux answers 405 with Allow header on non-GET // Method-scoped patterns: mux answers 405 with Allow header on non-GET
mux.HandleFunc(http.MethodGet+" "+h.config.StreamPath, h.handleStream) mux.HandleFunc(http.MethodGet+" "+h.config.StreamPath, h.handleStream)
+27 -73
View File
@@ -1,42 +1,22 @@
package http package http
import ( import (
"context" "bufio"
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"time" "time"
"logwisp/internal/session"
"logwisp/internal/sink" "logwisp/internal/sink"
"github.com/lixenwraith/log"
) )
func TestStatusReportsQueueAndConnectionBounds(t *testing.T) { func TestStatusReportsQueueAndConnectionBounds(t *testing.T) {
manager := session.NewManager(time.Hour) httpSink, _ := newTestHTTPSink(t, map[string]any{
defer manager.Stop()
created, err := NewHTTPSinkPlugin(
"stream",
map[string]any{
"host": "127.0.0.1",
"port": int64(8081),
"buffer_size": int64(4096), "buffer_size": int64(4096),
"client_buffer_size": int64(512), "client_buffer_size": int64(512),
"max_connections": int64(32), "max_connections": int64(32),
"write_timeout_ms": int64(5000), })
},
log.NewLogger(),
session.NewProxy(manager, "stream"),
)
if err != nil {
t.Fatal(err)
}
httpSink, ok := created.(*HTTPSink)
if !ok {
t.Fatalf("sink type = %T", created)
}
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
httpSink.handleStatus(recorder, httptest.NewRequest("GET", "/status", nil)) httpSink.handleStatus(recorder, httptest.NewRequest("GET", "/status", nil))
@@ -80,32 +60,14 @@ func TestStatusReportsQueueAndConnectionBounds(t *testing.T) {
// bumps activity otherwise, so a quiet source would idle-expire a healthy client // bumps activity otherwise, so a quiet source would idle-expire a healthy client
// and the broker would evict it on the next entry. // and the broker would evict it on the next entry.
func TestQuietStreamRefreshesItsSession(t *testing.T) { func TestQuietStreamRefreshesItsSession(t *testing.T) {
manager := session.NewManager(time.Hour) httpSink, manager := newTestHTTPSink(t, nil)
defer manager.Stop()
created, err := NewHTTPSinkPlugin(
"stream",
map[string]any{"host": "127.0.0.1", "port": int64(18191), "write_timeout_ms": int64(5000)},
log.NewLogger(),
session.NewProxy(manager, "stream"),
)
if err != nil {
t.Fatal(err)
}
httpSink := created.(*HTTPSink)
httpSink.keepalive = 100 * time.Millisecond httpSink.keepalive = 100 * time.Millisecond
client, baseURL := serveTestHTTPSink(t, httpSink)
ctx, cancel := context.WithCancel(context.Background()) resp, err := client.Get(baseURL + "/stream")
defer cancel()
if err := httpSink.Start(ctx); err != nil {
t.Fatal(err)
}
defer httpSink.Stop()
resp, err := http.Get("http://127.0.0.1:18191/stream")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer resp.Body.Close() t.Cleanup(func() { _ = resp.Body.Close() })
activity := func() time.Time { activity := func() time.Time {
for _, s := range manager.GetActiveSessions() { for _, s := range manager.GetActiveSessions() {
@@ -115,42 +77,34 @@ func TestQuietStreamRefreshesItsSession(t *testing.T) {
return time.Time{} return time.Time{}
} }
deadline := time.Now().Add(2 * time.Second)
for activity().IsZero() && time.Now().Before(deadline) {
time.Sleep(10 * time.Millisecond)
}
before := activity() before := activity()
if before.IsZero() {
// No events are sent for several keepalive periods. t.Fatal("stream session was not registered")
time.Sleep(350 * time.Millisecond) }
scanner := bufio.NewScanner(resp.Body)
heartbeats := 0
for heartbeats < 2 && scanner.Scan() {
if scanner.Text() == ":" {
heartbeats++
}
}
if err := scanner.Err(); err != nil {
t.Fatal(err)
}
if heartbeats != 2 {
t.Fatal("stream ended before two keepalives")
}
if after := activity(); !after.After(before) { if after := activity(); !after.After(before) {
t.Fatalf("last activity %v did not advance on a silent stream", after) t.Fatalf("last activity %v did not advance after keepalives from %v", after, before)
} }
} }
// HEAD on the stream path is refused rather than served from the GET pattern: // HEAD on the stream path is refused rather than served from the GET pattern:
// its body writes are discarded, so the client it would register never reads. // its body writes are discarded, so the client it would register never reads.
func TestHeadOnStreamPathIsRefused(t *testing.T) { func TestHeadOnStreamPathIsRefused(t *testing.T) {
manager := session.NewManager(time.Hour) httpSink, manager := newTestHTTPSink(t, nil)
defer manager.Stop() client, baseURL := serveTestHTTPSink(t, httpSink)
created, err := NewHTTPSinkPlugin( resp, err := client.Head(baseURL + "/stream")
"stream",
map[string]any{"host": "127.0.0.1", "port": int64(18192)},
log.NewLogger(),
session.NewProxy(manager, "stream"),
)
if err != nil {
t.Fatal(err)
}
httpSink := created.(*HTTPSink)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if err := httpSink.Start(ctx); err != nil {
t.Fatal(err)
}
defer httpSink.Stop()
resp, err := http.Head("http://127.0.0.1:18192/stream")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+48
View File
@@ -0,0 +1,48 @@
package http
import (
"context"
"maps"
"net"
"net/http"
"testing"
"time"
"logwisp/internal/session"
"github.com/lixenwraith/log"
)
func newTestHTTPSink(t *testing.T, overrides map[string]any) (*HTTPSink, *session.Manager) {
t.Helper()
manager := session.NewManager(time.Hour)
t.Cleanup(manager.Stop)
opts := map[string]any{"host": "127.0.0.1", "port": int64(8081), "write_timeout_ms": int64(5000)}
maps.Copy(opts, overrides)
created, err := NewHTTPSinkPlugin("stream", opts, log.NewLogger(), session.NewProxy(manager, "stream"))
if err != nil {
t.Fatal(err)
}
h, ok := created.(*HTTPSink)
if !ok {
t.Fatalf("sink type = %T", created)
}
return h, manager
}
func serveTestHTTPSink(t *testing.T, h *HTTPSink) (*http.Client, string) {
t.Helper()
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = listener.Close() })
ctx, cancel := context.WithCancel(t.Context())
t.Cleanup(func() { cancel(); h.Stop() })
if err := h.serve(ctx, listener); err != nil {
t.Fatal(err)
}
client := &http.Client{Timeout: 3 * time.Second}
t.Cleanup(client.CloseIdleConnections)
return client, "http://" + listener.Addr().String()
}
+3 -6
View File
@@ -9,13 +9,12 @@ import (
"time" "time"
"logwisp/internal/core" "logwisp/internal/core"
"logwisp/internal/testutil"
) )
func TestStoppedWatcherReturnsNormally(t *testing.T) { func TestStoppedWatcherReturnsNormally(t *testing.T) {
path := filepath.Join(t.TempDir(), "session.jsonl") path := filepath.Join(t.TempDir(), "session.jsonl")
if err := os.WriteFile(path, nil, 0o600); err != nil { testutil.WriteFile(t, path, "")
t.Fatal(err)
}
watcher := newFileWatcher(path, true, true, func(_ core.LogEntry) {}, nil) watcher := newFileWatcher(path, true, true, func(_ core.LogEntry) {}, nil)
watcher.stop() watcher.stop()
@@ -52,9 +51,7 @@ func TestRemoveWatcherPreservesReplacement(t *testing.T) {
func TestRotatedFileResumesInsteadOfReplaying(t *testing.T) { func TestRotatedFileResumesInsteadOfReplaying(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
active := filepath.Join(dir, "session.jsonl") active := filepath.Join(dir, "session.jsonl")
if err := os.WriteFile(active, []byte("one\ntwo\n"), 0o600); err != nil { testutil.WriteFile(t, active, "one\ntwo\n")
t.Fatal(err)
}
info, err := os.Stat(active) info, err := os.Stat(active)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
+31
View File
@@ -0,0 +1,31 @@
// Package testutil provides standard-library fixtures shared by logwisp tests.
// Application packages must not import it.
package testutil
import (
"os"
"strings"
"testing"
)
// ClearEnvPrefix isolates overrides while preserving the caller's environment.
// Like testing.Setenv, it must not be used by parallel tests.
func ClearEnvPrefix(t testing.TB, prefix string) {
t.Helper()
for _, entry := range os.Environ() {
key, value, _ := strings.Cut(entry, "=")
if strings.HasPrefix(key, prefix) {
t.Setenv(key, value) // Register restoration before unsetting it.
if err := os.Unsetenv(key); err != nil {
t.Fatalf("unset fixture variable %s: %v", key, err)
}
}
}
}
func WriteFile(t testing.TB, path, contents string) {
t.Helper()
if err := os.WriteFile(path, []byte(contents), 0600); err != nil {
t.Fatalf("write fixture %q: %v", path, err)
}
}