nixos/armagetronad: add module with tests
This commit is contained in:
parent
533bdea34f
commit
1dc5eb13b0
4 changed files with 477 additions and 0 deletions
|
@ -509,6 +509,7 @@
|
|||
./services/editors/infinoted.nix
|
||||
./services/finance/odoo.nix
|
||||
./services/games/archisteamfarm.nix
|
||||
./services/games/armagetronad.nix
|
||||
./services/games/crossfire-server.nix
|
||||
./services/games/deliantra-server.nix
|
||||
./services/games/factorio.nix
|
||||
|
|
221
nixos/modules/services/games/armagetronad.nix
Normal file
221
nixos/modules/services/games/armagetronad.nix
Normal file
|
@ -0,0 +1,221 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
inherit (lib) mkEnableOption mkIf mkOption mkMerge literalExpression;
|
||||
inherit (lib) mapAttrsToList filterAttrs unique recursiveUpdate types;
|
||||
|
||||
mkValueStringArmagetron = with lib; v:
|
||||
if isInt v then toString v
|
||||
else if isFloat v then toString v
|
||||
else if isString v then v
|
||||
else if true == v then "1"
|
||||
else if false == v then "0"
|
||||
else if null == v then ""
|
||||
else throw "unsupported type: ${builtins.typeOf v}: ${(lib.generators.toPretty {} v)}";
|
||||
|
||||
settingsFormat = pkgs.formats.keyValue {
|
||||
mkKeyValue = lib.generators.mkKeyValueDefault
|
||||
{
|
||||
mkValueString = mkValueStringArmagetron;
|
||||
} " ";
|
||||
listsAsDuplicateKeys = true;
|
||||
};
|
||||
|
||||
cfg = config.services.armagetronad;
|
||||
enabledServers = lib.filterAttrs (n: v: v.enable) cfg.servers;
|
||||
nameToId = serverName: "armagetronad-${serverName}";
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.armagetronad = {
|
||||
servers = mkOption {
|
||||
description = lib.mdDoc "Armagetron server definitions.";
|
||||
default = { };
|
||||
type = types.attrsOf (types.submodule {
|
||||
options = {
|
||||
enable = mkEnableOption (lib.mdDoc "armagetronad");
|
||||
package = lib.mkPackageOptionMD pkgs "armagetronad-dedicated" {
|
||||
example = ''
|
||||
pkgs.armagetronad."0.2.9-sty+ct+ap".dedicated
|
||||
'';
|
||||
extraDescription = ''
|
||||
Ensure that you use a derivation whose evaluation contains the path `bin/armagetronad-dedicated`.
|
||||
'';
|
||||
};
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "0.0.0.0";
|
||||
description = lib.mdDoc "Host to listen on. Used for SERVER_IP.";
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 4534;
|
||||
description = lib.mdDoc "Port to listen on. Used for SERVER_PORT.";
|
||||
};
|
||||
dns = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = lib.mdDoc "DNS address to use for this server. Optional.";
|
||||
};
|
||||
openFirewall = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = lib.mdDoc "Set to true to open a UDP port for Armagetron Advanced.";
|
||||
};
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
description = "The name of this server.";
|
||||
};
|
||||
settings = mkOption {
|
||||
type = settingsFormat.type;
|
||||
default = { };
|
||||
description = lib.mdDoc ''
|
||||
Armagetron Advanced server rules configuration. Refer to:
|
||||
<https://wiki.armagetronad.org/index.php?title=Console_Commands>
|
||||
or `armagetronad-dedicated --doc` for a list.
|
||||
|
||||
This attrset is used to populate `settings_custom.cfg`; see:
|
||||
<https://wiki.armagetronad.org/index.php/Configuration_Files>
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
CYCLE_RUBBER = 40;
|
||||
}
|
||||
'';
|
||||
};
|
||||
roundSettings = mkOption {
|
||||
type = settingsFormat.type;
|
||||
default = { };
|
||||
description = lib.mdDoc ''
|
||||
Armagetron Advanced server per-round configuration. Refer to:
|
||||
<https://wiki.armagetronad.org/index.php?title=Console_Commands>
|
||||
or `armagetronad-dedicated --doc` for a list.
|
||||
|
||||
This attrset is used to populate `everytime.cfg`; see:
|
||||
<https://wiki.armagetronad.org/index.php/Configuration_Files>
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
SAY = [
|
||||
"Hosted on NixOS"
|
||||
"https://nixos.org"
|
||||
"iD Tech High Rubber rul3z!! Happy New Year 2008!!1"
|
||||
];
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf (enabledServers != { }) {
|
||||
systemd.services = mkMerge (mapAttrsToList
|
||||
(serverName: serverCfg:
|
||||
let
|
||||
serverId = nameToId serverName;
|
||||
serverInfo = (
|
||||
{
|
||||
SERVER_IP = serverCfg.host;
|
||||
SERVER_PORT = serverCfg.port;
|
||||
SERVER_NAME = serverCfg.name;
|
||||
} // (
|
||||
if serverCfg.dns != null then { SERVER_DNS = serverCfg.dns; }
|
||||
else { }
|
||||
)
|
||||
);
|
||||
customSettings = serverCfg.settings;
|
||||
everytimeSettings = serverCfg.roundSettings;
|
||||
|
||||
serverInfoCfg = settingsFormat.generate "server_info.${serverName}.cfg" serverInfo;
|
||||
customSettingsCfg = settingsFormat.generate "settings_custom.${serverName}.cfg" customSettings;
|
||||
everytimeSettingsCfg = settingsFormat.generate "everytime.${serverName}.cfg" everytimeSettings;
|
||||
in
|
||||
{
|
||||
"armagetronad@${serverName}" = {
|
||||
description = "Armagetron Advanced Dedicated Server for ${serverName}";
|
||||
wants = [ "basic.target" ];
|
||||
after = [ "basic.target" "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig =
|
||||
let
|
||||
stateDirectory = "armagetronad/${serverName}";
|
||||
serverRoot = "/var/lib/${stateDirectory}";
|
||||
preStart = pkgs.writeShellScript "armagetronad-${serverName}-prestart.sh" ''
|
||||
owner="${serverId}:${serverId}"
|
||||
|
||||
# Create the config directories.
|
||||
for dirname in data settings var resource; do
|
||||
dir="${serverRoot}/$dirname"
|
||||
mkdir -p "$dir"
|
||||
chmod u+rwx,g+rx,o-rwx "$dir"
|
||||
chown "$owner" "$dir"
|
||||
done
|
||||
|
||||
# Link in the config files if present and non-trivial.
|
||||
ln -sf ${serverInfoCfg} "${serverRoot}/settings/server_info.cfg"
|
||||
ln -sf ${customSettingsCfg} "${serverRoot}/settings/settings_custom.cfg"
|
||||
ln -sf ${everytimeSettingsCfg} "${serverRoot}/settings/everytime.cfg"
|
||||
|
||||
# Create an input file for sending commands to the server.
|
||||
input="${serverRoot}/input"
|
||||
truncate -s0 "$input"
|
||||
chmod u+rw,g+r,o-rwx "$input"
|
||||
chown "$owner" "$input"
|
||||
'';
|
||||
in
|
||||
{
|
||||
Type = "simple";
|
||||
StateDirectory = stateDirectory;
|
||||
ExecStartPre = preStart;
|
||||
ExecStart = "${serverCfg.package}/bin/armagetronad-dedicated --daemon --input ${serverRoot}/input --userdatadir ${serverRoot}/data --userconfigdir ${serverRoot}/settings --vardir ${serverRoot}/var --autoresourcedir ${serverRoot}/resource";
|
||||
Restart = "on-failure";
|
||||
CapabilityBoundingSet = "";
|
||||
LockPersonality = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
RestrictNamespaces = true;
|
||||
RestrictSUIDSGID = true;
|
||||
User = serverId;
|
||||
Group = serverId;
|
||||
};
|
||||
};
|
||||
})
|
||||
enabledServers
|
||||
);
|
||||
|
||||
networking.firewall.allowedUDPPorts =
|
||||
unique (mapAttrsToList (serverName: serverCfg: serverCfg.port) (filterAttrs (serverName: serverCfg: serverCfg.openFirewall) enabledServers));
|
||||
|
||||
users.users = mkMerge (mapAttrsToList
|
||||
(serverName: serverCfg:
|
||||
{
|
||||
${nameToId serverName} = {
|
||||
group = nameToId serverName;
|
||||
description = "Armagetron Advanced dedicated user for server ${serverName}";
|
||||
isSystemUser = true;
|
||||
};
|
||||
})
|
||||
enabledServers
|
||||
);
|
||||
|
||||
users.groups = mkMerge (mapAttrsToList
|
||||
(serverName: serverCfg:
|
||||
{
|
||||
${nameToId serverName} = { };
|
||||
})
|
||||
enabledServers
|
||||
);
|
||||
};
|
||||
}
|
|
@ -128,6 +128,7 @@ in {
|
|||
appliance-repart-image = runTest ./appliance-repart-image.nix;
|
||||
apparmor = handleTest ./apparmor.nix {};
|
||||
archi = handleTest ./archi.nix {};
|
||||
armagetronad = handleTest ./armagetronad.nix {};
|
||||
atd = handleTest ./atd.nix {};
|
||||
atop = handleTest ./atop.nix {};
|
||||
atuin = handleTest ./atuin.nix {};
|
||||
|
|
254
nixos/tests/armagetronad.nix
Normal file
254
nixos/tests/armagetronad.nix
Normal file
|
@ -0,0 +1,254 @@
|
|||
import ./make-test-python.nix ({ pkgs, ...} :
|
||||
|
||||
let
|
||||
user = "alice";
|
||||
|
||||
client =
|
||||
{ pkgs, ... }:
|
||||
|
||||
{ imports = [ ./common/user-account.nix ./common/x11.nix ];
|
||||
hardware.opengl.driSupport = true;
|
||||
virtualisation.memorySize = 256;
|
||||
environment = {
|
||||
systemPackages = [ pkgs.armagetronad ];
|
||||
variables.XAUTHORITY = "/home/${user}/.Xauthority";
|
||||
};
|
||||
test-support.displayManager.auto.user = user;
|
||||
};
|
||||
|
||||
in {
|
||||
name = "armagetronad";
|
||||
meta = with pkgs.lib.maintainers; {
|
||||
maintainers = [ numinit ];
|
||||
};
|
||||
|
||||
enableOCR = true;
|
||||
|
||||
nodes =
|
||||
{
|
||||
server = {
|
||||
services.armagetronad.servers = {
|
||||
high-rubber = {
|
||||
enable = true;
|
||||
name = "Smoke Test High Rubber Server";
|
||||
port = 4534;
|
||||
settings = {
|
||||
SERVER_OPTIONS = "High Rubber server made to run smoke tests.";
|
||||
CYCLE_RUBBER = 40;
|
||||
SIZE_FACTOR = 0.5;
|
||||
};
|
||||
roundSettings = {
|
||||
SAY = [
|
||||
"NixOS Smoke Test Server"
|
||||
"https://nixos.org"
|
||||
];
|
||||
};
|
||||
};
|
||||
sty = {
|
||||
enable = true;
|
||||
name = "Smoke Test sty+ct+ap Server";
|
||||
package = pkgs.armagetronad."0.2.9-sty+ct+ap".dedicated;
|
||||
port = 4535;
|
||||
settings = {
|
||||
SERVER_OPTIONS = "sty+ct+ap server made to run smoke tests.";
|
||||
CYCLE_RUBBER = 20;
|
||||
SIZE_FACTOR = 0.5;
|
||||
};
|
||||
roundSettings = {
|
||||
SAY = [
|
||||
"NixOS Smoke Test sty+ct+ap Server"
|
||||
"https://nixos.org"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
client1 = client;
|
||||
client2 = client;
|
||||
};
|
||||
|
||||
testScript = let
|
||||
xdo = name: text: let
|
||||
xdoScript = pkgs.writeText "${name}.xdo" text;
|
||||
in "${pkgs.xdotool}/bin/xdotool ${xdoScript}";
|
||||
in
|
||||
''
|
||||
import shlex
|
||||
import threading
|
||||
from collections import namedtuple
|
||||
|
||||
class Client(namedtuple('Client', ('node', 'name'))):
|
||||
def send(self, *keys):
|
||||
for key in keys:
|
||||
self.node.send_key(key)
|
||||
|
||||
def send_on(self, text, *keys):
|
||||
self.node.wait_for_text(text)
|
||||
self.send(*keys)
|
||||
|
||||
Server = namedtuple('Server', ('node', 'name', 'address', 'port', 'welcome', 'attacker', 'victim', 'coredump_delay'))
|
||||
|
||||
# Clients and their in-game names
|
||||
clients = (
|
||||
Client(client1, 'Arduino'),
|
||||
Client(client2, 'SmOoThIcE')
|
||||
)
|
||||
|
||||
# Server configs.
|
||||
servers = (
|
||||
Server(server, 'high-rubber', 'server', 4534, 'NixOS Smoke Test Server', 'SmOoThIcE', 'Arduino', 8),
|
||||
Server(server, 'sty', 'server', 4535, 'NixOS Smoke Test sty+ct+ap Server', 'Arduino', 'SmOoThIcE', 8)
|
||||
)
|
||||
|
||||
"""
|
||||
Runs a command as the client user.
|
||||
"""
|
||||
def run(cmd):
|
||||
return "su - ${user} -c " + shlex.quote(cmd)
|
||||
|
||||
screenshot_idx = 1
|
||||
|
||||
"""
|
||||
Takes screenshots on all clients.
|
||||
"""
|
||||
def take_screenshots(screenshot_idx):
|
||||
for client in clients:
|
||||
client.node.screenshot(f"screen_{client.name}_{screenshot_idx}")
|
||||
return screenshot_idx + 1
|
||||
|
||||
# Wait for the servers to come up.
|
||||
start_all()
|
||||
for srv in servers:
|
||||
srv.node.wait_for_unit(f"armagetronad@{srv.name}")
|
||||
srv.node.wait_until_succeeds(f"ss --numeric --udp --listening | grep -q {srv.port}")
|
||||
|
||||
# Make sure console commands work through the named pipe we created.
|
||||
for srv in servers:
|
||||
srv.node.succeed(
|
||||
f"echo 'say Testing!' >> /var/lib/armagetronad/{srv.name}/input"
|
||||
)
|
||||
srv.node.succeed(
|
||||
f"echo 'say Testing again!' >> /var/lib/armagetronad/{srv.name}/input"
|
||||
)
|
||||
srv.node.wait_until_succeeds(
|
||||
f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: Testing!'"
|
||||
)
|
||||
srv.node.wait_until_succeeds(
|
||||
f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: Testing again!'"
|
||||
)
|
||||
|
||||
"""
|
||||
Sets up a client, waiting for the given barrier on completion.
|
||||
"""
|
||||
def client_setup(client, servers, barrier):
|
||||
client.node.wait_for_x()
|
||||
|
||||
# Configure Armagetron.
|
||||
client.node.succeed(
|
||||
run("mkdir -p ~/.armagetronad/var"),
|
||||
run(f"echo 'PLAYER_1 {client.name}' >> ~/.armagetronad/var/autoexec.cfg")
|
||||
)
|
||||
for idx, srv in enumerate(servers):
|
||||
client.node.succeed(
|
||||
run(f"echo 'BOOKMARK_{idx+1}_ADDRESS {srv.address}' >> ~/.armagetronad/var/autoexec.cfg"),
|
||||
run(f"echo 'BOOKMARK_{idx+1}_NAME {srv.name}' >> ~/.armagetronad/var/autoexec.cfg"),
|
||||
run(f"echo 'BOOKMARK_{idx+1}_PORT {srv.port}' >> ~/.armagetronad/var/autoexec.cfg")
|
||||
)
|
||||
|
||||
# Start Armagetron.
|
||||
client.node.succeed(run("ulimit -c unlimited; armagetronad >&2 & disown"))
|
||||
client.node.wait_until_succeeds(
|
||||
run(
|
||||
"${xdo "create_new_win-select_main_window" ''
|
||||
search --onlyvisible --name "Armagetron Advanced"
|
||||
windowfocus --sync
|
||||
windowactivate --sync
|
||||
''}"
|
||||
)
|
||||
)
|
||||
|
||||
# Get through the tutorial.
|
||||
client.send_on('Language Settings', 'ret')
|
||||
client.send_on('First Setup', 'ret')
|
||||
client.send_on('Welcome to Armagetron Advanced', 'ret')
|
||||
client.send_on('round 1', 'esc')
|
||||
client.send_on('Menu', 'up', 'up', 'ret')
|
||||
client.send_on('We hope you', 'ret')
|
||||
client.send_on('Armagetron Advanced', 'ret')
|
||||
client.send_on('Play Game', 'ret')
|
||||
|
||||
# Online > LAN > Network Setup > Mates > Server Bookmarks
|
||||
client.send_on('Multiplayer', 'down', 'down', 'down', 'down', 'ret')
|
||||
|
||||
barrier.wait()
|
||||
|
||||
# Get to the Server Bookmarks screen on both clients. This takes a while so do it asynchronously.
|
||||
barrier = threading.Barrier(3, timeout=120)
|
||||
for client in clients:
|
||||
threading.Thread(target=client_setup, args=(client, servers, barrier)).start()
|
||||
barrier.wait()
|
||||
|
||||
# Main testing loop. Iterates through each server bookmark and connects to them in sequence.
|
||||
# Assumes that the game is currently on the Server Bookmarks screen.
|
||||
for srv in servers:
|
||||
screenshot_idx = take_screenshots(screenshot_idx)
|
||||
|
||||
# Connect both clients at once, one second apart.
|
||||
for client in clients:
|
||||
client.send('ret')
|
||||
client.node.sleep(1)
|
||||
|
||||
# Wait for clients to connect
|
||||
for client in clients:
|
||||
srv.node.wait_until_succeeds(
|
||||
f"journalctl -u armagetronad@{srv.name} -e | grep -q '{client.name}.*entered the game'"
|
||||
)
|
||||
|
||||
# Wait for the match to start
|
||||
srv.node.wait_until_succeeds(
|
||||
f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: {srv.welcome}'"
|
||||
)
|
||||
srv.node.wait_until_succeeds(
|
||||
f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: https://nixos.org'"
|
||||
)
|
||||
srv.node.wait_until_succeeds(
|
||||
f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Go (round 1 of 10)'"
|
||||
)
|
||||
|
||||
# Wait a bit
|
||||
srv.node.sleep(srv.coredump_delay)
|
||||
|
||||
# Turn the attacker player's lightcycle left
|
||||
attacker = next(client for client in clients if client.name == srv.attacker)
|
||||
victim = next(client for client in clients if client.name == srv.victim)
|
||||
attacker.send('left')
|
||||
screenshot_idx = take_screenshots(screenshot_idx)
|
||||
|
||||
# Wait for coredump.
|
||||
srv.node.wait_until_succeeds(
|
||||
f"journalctl -u armagetronad@{srv.name} -e | grep -q '{attacker.name} core dumped {victim.name}'"
|
||||
)
|
||||
screenshot_idx = take_screenshots(screenshot_idx)
|
||||
|
||||
# Disconnect both clients from the server
|
||||
for client in clients:
|
||||
client.send('esc')
|
||||
client.send_on('Menu', 'up', 'up', 'ret')
|
||||
srv.node.wait_until_succeeds(
|
||||
f"journalctl -u armagetronad@{srv.name} -e | grep -q '{client.name}.*left the game'"
|
||||
)
|
||||
|
||||
# Next server.
|
||||
for client in clients:
|
||||
client.send_on('Server Bookmarks', 'down')
|
||||
|
||||
# Stop the servers
|
||||
for srv in servers:
|
||||
srv.node.succeed(
|
||||
f"systemctl stop armagetronad@{srv.name}"
|
||||
)
|
||||
srv.node.wait_until_fails(f"ss --numeric --udp --listening | grep -q {srv.port}")
|
||||
'';
|
||||
|
||||
})
|
Loading…
Reference in a new issue