test(backend): enhance stubs with logging, heartbeat, timed exit, and fake downloads
arma3server_x64 stub: - Logs binary path and all arguments ([STUB] binary=... args=...) - Prints heartbeat every 5 seconds - Accepts -t N flag to exit after N seconds (for auto-exit tests) steamcmd stub: - Parses +force_install_dir and +workshop_download_item args - Creates fake mod directories at the expected workshop content path - Prints fake download progress and success logs New tests: - TestStart_StubLogsArgsAndPath: verifies arg logging via log streamer - TestStart_StubLogsBinaryPath: verifies binary path logging - TestStart_StubHeartbeatLogs: verifies heartbeat lines appear within timeout - TestStart_StubAutoExit: verifies -t flag causes server to auto-exit - TestDownloadMods_StubCreatesModDirs: verifies single mod dir creation - TestDownloadMods_StubCreatesMultipleModDirs: verifies multi-mod dir creation - TestDownloadMods_StubLogsAppear: verifies fake download logs on channel - TestDownloadMods_StubSteamcmdArgsLogged: verifies install_dir logging
This commit is contained in:
@@ -610,3 +610,166 @@ func TestServerParamsEnvOverridesSettings(t *testing.T) {
|
|||||||
pm.Stop()
|
pm.Stop()
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// helper: sets up a ProcessManager with the stub and optional server params
|
||||||
|
func setupStubPM(t *testing.T, serverParams string) (*ProcessManager, *LogStreamer) {
|
||||||
|
t.Helper()
|
||||||
|
dir := t.TempDir()
|
||||||
|
serverfileDir := filepath.Join(dir, "server")
|
||||||
|
os.MkdirAll(serverfileDir, 0755)
|
||||||
|
|
||||||
|
stubSrc, _ := os.ReadFile("testdata/arma3server_x64")
|
||||||
|
os.WriteFile(filepath.Join(serverfileDir, "arma3server_x64"), stubSrc, 0755)
|
||||||
|
|
||||||
|
sm := NewSettingsManager(dir)
|
||||||
|
mm := NewModlistManager(filepath.Join(dir, "modlists"))
|
||||||
|
cm := NewConfigManager(filepath.Join(dir, "cfg"))
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
pm := NewProcessManager(serverfileDir, filepath.Join(dir, "mods"), filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
|
||||||
|
|
||||||
|
if serverParams != "" {
|
||||||
|
s, _ := sm.Load()
|
||||||
|
s.ServerParameters = serverParams
|
||||||
|
sm.Save(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pm, streamer
|
||||||
|
}
|
||||||
|
|
||||||
|
// readLines reads up to n lines from ch within timeout
|
||||||
|
func readLines(ch chan string, n int, timeout time.Duration) []string {
|
||||||
|
var lines []string
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
for len(lines) < n {
|
||||||
|
remaining := time.Until(deadline)
|
||||||
|
if remaining <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case line, ok := <-ch:
|
||||||
|
if !ok {
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
lines = append(lines, line)
|
||||||
|
case <-time.After(remaining):
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsAny(s string, substrs ...string) bool {
|
||||||
|
for _, sub := range substrs {
|
||||||
|
if strings.Contains(s, sub) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStart_StubLogsArgsAndPath(t *testing.T) {
|
||||||
|
pm, streamer := setupStubPM(t, "-server -port=2402")
|
||||||
|
ch := streamer.Subscribe("server", "test-args")
|
||||||
|
defer streamer.Unsubscribe("server", "test-args")
|
||||||
|
|
||||||
|
if err := pm.Start(); err != nil {
|
||||||
|
t.Fatalf("Start() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := readLines(ch, 10, 3*time.Second)
|
||||||
|
|
||||||
|
foundArgsLog := false
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "[STUB]") && strings.Contains(line, "args=") {
|
||||||
|
foundArgsLog = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundArgsLog {
|
||||||
|
t.Errorf("expected [STUB] args= log line, got %v", lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.Stop()
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStart_StubLogsBinaryPath(t *testing.T) {
|
||||||
|
pm, streamer := setupStubPM(t, "-server -world=empty")
|
||||||
|
ch := streamer.Subscribe("server", "test-path")
|
||||||
|
defer streamer.Unsubscribe("server", "test-path")
|
||||||
|
|
||||||
|
if err := pm.Start(); err != nil {
|
||||||
|
t.Fatalf("Start() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := readLines(ch, 10, 3*time.Second)
|
||||||
|
|
||||||
|
foundBinaryLog := false
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "[STUB]") && strings.Contains(line, "binary=") {
|
||||||
|
foundBinaryLog = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundBinaryLog {
|
||||||
|
t.Errorf("expected [STUB] binary= log line, got %v", lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.Stop()
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStart_StubHeartbeatLogs(t *testing.T) {
|
||||||
|
pm, streamer := setupStubPM(t, "-t 12")
|
||||||
|
ch := streamer.Subscribe("server", "test-heartbeat")
|
||||||
|
defer streamer.Unsubscribe("server", "test-heartbeat")
|
||||||
|
|
||||||
|
if err := pm.Start(); err != nil {
|
||||||
|
t.Fatalf("Start() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect lines for up to 10s, looking for a heartbeat
|
||||||
|
lines := readLines(ch, 50, 10*time.Second)
|
||||||
|
|
||||||
|
foundHeartbeat := false
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "[STUB]") && strings.Contains(line, "heartbeat") {
|
||||||
|
foundHeartbeat = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundHeartbeat {
|
||||||
|
t.Errorf("expected [STUB] heartbeat log line within 10s, got %v", lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.Stop()
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStart_StubAutoExit(t *testing.T) {
|
||||||
|
pm, streamer := setupStubPM(t, "-t 2")
|
||||||
|
streamer.Subscribe("server", "test-autoexit")
|
||||||
|
defer streamer.Unsubscribe("server", "test-autoexit")
|
||||||
|
|
||||||
|
if err := pm.Start(); err != nil {
|
||||||
|
t.Fatalf("Start() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pm.IsRunning() {
|
||||||
|
t.Fatal("IsRunning() should be true immediately after Start()")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for stub to exit (2s + margin)
|
||||||
|
deadline := time.Now().Add(5 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if !pm.IsRunning() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pm.IsRunning() {
|
||||||
|
t.Error("IsRunning() should be false after stub auto-exited (timeout 2s)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package services
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -185,3 +186,147 @@ func TestSteamCmdManager_SteampathEnv(t *testing.T) {
|
|||||||
|
|
||||||
waitForNotRunning(t, sm, 30*time.Second)
|
waitForNotRunning(t, sm, 30*time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDownloadMods_StubCreatesModDirs(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
sm := NewSteamCmdManager(dir, streamer)
|
||||||
|
|
||||||
|
stubPath, _ := filepath.Abs("testdata/steamcmd")
|
||||||
|
t.Setenv("STEAMCMD_PATH", stubPath)
|
||||||
|
|
||||||
|
modID := "999999"
|
||||||
|
err := sm.DownloadMod(modID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DownloadMod error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForNotRunning(t, sm, 30*time.Second)
|
||||||
|
|
||||||
|
// Stub should have created the workshop mod directory
|
||||||
|
modDir := filepath.Join(dir, "steamapps", "workshop", "content", "107410", modID)
|
||||||
|
fi, err := os.Stat(modDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mod directory should exist after stub download: %v", err)
|
||||||
|
}
|
||||||
|
if !fi.IsDir() {
|
||||||
|
t.Errorf("mod path %q should be a directory", modDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloadMods_StubCreatesMultipleModDirs(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
sm := NewSteamCmdManager(dir, streamer)
|
||||||
|
|
||||||
|
stubPath, _ := filepath.Abs("testdata/steamcmd")
|
||||||
|
t.Setenv("STEAMCMD_PATH", stubPath)
|
||||||
|
|
||||||
|
mods := []string{"111111", "222222", "333333"}
|
||||||
|
err := sm.DownloadMods(mods)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DownloadMods error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForNotRunning(t, sm, 30*time.Second)
|
||||||
|
|
||||||
|
for _, modID := range mods {
|
||||||
|
modDir := filepath.Join(dir, "steamapps", "workshop", "content", "107410", modID)
|
||||||
|
if _, err := os.Stat(modDir); os.IsNotExist(err) {
|
||||||
|
t.Errorf("mod directory for %s should exist", modID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloadMods_StubLogsAppear(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
sm := NewSteamCmdManager(dir, streamer)
|
||||||
|
|
||||||
|
stubPath, _ := filepath.Abs("testdata/steamcmd")
|
||||||
|
t.Setenv("STEAMCMD_PATH", stubPath)
|
||||||
|
|
||||||
|
ch := streamer.Subscribe("steamcmd", "test-dl-logs")
|
||||||
|
defer streamer.Unsubscribe("steamcmd", "test-dl-logs")
|
||||||
|
|
||||||
|
err := sm.DownloadMod("444444")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DownloadMod error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read lines with generous timeout (stub may take a moment)
|
||||||
|
lines := readSteamcmdLines(ch, 20, 5*time.Second)
|
||||||
|
|
||||||
|
foundDownloading := false
|
||||||
|
foundSuccess := false
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "Downloading") && strings.Contains(line, "444444") {
|
||||||
|
foundDownloading = true
|
||||||
|
}
|
||||||
|
if strings.Contains(line, "Success") {
|
||||||
|
foundSuccess = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundDownloading {
|
||||||
|
t.Errorf("expected 'Downloading 444444' log line, got %v", lines)
|
||||||
|
}
|
||||||
|
if !foundSuccess {
|
||||||
|
t.Errorf("expected 'Success' log line, got %v", lines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloadMods_StubSteamcmdArgsLogged(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
sm := NewSteamCmdManager(dir, streamer)
|
||||||
|
|
||||||
|
stubPath, _ := filepath.Abs("testdata/steamcmd")
|
||||||
|
t.Setenv("STEAMCMD_PATH", stubPath)
|
||||||
|
|
||||||
|
ch := streamer.Subscribe("steamcmd", "test-args-logs")
|
||||||
|
defer streamer.Unsubscribe("steamcmd", "test-args-logs")
|
||||||
|
|
||||||
|
err := sm.DownloadMod("555555")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DownloadMod error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := readSteamcmdLines(ch, 20, 5*time.Second)
|
||||||
|
|
||||||
|
foundArgsLog := false
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "[STUB]") && strings.Contains(line, "install_dir=") {
|
||||||
|
foundArgsLog = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundArgsLog {
|
||||||
|
t.Errorf("expected [STUB] install_dir= log line, got %v", lines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// readSteamcmdLines reads lines from a steamcmd log channel
|
||||||
|
func readSteamcmdLines(ch chan string, n int, timeout time.Duration) []string {
|
||||||
|
var lines []string
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
for len(lines) < n {
|
||||||
|
remaining := time.Until(deadline)
|
||||||
|
if remaining <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case line, ok := <-ch:
|
||||||
|
if !ok {
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
lines = append(lines, line)
|
||||||
|
case <-time.After(remaining):
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|||||||
+29
-3
@@ -1,5 +1,31 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Stub arma3server binary for testing.
|
# Stub arma3server binary for testing.
|
||||||
# Logs args to stdout then sleeps until killed (simulates a running server).
|
# Logs binary path and all arguments, prints heartbeat every 5s.
|
||||||
echo "[STUB] arma3server_x64 called with: $@"
|
# Use -t N to exit after N seconds (for auto-exit tests).
|
||||||
sleep 3600
|
echo "[STUB] binary=$0 args=$*"
|
||||||
|
|
||||||
|
DURATION=0
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-t) DURATION="$2"; shift 2 ;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$DURATION" -gt 0 ] 2>/dev/null; then
|
||||||
|
ELAPSED=0
|
||||||
|
while [ "$ELAPSED" -lt "$DURATION" ]; do
|
||||||
|
REMAINING=$((DURATION - ELAPSED))
|
||||||
|
WAIT=5
|
||||||
|
if [ "$REMAINING" -lt 5 ]; then WAIT=$REMAINING; fi
|
||||||
|
sleep "$WAIT"
|
||||||
|
ELAPSED=$((ELAPSED + WAIT))
|
||||||
|
echo "[STUB] heartbeat elapsed=${ELAPSED}s remaining=$((REMAINING - WAIT))s"
|
||||||
|
done
|
||||||
|
echo "[STUB] server exiting after ${DURATION}s"
|
||||||
|
else
|
||||||
|
while true; do
|
||||||
|
sleep 5
|
||||||
|
echo "[STUB] heartbeat running..."
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|||||||
+28
-2
@@ -1,5 +1,31 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Stub steamcmd binary for testing.
|
# Stub steamcmd binary for testing.
|
||||||
# Logs args to stdout then exits immediately.
|
# Parses +force_install_dir and +workshop_download_item args.
|
||||||
echo "[STUB] steamcmd called with: $@"
|
# Creates fake mod directories and prints fake download logs.
|
||||||
|
|
||||||
|
INSTALL_DIR=""
|
||||||
|
MOD_IDS=""
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
+force_install_dir) INSTALL_DIR="$2"; shift 2 ;;
|
||||||
|
+workshop_download_item)
|
||||||
|
APP_ID="$2"; MOD_ID="$3"; shift 3
|
||||||
|
MOD_IDS="$MOD_IDS $MOD_ID"
|
||||||
|
;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[STUB] steamcmd install_dir=$INSTALL_DIR mods=$MOD_IDS"
|
||||||
|
|
||||||
|
for MOD_ID in $MOD_IDS; do
|
||||||
|
MOD_DIR="$INSTALL_DIR/steamapps/workshop/content/107410/$MOD_ID"
|
||||||
|
echo "[STUB] Downloading item $MOD_ID (App $APP_ID)..."
|
||||||
|
echo "[STUB] Downloading 100% [//////////]"
|
||||||
|
mkdir -p "$MOD_DIR"
|
||||||
|
echo "[STUB] Success! App $APP_ID item $MOD_ID installed to $MOD_DIR"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[STUB] steamcmd exiting"
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
Reference in New Issue
Block a user