docs: rewrite PLAN.md and update CODEBASE.md, README.md
- Full rewrite of PLAN.md to reflect current architecture (27 REST + 3 WS endpoints, embedded frontend, automation, scheduler, health check, GoReleaser CI) - Added health.go, mods.go, scheduler.go, robfig/cron dep to CODEBASE.md - Added Gitea Actions CI/CD section to CODEBASE.md - Added conventional commits to code style section - Added missing API routes and embed/ to README.md
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLogStreamer_SubscribeUnsubscribe(t *testing.T) {
|
||||
ls := NewLogStreamer()
|
||||
|
||||
ch := ls.Subscribe("server", "client1")
|
||||
if ch == nil {
|
||||
t.Fatal("Subscribe() returned nil channel")
|
||||
}
|
||||
|
||||
ls.Unsubscribe("server", "client1")
|
||||
|
||||
// Channel should be closed after unsubscribe
|
||||
select {
|
||||
case _, ok := <-ch:
|
||||
if ok {
|
||||
t.Error("channel should be closed after unsubscribe")
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("channel not closed within timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogStreamer_Broadcast(t *testing.T) {
|
||||
ls := NewLogStreamer()
|
||||
|
||||
ch1 := ls.Subscribe("server", "client1")
|
||||
ch2 := ls.Subscribe("server", "client2")
|
||||
|
||||
ls.Broadcast("server", "hello")
|
||||
|
||||
for _, ch := range []chan string{ch1, ch2} {
|
||||
select {
|
||||
case line := <-ch:
|
||||
if line != "hello" {
|
||||
t.Errorf("received %q, want %q", line, "hello")
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("timeout waiting for broadcast")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogStreamer_BroadcastNonBlocking(t *testing.T) {
|
||||
ls := NewLogStreamer()
|
||||
|
||||
ch := ls.Subscribe("server", "client1")
|
||||
|
||||
// Fill the channel (capacity 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
ls.Broadcast("server", "line")
|
||||
}
|
||||
|
||||
// This should not block even though channel is full
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
ls.Broadcast("server", "dropped")
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Good, broadcast didn't block
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("broadcast blocked on full channel")
|
||||
}
|
||||
|
||||
// Drain all queued messages
|
||||
for i := 0; i < 256; i++ {
|
||||
<-ch
|
||||
}
|
||||
|
||||
// Now the next broadcast should succeed
|
||||
ls.Broadcast("server", "after")
|
||||
select {
|
||||
case line := <-ch:
|
||||
if line != "after" {
|
||||
t.Errorf("received %q, want %q", line, "after")
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("timeout waiting for broadcast")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogStreamer_Stream(t *testing.T) {
|
||||
ls := NewLogStreamer()
|
||||
|
||||
ch := ls.Subscribe("server", "client1")
|
||||
|
||||
reader := strings.NewReader("line1\nline2\nline3\n")
|
||||
go ls.Stream("server", reader, "DONE")
|
||||
|
||||
lines := []string{}
|
||||
for i := 0; i < 4; i++ { // 3 lines + DONE close message
|
||||
select {
|
||||
case line := <-ch:
|
||||
lines = append(lines, line)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatalf("timeout waiting for line %d", i+1)
|
||||
}
|
||||
}
|
||||
|
||||
if lines[0] != "line1" || lines[1] != "line2" || lines[2] != "line3" || lines[3] != "DONE" {
|
||||
t.Errorf("lines = %v, want [line1 line2 line3 DONE]", lines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogStreamer_UnsubscribeCleansUpEmptyServer(t *testing.T) {
|
||||
ls := NewLogStreamer()
|
||||
|
||||
ls.Subscribe("server", "client1")
|
||||
ls.Unsubscribe("server", "client1")
|
||||
|
||||
// After unsubscribing the last client, the server entry should be cleaned up
|
||||
ls.mu.RLock()
|
||||
_, exists := ls.subs["server"]
|
||||
ls.mu.RUnlock()
|
||||
|
||||
if exists {
|
||||
t.Error("empty server entry should be cleaned up after last unsubscribe")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogStreamer_MultipleServers(t *testing.T) {
|
||||
ls := NewLogStreamer()
|
||||
|
||||
chServer := ls.Subscribe("server", "client1")
|
||||
chSteamcmd := ls.Subscribe("steamcmd", "client1")
|
||||
|
||||
ls.Broadcast("server", "server msg")
|
||||
ls.Broadcast("steamcmd", "steamcmd msg")
|
||||
|
||||
select {
|
||||
case line := <-chServer:
|
||||
if line != "server msg" {
|
||||
t.Errorf("server channel received %q, want %q", line, "server msg")
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("timeout on server channel")
|
||||
}
|
||||
|
||||
// SteamCMD channel should not have received the server message
|
||||
select {
|
||||
case line := <-chSteamcmd:
|
||||
if line != "steamcmd msg" {
|
||||
t.Errorf("steamcmd channel received %q, want %q", line, "steamcmd msg")
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("timeout on steamcmd channel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogStreamer_ConcurrentSubscribeUnsubscribe(t *testing.T) {
|
||||
ls := NewLogStreamer()
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
clientID := "client"
|
||||
ch := ls.Subscribe("server", clientID)
|
||||
_ = ch
|
||||
ls.Unsubscribe("server", clientID)
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
Reference in New Issue
Block a user