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
333 lines
8.1 KiB
Go
333 lines
8.1 KiB
Go
package services
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCheckWorkshopMod(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
// Existing workshop mod
|
|
modDir := filepath.Join(dir, "steamapps", "workshop", "content", "107410", "123456")
|
|
os.MkdirAll(modDir, 0755)
|
|
|
|
if !CheckWorkshopMod(dir, "123456") {
|
|
t.Error("CheckWorkshopMod should return true for existing mod")
|
|
}
|
|
|
|
// Non-existing mod
|
|
if CheckWorkshopMod(dir, "999999") {
|
|
t.Error("CheckWorkshopMod should return false for non-existing mod")
|
|
}
|
|
|
|
// Empty mod ID
|
|
if CheckWorkshopMod(dir, "") {
|
|
t.Error("CheckWorkshopMod should return false for empty mod ID")
|
|
}
|
|
}
|
|
|
|
func TestCheckWorkshopMod_IsFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
// Create a file instead of directory at the expected path
|
|
workshopDir := filepath.Join(dir, "steamapps", "workshop", "content", "107410")
|
|
os.MkdirAll(workshopDir, 0755)
|
|
os.WriteFile(filepath.Join(workshopDir, "111111"), []byte("not a dir"), 0644)
|
|
|
|
if CheckWorkshopMod(dir, "111111") {
|
|
t.Error("CheckWorkshopMod should return false for file (not directory)")
|
|
}
|
|
}
|
|
|
|
func TestSteamCmdManager_IsRunning(t *testing.T) {
|
|
dir := t.TempDir()
|
|
streamer := NewLogStreamer()
|
|
|
|
sm := NewSteamCmdManager(dir, streamer)
|
|
|
|
if sm.IsRunning() {
|
|
t.Error("IsRunning() should be false initially")
|
|
}
|
|
}
|
|
|
|
func TestSteamCmdManager_DoubleStart(t *testing.T) {
|
|
dir := t.TempDir()
|
|
streamer := NewLogStreamer()
|
|
|
|
sm := NewSteamCmdManager(dir, streamer)
|
|
|
|
// First DownloadMods sets running=true. If the binary exists, the goroutine
|
|
// handles cleanup. If it doesn't, running is reset synchronously.
|
|
sm.DownloadMods([]string{"123456"})
|
|
|
|
// Second call while running should get "already running"
|
|
err2 := sm.DownloadMods([]string{"789012"})
|
|
if err2 == nil || err2.Error() != "steamcmd already running" {
|
|
t.Logf("second DownloadMods: %v (may vary based on timing)", err2)
|
|
}
|
|
|
|
waitForNotRunning(t, sm, 30*time.Second)
|
|
}
|
|
|
|
func waitForNotRunning(t *testing.T, sm *SteamCmdManager, timeout time.Duration) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
if !sm.IsRunning() {
|
|
return
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
t.Errorf("IsRunning() still true after %v", timeout)
|
|
}
|
|
|
|
func TestSteamCmdManager_StopResetsFlag(t *testing.T) {
|
|
dir := t.TempDir()
|
|
streamer := NewLogStreamer()
|
|
|
|
sm := NewSteamCmdManager(dir, streamer)
|
|
|
|
// After run completes (success or failure), running flag should be reset
|
|
sm.DownloadMods([]string{"123"})
|
|
waitForNotRunning(t, sm, 30*time.Second)
|
|
}
|
|
|
|
func TestSteamCmdManager_UpdateGameArgs(t *testing.T) {
|
|
dir := t.TempDir()
|
|
streamer := NewLogStreamer()
|
|
|
|
sm := NewSteamCmdManager(dir, streamer)
|
|
|
|
err := sm.UpdateGame("stable", "anonymous")
|
|
if err != nil {
|
|
t.Logf("UpdateGame returned (expected): %v", err)
|
|
}
|
|
|
|
waitForNotRunning(t, sm, 30*time.Second)
|
|
}
|
|
|
|
func TestSteamCmdManager_UpdateGameBeta(t *testing.T) {
|
|
dir := t.TempDir()
|
|
streamer := NewLogStreamer()
|
|
|
|
sm := NewSteamCmdManager(dir, streamer)
|
|
|
|
// Test with a beta branch
|
|
err := sm.UpdateGame("creatordlc", "anonymous")
|
|
if err != nil {
|
|
t.Logf("UpdateGame(creatordlc) returned (expected): %v", err)
|
|
}
|
|
|
|
waitForNotRunning(t, sm, 30*time.Second)
|
|
}
|
|
|
|
func TestSteamCmdManager_DownloadModEmpty(t *testing.T) {
|
|
dir := t.TempDir()
|
|
streamer := NewLogStreamer()
|
|
|
|
sm := NewSteamCmdManager(dir, streamer)
|
|
|
|
err := sm.DownloadMods([]string{})
|
|
if err == nil {
|
|
t.Error("DownloadMods with empty list should return error")
|
|
}
|
|
}
|
|
|
|
func TestSteamCmdManager_DownloadModSingle(t *testing.T) {
|
|
dir := t.TempDir()
|
|
streamer := NewLogStreamer()
|
|
|
|
sm := NewSteamCmdManager(dir, streamer)
|
|
|
|
err := sm.DownloadMod("123456")
|
|
// steamcmd may or may not exist; either way the call should complete
|
|
if err != nil {
|
|
t.Logf("DownloadMod returned (expected): %v", err)
|
|
}
|
|
|
|
waitForNotRunning(t, sm, 30*time.Second)
|
|
}
|
|
|
|
func TestNewSteamCmdManager(t *testing.T) {
|
|
dir := t.TempDir()
|
|
streamer := NewLogStreamer()
|
|
|
|
sm := NewSteamCmdManager(dir, streamer)
|
|
|
|
if sm.serverfileDir != dir {
|
|
t.Errorf("serverfileDir = %q, want %q", sm.serverfileDir, dir)
|
|
}
|
|
if sm.streamer != streamer {
|
|
t.Error("streamer should be set")
|
|
}
|
|
if sm.IsRunning() {
|
|
t.Error("new manager should not be running")
|
|
}
|
|
}
|
|
|
|
func TestSteamCmdManager_SteampathEnv(t *testing.T) {
|
|
dir := t.TempDir()
|
|
streamer := NewLogStreamer()
|
|
|
|
sm := NewSteamCmdManager(dir, streamer)
|
|
|
|
// Point STEAMCMD_PATH at the stub so it doesn't need the real binary
|
|
stubPath, _ := filepath.Abs("testdata/steamcmd")
|
|
t.Setenv("STEAMCMD_PATH", stubPath)
|
|
|
|
err := sm.DownloadMod("123456")
|
|
if err != nil {
|
|
t.Fatalf("DownloadMod with STEAMCMD_PATH env error = %v", err)
|
|
}
|
|
|
|
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
|
|
}
|