package main import ( "regexp" "testing" ) func TestParseConfigFormats(t *testing.T) { // An Unreal-style option list list := parseConfig("/server/Game/Saved/Config/LinuxServer/GameSettings.ini", "[/Script/Game.WorldSettings]\n"+ `OptionSettings=(Difficulty=None,ExpRate=1.000000,ServerName="Default Server",CrossplayPlatforms=(Steam,Xbox,PS5,Mac))`) want := map[string]string{"Difficulty": "None", "ExpRate": "1.000000", "ServerName": "Default Server", "CrossplayPlatforms": "(Steam,Xbox,PS5,Mac)"} if len(list) != len(want) { t.Fatalf("option list: got %d settings: %+v", len(list), list) } for _, s := range list { if s.Section != "OptionSettings" || want[s.Key] != s.Value { t.Errorf("option list setting %+v", s) } } // A server.properties file mc := parseConfig("/data/server.properties", "#Server properties\nmax-players=20\nmotd=A Server\nlevel-seed=\n") if len(mc) != 3 || mc[0].Key != "max-players" || mc[0].Value != "20" || mc[2].Value != "" { t.Errorf("properties: %+v", mc) } // INI sections and comments ini := parseConfig("/server/x.ini", "; comment\n[ServerSettings]\nRCONPort = 27020\n[Other]\nKey=\"quoted\"\n") if len(ini) != 2 || ini[0].Section != "ServerSettings" || ini[0].Value != "27020" || ini[1].Value != "quoted" { t.Errorf("ini: %+v", ini) } // JSON, one level deep js := parseConfig("/config/settings.json", `{"name":"x","limits":{"players":8},"tags":["a"]}`) if len(js) != 3 || js[0].Key != "players" || js[0].Section != "limits" || js[0].Value != "8" { t.Errorf("json: %+v", js) } } func TestSuggestPattern(t *testing.T) { for line, mustMatch := range map[string]string{ `[01:32:38 INFO]: Done (24.283s)! For help, type "help"`: `[03:01:02 INFO]: Done (7.5s)! For help, type "help"`, `[2026.10.25-03.10.36:455][ 0]Server has completed startup and is now advertising for join.`: `[2026.10.26-11.00.00:001][ 12]Server has completed startup and is now advertising for join.`, `2026-10-25T03:10:36Z Server started on port 7777`: `2026-10-26T09:00:00Z Server started on port 8000`, } { p := suggestPattern(line) if p == "" { t.Errorf("no pattern for %q", line) continue } re := regexp.MustCompile(p) if !re.MatchString(line) || !re.MatchString(mustMatch) { t.Errorf("pattern %q should match %q and %q", p, line, mustMatch) } } if p := suggestPattern("[12:00:00] ok"); p != "" { t.Errorf("too-short line gave pattern %q", p) } } func TestAnalyzeLines(t *testing.T) { r := &bootReport{} analyzeLines(r, []logLine{ {At: 1, Source: "console", Text: "Starting server"}, {At: 2, Source: "console", Text: "Failed to load optional module"}, {At: 9, Source: "console", Text: "[09:00:01 INFO]: Done (8.1s)! For help, type \"help\""}, {At: 9, Source: "console", Text: "[09:00:01 INFO]: Thread RCON Listener started"}, // After the stop at 200s: the shutdown, as a wrapper script prints it {At: 201, Source: "console", Text: "[06:01:39] [RCON Listener #1/INFO]: Thread RCON Client /0:0:0:0:0:0:0:1 started"}, {At: 202, Source: "console", Text: "2026-10-25T06:01:39.976Z\tINFO\tmc-server-runner\tDone"}, }, []logLine{{At: -1, Source: "/server/x.log", Text: "Listening on 0.0.0.0:7777"}}, 200) if len(r.ErrorLines) != 1 { t.Fatalf("errors %v", r.ErrorLines) } // The last ready line before the stop, not the shutdown's "Done" if r.SuggestedExample != `[09:00:01 INFO]: Done (8.1s)! For help, type "help"` { t.Errorf("example = %q", r.SuggestedExample) } // No console candidate: fall back to the first one in a log file r = &bootReport{} analyzeLines(r, []logLine{{At: 1, Source: "console", Text: "booting"}}, []logLine{{At: -1, Source: "/server/a.log", Text: "Server has completed startup and is now advertising for join."}, {At: -1, Source: "/server/a.log", Text: "Server shutdown done"}}, 100) if r.SuggestedExample != "Server has completed startup and is now advertising for join." { t.Errorf("file fallback example = %q", r.SuggestedExample) } } func TestConfigCandidates(t *testing.T) { for rel, want := range map[string]bool{ "server.properties": true, "Game/Saved/Config/LinuxServer/x.ini": true, "Game/Saved/Logs/Server.log": true, "steamapps/appmanifest_123.acf": false, "Engine/Config/BaseEngine.ini": false, "plugins/SomePlugin/config.yml": false, "world/level.dat": false, } { if got := isConfigCandidate(rel, 1000); got != want { t.Errorf("isConfigCandidate(%q) = %v, want %v", rel, got, want) } } }