panel — open-source game server manager (public release)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
modulepkg "github.com/dbledeez/panel/pkg/module"
|
||||
panelv1 "github.com/dbledeez/panel/proto/panel/v1"
|
||||
)
|
||||
|
||||
type captureEmitter struct {
|
||||
appStates []*panelv1.AppStateUpdate
|
||||
playerEvents []*panelv1.PlayerEvent
|
||||
}
|
||||
|
||||
func (c *captureEmitter) EmitAppState(a *panelv1.AppStateUpdate) { c.appStates = append(c.appStates, a) }
|
||||
func (c *captureEmitter) EmitPlayerEvent(p *panelv1.PlayerEvent) { c.playerEvents = append(c.playerEvents, p) }
|
||||
|
||||
func newTestTracker(t *testing.T, m *modulepkg.Manifest) (*Tracker, *captureEmitter) {
|
||||
t.Helper()
|
||||
cap := &captureEmitter{}
|
||||
log := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
tr, err := New(log, Config{
|
||||
InstanceID: "inst-1",
|
||||
Manifest: m,
|
||||
Emitter: cap,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
return tr, cap
|
||||
}
|
||||
|
||||
func Test7DTDJoinPattern(t *testing.T) {
|
||||
manifest, err := modulepkg.LoadFile("../../../modules/7dtd/module.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("load 7dtd: %v", err)
|
||||
}
|
||||
tr, cap := newTestTracker(t, manifest)
|
||||
|
||||
// Real-world 7DTD log line shape
|
||||
line := `2026-04-19T10:00:00 42.123 INF PlayerSpawnedInWorld (by system): PltfmId='Steam_76561198000000001', CrossId='EOS_00000000', OwnerID='Steam_76561198000000001', PlayerName='Alice'`
|
||||
tr.OnLogLine(line)
|
||||
|
||||
if len(cap.playerEvents) != 1 {
|
||||
t.Fatalf("expected 1 player event, got %d", len(cap.playerEvents))
|
||||
}
|
||||
ev := cap.playerEvents[0]
|
||||
if ev.Kind != panelv1.PlayerEvent_KIND_JOIN {
|
||||
t.Errorf("kind = %v, want JOIN", ev.Kind)
|
||||
}
|
||||
if ev.PlayerName != "Alice" {
|
||||
t.Errorf("name = %q, want Alice", ev.PlayerName)
|
||||
}
|
||||
if ev.PlayerId == "" {
|
||||
t.Errorf("player_id empty; expected Steam_... from named groups")
|
||||
}
|
||||
}
|
||||
|
||||
func Test7DTDChatPattern(t *testing.T) {
|
||||
manifest, err := modulepkg.LoadFile("../../../modules/7dtd/module.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("load 7dtd: %v", err)
|
||||
}
|
||||
tr, cap := newTestTracker(t, manifest)
|
||||
|
||||
line := `2026-04-19T10:00:00 42.123 INF Chat (from 'Steam_76561198000000001', entity id '123', to 'Global'): 'Alice': hello world`
|
||||
tr.OnLogLine(line)
|
||||
|
||||
if len(cap.playerEvents) != 1 {
|
||||
t.Fatalf("expected 1 player event, got %d", len(cap.playerEvents))
|
||||
}
|
||||
ev := cap.playerEvents[0]
|
||||
if ev.Kind != panelv1.PlayerEvent_KIND_CHAT {
|
||||
t.Errorf("kind = %v, want CHAT", ev.Kind)
|
||||
}
|
||||
if ev.PlayerName != "Alice" {
|
||||
t.Errorf("name = %q, want Alice", ev.PlayerName)
|
||||
}
|
||||
if ev.Detail != "hello world" {
|
||||
t.Errorf("detail = %q, want 'hello world'", ev.Detail)
|
||||
}
|
||||
}
|
||||
|
||||
func Test7DTDLPParse(t *testing.T) {
|
||||
manifest, err := modulepkg.LoadFile("../../../modules/7dtd/module.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("load 7dtd: %v", err)
|
||||
}
|
||||
var lp *modulepkg.StateSource
|
||||
for i := range manifest.StateSources {
|
||||
if manifest.StateSources[i].Type == "rcon" {
|
||||
lp = &manifest.StateSources[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if lp == nil {
|
||||
t.Fatal("no rcon state source in 7dtd manifest")
|
||||
}
|
||||
|
||||
sample := "Total of 3 in the game\n\n1. id=1, Alice, pos=(0,0,0)\n2. id=2, Bob, pos=(1,0,0)\n3. id=3, Carol, pos=(2,0,0)"
|
||||
app := parseStateOutput("inst-1", sample, *lp)
|
||||
if app == nil {
|
||||
t.Fatal("parse returned nil")
|
||||
}
|
||||
if app.PlayersOnline != 3 {
|
||||
t.Errorf("players_online = %d, want 3", app.PlayersOnline)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLineWithNoMatchYieldsNoEvent(t *testing.T) {
|
||||
manifest, err := modulepkg.LoadFile("../../../modules/7dtd/module.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("load 7dtd: %v", err)
|
||||
}
|
||||
tr, cap := newTestTracker(t, manifest)
|
||||
tr.OnLogLine("this is some random log line that matches nothing")
|
||||
if len(cap.playerEvents) != 0 {
|
||||
t.Errorf("expected 0 events, got %d", len(cap.playerEvents))
|
||||
}
|
||||
}
|
||||
|
||||
// ---- WI-05: hoisted state-source regex compilation ----
|
||||
|
||||
// TestNewRejectsBadStateSourcePattern: a malformed parse regex must fail at
|
||||
// construction (surfacing the manifest bug at init) instead of silently
|
||||
// recompiling-and-failing on every poll tick.
|
||||
func TestNewRejectsBadStateSourcePattern(t *testing.T) {
|
||||
m := &modulepkg.Manifest{
|
||||
StateSources: []modulepkg.StateSource{{
|
||||
Type: "rcon",
|
||||
Command: "lp",
|
||||
Parse: &modulepkg.ParseConfig{Kind: "regex", Pattern: "("},
|
||||
}},
|
||||
}
|
||||
log := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
if _, err := New(log, Config{InstanceID: "i", Manifest: m, Emitter: &captureEmitter{}}); err == nil {
|
||||
t.Fatal("New accepted an invalid state-source regex; want construction error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseStateUsesPrecompiledRegex drives the tracker's precompiled parse
|
||||
// path with the real 7DTD manifest and confirms it matches the uncached
|
||||
// package-level parser.
|
||||
func TestParseStateUsesPrecompiledRegex(t *testing.T) {
|
||||
manifest, err := modulepkg.LoadFile("../../../modules/7dtd/module.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("load 7dtd: %v", err)
|
||||
}
|
||||
tr, _ := newTestTracker(t, manifest)
|
||||
var lp *modulepkg.StateSource
|
||||
for i := range manifest.StateSources {
|
||||
if manifest.StateSources[i].Type == "rcon" {
|
||||
lp = &manifest.StateSources[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if lp == nil {
|
||||
t.Fatal("no rcon state source in 7dtd manifest")
|
||||
}
|
||||
sample := "Total of 3 in the game\n\n1. id=1, Alice, pos=(0,0,0)\n2. id=2, Bob, pos=(1,0,0)\n3. id=3, Carol, pos=(2,0,0)"
|
||||
app := tr.parseState(sample, *lp)
|
||||
if app == nil {
|
||||
t.Fatal("precompiled parseState returned nil")
|
||||
}
|
||||
if app.PlayersOnline != 3 {
|
||||
t.Errorf("players_online = %d, want 3", app.PlayersOnline)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user