Files
CosmicClash/Game/tests/cases/test_server_config.gd
T
Josh Creek 06881f05ca feat(server): task 6.1/6.3 — dedicated server export preset and a real CLI surface
6.1: "Linux Dedicated Server" preset (dedicated_server=true,
custom_features="dedicated_server") mirroring the existing training
preset, plus run/main_scene.dedicated_server so the server binary reaches
its own entry point with no flag. Builds: an 85MB Linux x86_64 binary,
gitignored like the training one.

6.3: scripts/server_config.gd declares every server flag once - name,
type, default, section, help - and one parser turns that into parsing,
type checking, range validation, config-file backing and --help. The
flags had grown to ~30 across server_boot.gd and networked_match.gd, each
parsed inline with begins_with, none documented, and an unrecognised flag
was SILENTLY IGNORED: --max-clientss=8 ran a server on the default cap
and said nothing. Unknown flags, missing values, wrong types, duplicates
and out-of-range values are now hard errors, reported all at once.

Precedence is command line > config file > default. server_boot.gd parses
strictly because it owns the whole command line; networked_match.gd reads
the same declaration leniently because it is one consumer of an argv the
smoke harnesses also fill with --role= and --drive-seconds=. Nothing is
lost - every server flag is declared, so the strict pass already caught
any typo before the match scene re-reads its own.

13 unit tests covering the precedence order, the typo rejection that
motivated this, --no-<bool> not double-listing in --help, and --help
documenting every flag asserted against the declaration rather than a
hand-kept list. Verified end to end: --help prints, a typo'd flag refuses
to start, and the plain/replay-log/late-joiner smoke scenarios still pass.
2026-08-21 17:03:08 +01:00

128 lines
5.8 KiB
GDScript

extends "res://tests/test_case.gd"
# Task 6.3. The behaviour that matters is not "it parses a port" — it is the
# precedence order an operator relies on, and the refusal to run on a typo.
const ServerConfigScript = preload("res://scripts/server_config.gd")
func _parse(args: Array) -> Variant:
var argv := PackedStringArray()
for a in args:
argv.append(a)
return ServerConfigScript.parse(argv)
func _temp_config(body: String) -> String:
var path := "user://test_server_%d.cfg" % Time.get_ticks_usec()
var f := FileAccess.open(path, FileAccess.WRITE)
f.store_string(body)
f.close()
return path
func test_defaults_apply_when_nothing_is_given() -> void:
var config = _parse([])
assert_true(config.is_valid(), "an empty command line is valid")
assert_eq(config.get_value("port"), 7777, "default port")
assert_eq(config.get_value("max-matches"), 0, "0 means run forever")
assert_eq(config.get_value("log-level"), "info", "default log level")
func test_command_line_values_are_typed_not_strings() -> void:
var config = _parse(["--port=7000", "--match-length=90.5", "--fill-bots"])
assert_true(config.is_valid(), "valid: %s" % str(config.errors))
assert_eq(config.get_value("port"), 7000, "int stays an int")
assert_almost_eq(config.get_value("match-length"), 90.5, 0.001, "float stays a float")
assert_eq(config.get_value("fill-bots"), true, "a bare bool flag is true")
func test_an_unknown_flag_is_an_error_not_a_shrug() -> void:
# The whole reason this class exists: `--max-clientss=8` used to run a
# server on the default cap and say nothing at all.
var config = _parse(["--max-clientss=8"])
assert_true(not config.is_valid(), "a typo'd flag is rejected")
assert_true("max-clientss" in " ".join(config.errors), "and the error names it: %s" % str(config.errors))
func test_a_value_that_is_not_the_declared_type_is_rejected() -> void:
var config = _parse(["--port=seven"])
assert_true(not config.is_valid(), "a non-numeric port is rejected")
var config_ok = _parse(["--port=7000"])
assert_true(config_ok.is_valid(), "control: a numeric port is accepted")
func test_a_non_bool_flag_without_a_value_is_rejected() -> void:
# Silently defaulting here would hide a shell-quoting mistake.
var config = _parse(["--port"])
assert_true(not config.is_valid(), "--port with no value is an error")
func test_no_prefix_turns_a_bool_off() -> void:
var config = _parse(["--no-fill-bots"])
assert_true(config.is_valid(), "valid: %s" % str(config.errors))
assert_eq(config.get_value("fill-bots"), false, "--no- inverts it")
# And it must not be listed separately, or --help doubles in length.
var help: String = ServerConfigScript.help_text()
assert_eq(help.count("--no-fill-bots"), 1, "--no- form appears once, in the default hint")
func test_the_command_line_beats_the_config_file() -> void:
var path := _temp_config("[server]\nport=8100\nmax-clients=4\n")
var config = _parse(["--config=%s" % path, "--port=9200"])
assert_true(config.is_valid(), "valid: %s" % str(config.errors))
assert_eq(config.get_value("port"), 9200, "the command line wins")
assert_eq(config.get_value("max-clients"), 4, "the file still supplies what the command line omits")
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
func test_the_config_file_beats_the_default() -> void:
var path := _temp_config("[server]\nport=8100\n")
var config = _parse(["--config=%s" % path])
assert_true(config.is_valid(), "valid: %s" % str(config.errors))
assert_eq(config.get_value("port"), 8100, "the file overrides the default")
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
func test_a_missing_or_malformed_config_file_is_an_error() -> void:
var config = _parse(["--config=user://definitely_not_here_%d.cfg" % Time.get_ticks_usec()])
assert_true(not config.is_valid(), "a config file that cannot be read is an error, not silence")
var path := _temp_config("[server]\nnonsense=1\n")
var unknown_key = _parse(["--config=%s" % path])
assert_true(not unknown_key.is_valid(), "an unknown key in the file is rejected too")
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
func test_out_of_range_values_are_rejected_with_their_own_message() -> void:
assert_true(not _parse(["--port=0"]).is_valid(), "port 0 is out of range")
assert_true(not _parse(["--port=70000"]).is_valid(), "port 70000 is out of range")
assert_true(not _parse(["--max-clients=0"]).is_valid(), "a server for nobody is rejected")
assert_true(not _parse(["--match-length=0"]).is_valid(), "a zero-length match is rejected")
assert_true(not _parse(["--log-level=chatty"]).is_valid(), "an undefined log level is rejected")
assert_true(not _parse(["--arena-rotation=spiral"]).is_valid(), "an undefined rotation mode is rejected")
# Control: the same flags at legal values all pass together.
var ok = _parse(["--port=7000", "--max-clients=6", "--match-length=90", "--log-level=warn", "--arena-rotation=random"])
assert_true(ok.is_valid(), "control: legal values pass (%s)" % str(ok.errors))
func test_a_repeated_flag_is_rejected_rather_than_last_wins() -> void:
# Last-wins hides a duplicated line in a generated systemd unit.
var config = _parse(["--port=7000", "--port=8000"])
assert_true(not config.is_valid(), "the same flag twice is an error")
func test_help_documents_every_declared_flag() -> void:
# The acceptance criterion is literally "--help documents every flag", so
# assert it against the declaration rather than against a hand-kept list.
var help: String = ServerConfigScript.help_text()
for spec in ServerConfigScript.specs():
assert_true("--%s" % spec.key in help, "--%s appears in --help" % spec.key)
assert_true(spec.help in help, "and so does its description")
func test_help_is_requested_without_needing_a_valid_command_line() -> void:
var config = _parse(["--help"])
assert_true(config.help_requested, "--help is recognised")
var short = _parse(["-h"])
assert_true(short.help_requested, "-h too")