Files
ArmA-3-web-server/backend/internal/services/server_process_test.go
T
MrFastwind 6f3175ac9a
CI / build (push) Successful in 1m44s
feat: add Wine Z: path prefix for Windows platform on Linux host
- Add winePath helper that prepends Z: when platform is windows on Linux
- Thread platform through buildModPath, buildAutoArgs, and Start()
- Apply Z: prefix to -config=, -mod=, -profiles=, and %command% binary paths
- Add tests for winePath, buildAutoArgs, and buildModPath with platform param
- Fix formatting across services package with gofmt
- Include all prior bug fixes from this branch
2026-07-24 03:57:51 +02:00

986 lines
27 KiB
Go

package services
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"arma3-web-server/internal/models"
)
func TestSplitArgs(t *testing.T) {
tests := []struct {
name string
raw string
want []string
}{
{"empty", "", nil},
{"single", "-server", []string{"-server"}},
{"multiple", "-server -port=2302 -world=empty", []string{"-server", "-port=2302", "-world=empty"}},
{"extra spaces", " -server -port=2302 ", []string{"-server", "-port=2302"}},
{"tabs", "-server\t-port=2302", []string{"-server", "-port=2302"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := splitArgs(tt.raw)
if len(got) != len(tt.want) {
t.Fatalf("splitArgs(%q) = %v, want %v", tt.raw, got, tt.want)
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("splitArgs(%q)[%d] = %q, want %q", tt.raw, i, got[i], tt.want[i])
}
}
})
}
}
func TestHasArgPrefix(t *testing.T) {
args := []string{"-config=foo.cfg", "-mod=@ace", "-port=2302"}
if !hasArgPrefix(args, "-config=") {
t.Error("should find -config=")
}
if !hasArgPrefix(args, "-mod=") {
t.Error("should find -mod=")
}
if hasArgPrefix(args, "-profiles=") {
t.Error("should not find -profiles=")
}
if hasArgPrefix(nil, "-config=") {
t.Error("nil args should return false")
}
}
func TestBuildAutoArgs_ConfigAndPort(t *testing.T) {
dir := t.TempDir()
cfgDir := filepath.Join(dir, "cfg")
profilesDir := filepath.Join(dir, "profiles")
modsDir := filepath.Join(dir, "mods")
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(cfgDir)
streamer := NewLogStreamer()
pm := NewProcessManager(dir, modsDir, cfgDir, profilesDir, sm, mm, cm, streamer)
s := &models.ServerSettings{
ActiveConfig: "server_config",
IPPort: "0.0.0.0:2302",
}
args := pm.buildAutoArgs(s)
// Should contain -config=<cfgDir>/server_config.cfg
foundConfig := false
for _, a := range args {
if strings.HasPrefix(a, "-config=") && strings.HasSuffix(a, "server_config.cfg") {
foundConfig = true
}
}
if !foundConfig {
t.Errorf("expected -config=...server_config.cfg in args, got %v", args)
}
// Should contain -port=2302
foundPort := false
for _, a := range args {
if a == "-port=2302" {
foundPort = true
}
}
if !foundPort {
t.Errorf("expected -port=2302 in args, got %v", args)
}
// Should contain -profiles=
foundProfiles := false
for _, a := range args {
if strings.HasPrefix(a, "-profiles=") {
foundProfiles = true
}
}
if !foundProfiles {
t.Errorf("expected -profiles=... in args, got %v", args)
}
}
func TestBuildAutoArgs_NoConfigNoPort(t *testing.T) {
dir := t.TempDir()
cfgDir := filepath.Join(dir, "cfg")
profilesDir := filepath.Join(dir, "profiles")
modsDir := filepath.Join(dir, "mods")
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(cfgDir)
streamer := NewLogStreamer()
pm := NewProcessManager(dir, modsDir, cfgDir, profilesDir, sm, mm, cm, streamer)
s := &models.ServerSettings{}
args := pm.buildAutoArgs(s)
// Should NOT contain -config= or -port=
for _, a := range args {
if strings.HasPrefix(a, "-config=") {
t.Errorf("unexpected -config= in args: %v", a)
}
if strings.HasPrefix(a, "-port=") {
t.Errorf("unexpected -port= in args: %v", a)
}
}
}
func TestBuildAutoArgs_PortAlreadyInUserArgs(t *testing.T) {
dir := t.TempDir()
cfgDir := filepath.Join(dir, "cfg")
profilesDir := filepath.Join(dir, "profiles")
modsDir := filepath.Join(dir, "mods")
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(cfgDir)
streamer := NewLogStreamer()
pm := NewProcessManager(dir, modsDir, cfgDir, profilesDir, sm, mm, cm, streamer)
s := &models.ServerSettings{
IPPort: "0.0.0.0:2302",
}
args := pm.buildAutoArgs(s)
// -port= should only appear once (from auto args)
portCount := 0
for _, a := range args {
if strings.HasPrefix(a, "-port=") {
portCount++
}
}
if portCount != 1 {
t.Errorf("expected exactly 1 -port= arg, got %d in %v", portCount, args)
}
}
func TestBuildArgs_CombinesParametersAndAutoArgs(t *testing.T) {
dir := t.TempDir()
cfgDir := filepath.Join(dir, "cfg")
profilesDir := filepath.Join(dir, "profiles")
modsDir := filepath.Join(dir, "mods")
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(cfgDir)
streamer := NewLogStreamer()
pm := NewProcessManager(dir, modsDir, cfgDir, profilesDir, sm, mm, cm, streamer)
s := &models.ServerSettings{
ServerParameters: "-server -world=empty -noPause",
ActiveConfig: "main",
IPPort: "0.0.0.0:2400",
}
args := pm.buildArgs(s)
// Should start with user params
if args[0] != "-server" || args[1] != "-world=empty" || args[2] != "-noPause" {
t.Errorf("user params missing from front of args: %v", args)
}
// Should contain auto-generated args
foundConfig := false
foundPort := false
for _, a := range args {
if strings.Contains(a, "main.cfg") {
foundConfig = true
}
if a == "-port=2400" {
foundPort = true
}
}
if !foundConfig {
t.Errorf("expected config arg in %v", args)
}
if !foundPort {
t.Errorf("expected port arg in %v", args)
}
}
func TestResolveModPath_ByModName(t *testing.T) {
dir := t.TempDir()
modsDir := filepath.Join(dir, "mods")
modDir := filepath.Join(modsDir, "@CBA_A3")
os.MkdirAll(modDir, 0755)
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(filepath.Join(dir, "cfg"))
streamer := NewLogStreamer()
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
// Existing mod dir → resolved path
got := pm.resolveModPath(models.ModEntry{Name: "CBA_A3"})
if got != modDir {
t.Errorf("resolveModPath(CBA_A3) = %q, want %q", got, modDir)
}
// Non-existing mod dir → fallback to expected path
got = pm.resolveModPath(models.ModEntry{Name: "NonExistent"})
expected := filepath.Join(modsDir, "@NonExistent")
if got != expected {
t.Errorf("resolveModPath(NonExistent) = %q, want %q", got, expected)
}
}
func TestResolveModPath_ByWorkshopID(t *testing.T) {
dir := t.TempDir()
workshopDir := filepath.Join(dir, "steamapps", "workshop", "content", "107410", "456789")
os.MkdirAll(workshopDir, 0755)
modsDir := filepath.Join(dir, "mods")
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(filepath.Join(dir, "cfg"))
streamer := NewLogStreamer()
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
got := pm.resolveModPath(models.ModEntry{ID: "456789"})
if got != workshopDir {
t.Errorf("resolveModPath(ID=456789) = %q, want %q", got, workshopDir)
}
}
func TestResolveModPath_EmptyEntry(t *testing.T) {
dir := t.TempDir()
modsDir := filepath.Join(dir, "mods")
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(filepath.Join(dir, "cfg"))
streamer := NewLogStreamer()
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
got := pm.resolveModPath(models.ModEntry{})
if got != "" {
t.Errorf("resolveModPath(empty) = %q, want empty", got)
}
}
func TestBuildModPath_MultipleMods(t *testing.T) {
dir := t.TempDir()
modsDir := filepath.Join(dir, "mods")
os.MkdirAll(filepath.Join(modsDir, "@CBA_A3"), 0755)
os.MkdirAll(filepath.Join(modsDir, "@ACE3"), 0755)
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(filepath.Join(dir, "cfg"))
streamer := NewLogStreamer()
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
ml, _ := mm.Create("Test List")
mm.Update(ml.ID, "Test List", []models.ModEntry{
{Name: "CBA_A3", Enabled: true},
{Name: "ACE3", Enabled: true},
{Name: "DisabledMod", Enabled: false},
})
got := pm.buildModPath(ml.ID, "")
if !strings.HasPrefix(got, "-mod=") {
t.Errorf("expected -mod= prefix, got %q", got)
}
if !strings.Contains(got, "CBA_A3") || !strings.Contains(got, "ACE3") {
t.Errorf("expected CBA_A3 and ACE3 in mod path, got %q", got)
}
// Disabled mod should not appear
if strings.Contains(got, "DisabledMod") {
t.Errorf("DisabledMod should not be in mod path: %q", got)
}
// Separator should be ;
if !strings.Contains(got, ";") {
t.Errorf("expected ; separator in mod path, got %q", got)
}
}
func TestBuildModPath_EmptyModlist(t *testing.T) {
dir := t.TempDir()
modsDir := filepath.Join(dir, "mods")
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(filepath.Join(dir, "cfg"))
streamer := NewLogStreamer()
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
got := pm.buildModPath("nonexistent-id", "")
if got != "" {
t.Errorf("buildModPath(nonexistent) = %q, want empty", got)
}
}
func TestWriteUserconfigFiles(t *testing.T) {
dir := t.TempDir()
modsDir := filepath.Join(dir, "mods")
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(filepath.Join(dir, "cfg"))
streamer := NewLogStreamer()
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
s := &models.ServerSettings{
CBASettings: "force = 1",
AILevelPresets: "preset1",
DifficultyPresets: "difficulty_normal",
}
if err := pm.WriteUserconfigFiles(s); err != nil {
t.Fatalf("WriteUserconfigFiles() error = %v", err)
}
userconfigDir := filepath.Join(dir, "userconfig")
tests := []struct {
filename string
content string
}{
{"cba_settings.sqf", "force = 1"},
{"CfgAILevelPresets.sqf", "preset1"},
{"CfgDifficultyPresets.sqf", "difficulty_normal"},
}
for _, tt := range tests {
path := filepath.Join(userconfigDir, tt.filename)
data, err := os.ReadFile(path)
if err != nil {
t.Errorf("ReadFile(%s) error = %v", tt.filename, err)
continue
}
if string(data) != tt.content {
t.Errorf("%s content = %q, want %q", tt.filename, string(data), tt.content)
}
}
}
func TestWriteUserconfigFiles_EmptyContent(t *testing.T) {
dir := t.TempDir()
modsDir := filepath.Join(dir, "mods")
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(filepath.Join(dir, "cfg"))
streamer := NewLogStreamer()
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
s := &models.ServerSettings{}
if err := pm.WriteUserconfigFiles(s); err != nil {
t.Fatalf("WriteUserconfigFiles() error = %v", err)
}
// Files should still be created (even if empty)
for _, name := range []string{"cba_settings.sqf", "CfgAILevelPresets.sqf", "CfgDifficultyPresets.sqf"} {
path := filepath.Join(dir, "userconfig", name)
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Errorf("expected %s to exist", name)
}
}
}
func TestWinePath(t *testing.T) {
tests := []struct {
name string
path string
platform string
want string
}{
{"linux platform returns as-is", "/server/cfg/test.cfg", "linux", "/server/cfg/test.cfg"},
{"empty platform returns as-is", "/server/cfg/test.cfg", "", "/server/cfg/test.cfg"},
{"windows platform on linux adds prefix", "/server/cfg/test.cfg", "windows", "Z:/server/cfg/test.cfg"},
{"empty path linux", "", "linux", ""},
{"empty path windows", "", "windows", "Z:"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := winePath(tt.path, tt.platform)
if got != tt.want {
t.Errorf("winePath(%q, %q) = %q, want %q", tt.path, tt.platform, got, tt.want)
}
})
}
}
func TestBuildAutoArgs_WindowsPlatform(t *testing.T) {
dir := t.TempDir()
cfgDir := filepath.Join(dir, "cfg")
profilesDir := filepath.Join(dir, "profiles")
modsDir := filepath.Join(dir, "mods")
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(cfgDir)
streamer := NewLogStreamer()
pm := NewProcessManager(dir, modsDir, cfgDir, profilesDir, sm, mm, cm, streamer)
s := &models.ServerSettings{
Platform: "windows",
ActiveConfig: "server_config",
IPPort: "0.0.0.0:2302",
}
args := pm.buildAutoArgs(s)
foundConfig := false
foundProfiles := false
for _, a := range args {
if strings.HasPrefix(a, "-config=Z:") && strings.HasSuffix(a, "server_config.cfg") {
foundConfig = true
}
if strings.HasPrefix(a, "-profiles=Z:") {
foundProfiles = true
}
}
if !foundConfig {
t.Errorf("expected -config=Z:...server_config.cfg in args, got %v", args)
}
if !foundProfiles {
t.Errorf("expected -profiles=Z:... in args, got %v", args)
}
}
func TestBuildAutoArgs_LinuxPlatform(t *testing.T) {
dir := t.TempDir()
cfgDir := filepath.Join(dir, "cfg")
profilesDir := filepath.Join(dir, "profiles")
modsDir := filepath.Join(dir, "mods")
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(cfgDir)
streamer := NewLogStreamer()
pm := NewProcessManager(dir, modsDir, cfgDir, profilesDir, sm, mm, cm, streamer)
s := &models.ServerSettings{
Platform: "linux",
ActiveConfig: "server_config",
IPPort: "0.0.0.0:2302",
}
args := pm.buildAutoArgs(s)
for _, a := range args {
if strings.HasPrefix(a, "-config=Z:") {
t.Errorf("unexpected Z: prefix in args: %v", a)
}
}
}
func TestBuildModPath_WindowsPlatform(t *testing.T) {
dir := t.TempDir()
modsDir := filepath.Join(dir, "mods")
os.MkdirAll(filepath.Join(modsDir, "@CBA_A3"), 0755)
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(filepath.Join(dir, "cfg"))
streamer := NewLogStreamer()
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
ml, _ := mm.Create("Test List")
mm.Update(ml.ID, "Test List", []models.ModEntry{
{Name: "CBA_A3", Enabled: true},
})
got := pm.buildModPath(ml.ID, "windows")
if !strings.HasPrefix(got, "-mod=Z:") {
t.Errorf("expected -mod=Z: prefix, got %q", got)
}
}
func TestBuildModPath_LinuxPlatform(t *testing.T) {
dir := t.TempDir()
modsDir := filepath.Join(dir, "mods")
os.MkdirAll(filepath.Join(modsDir, "@CBA_A3"), 0755)
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(filepath.Join(dir, "cfg"))
streamer := NewLogStreamer()
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
ml, _ := mm.Create("Test List")
mm.Update(ml.ID, "Test List", []models.ModEntry{
{Name: "CBA_A3", Enabled: true},
})
got := pm.buildModPath(ml.ID, "linux")
if strings.Contains(got, "Z:") {
t.Errorf("unexpected Z: prefix in args: %q", got)
}
}
func TestStartAndStopWithStub(t *testing.T) {
dir := t.TempDir()
serverfileDir := filepath.Join(dir, "server")
modsDir := filepath.Join(dir, "mods")
cfgDir := filepath.Join(dir, "cfg")
profilesDir := filepath.Join(dir, "profiles")
os.MkdirAll(serverfileDir, 0755)
// Copy stub into serverfileDir so exec finds it
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(cfgDir)
streamer := NewLogStreamer()
pm := NewProcessManager(serverfileDir, modsDir, cfgDir, profilesDir, sm, mm, cm, streamer)
s, _ := sm.Load()
s.ServerParameters = "-server -world=empty"
sm.Save(s)
if err := pm.Start(); err != nil {
t.Fatalf("Start() error = %v", err)
}
if !pm.IsRunning() {
t.Fatal("IsRunning() should be true after Start()")
}
// Second Start should fail (already running)
if err := pm.Start(); err == nil {
t.Error("second Start() should fail")
}
if err := pm.Stop(); err != nil {
t.Fatalf("Stop() error = %v", err)
}
// Give goroutine time to update state
time.Sleep(100 * time.Millisecond)
if pm.IsRunning() {
t.Error("IsRunning() should be false after Stop()")
}
// Second Stop should fail
if err := pm.Stop(); err == nil {
t.Error("second Stop() should fail")
}
}
func TestStartWithPercentCommand(t *testing.T) {
dir := t.TempDir()
serverfileDir := filepath.Join(dir, "server")
modsDir := filepath.Join(dir, "mods")
cfgDir := filepath.Join(dir, "cfg")
profilesDir := filepath.Join(dir, "profiles")
os.MkdirAll(serverfileDir, 0755)
// Copy stub
stubSrc, _ := os.ReadFile("testdata/arma3server_x64")
stubPath := filepath.Join(serverfileDir, "arma3server_x64")
os.WriteFile(stubPath, stubSrc, 0755)
sm := NewSettingsManager(dir)
mm := NewModlistManager(filepath.Join(dir, "modlists"))
cm := NewConfigManager(cfgDir)
streamer := NewLogStreamer()
pm := NewProcessManager(serverfileDir, modsDir, cfgDir, profilesDir, sm, mm, cm, streamer)
s, _ := sm.Load()
s.ServerParameters = "%command% -world=empty"
sm.Save(s)
if err := pm.Start(); err != nil {
t.Fatalf("Start() with %%command%% error = %v", err)
}
if !pm.IsRunning() {
t.Fatal("IsRunning() should be true")
}
pm.Stop()
time.Sleep(100 * time.Millisecond)
}
func TestStartWithEmptyCommand(t *testing.T) {
dir := t.TempDir()
serverfileDir := filepath.Join(dir, "server")
os.MkdirAll(serverfileDir, 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)
s, _ := sm.Load()
s.ServerParameters = ""
sm.Save(s)
// Empty params without %command% should still work (starts with just the binary)
// But if we use %command% with nothing after it, it should fail
s.ServerParameters = "%command%"
sm.Save(s)
err := pm.Start()
if err == nil {
t.Error("Start() with percent-command-percent only (no other args) should fail")
pm.Stop()
}
}
func TestStartWithServerBinaryEnv(t *testing.T) {
dir := t.TempDir()
serverfileDir := filepath.Join(dir, "server")
os.MkdirAll(serverfileDir, 0755)
stubSrc, _ := os.ReadFile("testdata/arma3server_x64")
customBin := "my-custom-server"
os.WriteFile(filepath.Join(serverfileDir, customBin), 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)
s, _ := sm.Load()
s.ServerParameters = "-server -world=empty"
sm.Save(s)
t.Setenv("SERVER_BINARY", customBin)
if err := pm.Start(); err != nil {
t.Fatalf("Start() with SERVER_BINARY env error = %v", err)
}
if !pm.IsRunning() {
t.Fatal("IsRunning() should be true")
}
pm.Stop()
time.Sleep(100 * time.Millisecond)
}
func TestStartWithServerParamsEnv(t *testing.T) {
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)
s, _ := sm.Load()
s.ServerParameters = "-server -world=empty"
sm.Save(s)
t.Setenv("SERVER_PARAMS", "%command% -custom-param")
if err := pm.Start(); err != nil {
t.Fatalf("Start() with SERVER_PARAMS env error = %v", err)
}
if !pm.IsRunning() {
t.Fatal("IsRunning() should be true")
}
pm.Stop()
time.Sleep(100 * time.Millisecond)
}
func TestServerParamsEnvOverridesSettings(t *testing.T) {
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)
s, _ := sm.Load()
s.ServerParameters = "-server -world=empty"
sm.Save(s)
// Without SERVER_PARAMS override, Start() would use settings value (no %command%).
// With override, it uses %command% substitution.
t.Setenv("SERVER_PARAMS", "%command% -test-override")
if err := pm.Start(); err != nil {
t.Fatalf("Start() error = %v (SERVER_PARAMS override not applied?)", err)
}
pm.Stop()
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)")
}
}
func TestStart_StubWritesRPTLog(t *testing.T) {
dir := t.TempDir()
serverfileDir := filepath.Join(dir, "server")
profilesDir := filepath.Join(dir, "profiles")
modsDir := filepath.Join(dir, "mods")
cfgDir := filepath.Join(dir, "cfg")
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(cfgDir)
streamer := NewLogStreamer()
pm := NewProcessManager(serverfileDir, modsDir, cfgDir, profilesDir, sm, mm, cm, streamer)
s, _ := sm.Load()
s.ServerParameters = "-t 7"
sm.Save(s)
if err := pm.Start(); err != nil {
t.Fatalf("Start() error = %v", err)
}
// Wait for the stub to write RPT entries (heartbeat at ~5s)
deadline := time.Now().Add(8 * time.Second)
var rptPath string
for time.Now().Before(deadline) {
matches, _ := filepath.Glob(filepath.Join(profilesDir, "arma3server_x64_*.rpt"))
if len(matches) > 0 {
rptPath = matches[0]
break
}
time.Sleep(200 * time.Millisecond)
}
if rptPath == "" {
pm.Stop()
t.Fatal("RPT file not found in profiles dir")
}
// Wait for heartbeat entries to be written (heartbeat at ~5s)
deadline2 := time.Now().Add(6 * time.Second)
for time.Now().Before(deadline2) {
data, _ := os.ReadFile(rptPath)
if strings.Contains(string(data), "MissionEditor") {
break
}
time.Sleep(500 * time.Millisecond)
}
data, err := os.ReadFile(rptPath)
if err != nil {
pm.Stop()
t.Fatalf("ReadFile(%s) error = %v", rptPath, err)
}
content := string(data)
if !strings.Contains(content, "RPT log started") {
t.Errorf("RPT missing header, got:\n%s", content)
}
if !strings.Contains(content, "MissionEditor") {
t.Errorf("RPT missing MissionEditor entry, got:\n%s", content)
}
if !strings.Contains(content, "Server: Player") {
t.Errorf("RPT missing Server: Player entry, got:\n%s", content)
}
pm.Stop()
time.Sleep(100 * time.Millisecond)
}