Files
tslink/core/concurrent_test.go
2026-07-26 20:44:49 +08:00

186 lines
4.5 KiB
Go

package core
import (
"context"
"log/slog"
"sync"
"testing"
"time"
)
// These tests exist to give `go test -race` something to chew on. The GUI
// reads every one of these structures from its frame loop while background
// goroutines write to them, which is exactly the shape of bug that never
// shows up in a single-threaded test.
func TestLogBufferConcurrentAccess(t *testing.T) {
buf := NewLogBuffer(128) // small, so eviction runs constantly
logger := slog.New(buf.Handler(nil))
ctx, cancel := context.WithTimeout(context.Background(), 400*time.Millisecond)
defer cancel()
var wg sync.WaitGroup
// Writers.
for i := 0; i < 4; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
l := logger.With("from", "writer", "id", id)
for ctx.Err() == nil {
l.Info("message", "n", id, "auth_key", "tskey-auth-SECRETVALUE123")
l.Debug("detail", slog.Group("g", slog.String("k", "v")))
}
}(i)
}
// Readers, mimicking the GUI's frame loop.
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for ctx.Err() == nil {
_ = buf.Tail(50)
_ = buf.Filter(LogQuery{MinLevel: slog.LevelInfo, Text: "message", Limit: 20})
_ = buf.Sources()
_ = buf.Counts()
_ = buf.Len()
_ = buf.Dropped()
_ = buf.LastSeq()
}
}()
}
// Subscribers churning in and out.
wg.Add(1)
go func() {
defer wg.Done()
for ctx.Err() == nil {
ch, cancelSub := buf.Subscribe()
select {
case <-ch:
case <-time.After(5 * time.Millisecond):
}
cancelSub()
}
}()
// Exporter, which walks the whole ring and redacts.
wg.Add(1)
go func() {
defer wg.Done()
for ctx.Err() == nil {
out := buf.ExportText(ExportOptions{Query: LogQuery{MinLevel: slog.LevelDebug, Limit: 100}})
if len(out) > 0 && containsSecret(out) {
t.Error("export leaked an auth key")
return
}
time.Sleep(time.Millisecond)
}
}()
wg.Wait()
if buf.Len() > 128 {
t.Fatalf("ring exceeded its capacity: %d", buf.Len())
}
if buf.Dropped() == 0 {
t.Fatal("expected eviction to have occurred")
}
}
func containsSecret(s string) bool {
return len(s) > 0 && (indexOf(s, "SECRETVALUE123") >= 0)
}
func indexOf(hay, needle string) int {
for i := 0; i+len(needle) <= len(hay); i++ {
if hay[i:i+len(needle)] == needle {
return i
}
}
return -1
}
func TestLogBufferTailOrderAndBounds(t *testing.T) {
buf := NewLogBuffer(4)
logger := slog.New(buf.Handler(nil))
for i := 0; i < 10; i++ {
logger.Info("m", "i", i)
}
got := buf.Tail(3)
if len(got) != 3 {
t.Fatalf("Tail(3) returned %d entries", len(got))
}
// Oldest first, and the newest must be last.
for i := 1; i < len(got); i++ {
if got[i].Seq <= got[i-1].Seq {
t.Fatalf("Tail is not in chronological order: %v", got)
}
}
if got[len(got)-1].Seq != buf.LastSeq() {
t.Fatalf("Tail did not end at the newest record")
}
if n := len(buf.Tail(100)); n != 4 {
t.Fatalf("Tail beyond capacity returned %d, want 4", n)
}
if n := len(buf.Tail(0)); n != 0 {
t.Fatalf("Tail(0) returned %d entries", n)
}
}
func TestLogBufferRedactsOnExport(t *testing.T) {
buf := NewLogBuffer(16)
logger := slog.New(buf.Handler(nil))
logger.Info("joining", "auth_key", "tskey-auth-kSomeRealLookingKey123")
logger.Info("inline", "url", "https://x/?k=tskey-client-abcdefghijkl")
out := buf.ExportText(ExportOptions{Query: LogQuery{MinLevel: slog.LevelDebug}})
if indexOf(out, "kSomeRealLookingKey123") >= 0 {
t.Error("attribute-named secret survived redaction")
}
if indexOf(out, "abcdefghijkl") >= 0 {
t.Error("inline tskey survived redaction")
}
raw := buf.ExportText(ExportOptions{Query: LogQuery{MinLevel: slog.LevelDebug}, NoRedact: true})
if indexOf(raw, "kSomeRealLookingKey123") < 0 {
t.Error("NoRedact should preserve the original text")
}
}
func TestPeerMonitorSnapshotIsIsolated(t *testing.T) {
m := NewPeerMonitor(nil, nil, slog.New(slog.DiscardHandler), PeerMonitorOptions{})
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
defer cancel()
// A nil server must not panic; the monitor should degrade to an invalid
// snapshot with an error rather than taking the GUI down.
m.Start(ctx)
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for ctx.Err() == nil {
snap := m.Snapshot()
for _, p := range snap.Peers {
_ = p.DisplayName
_ = len(p.Samples)
}
_ = m.History("nonexistent")
m.RefreshNow()
}
}()
}
wg.Wait()
snap := m.Snapshot()
if snap.Valid {
t.Error("snapshot from a nil tsnet server should not be valid")
}
}