mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-28 16:00:38 +01:00
99 lines
6.0 KiB
Python
99 lines
6.0 KiB
Python
"""The words of a dedicated server's commands (port/linux/game/server_commands.c): the tokenizer (spaces, quotes, no
|
|
escapes, at most MAXIMUM_WORDS), the text after some words, a reason kept printable, ban durations, numbers in range,
|
|
bans.txt's fields, and a player found by slot or the start of their name, with the name as server_status.c reads it
|
|
and checks it against the web page's; and how long each role may ban for (a moderator a day at most)."""
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
from harness import CHECK_FAILED, build, constant, enum_with, function, mutated, read, run # noqa: E402
|
|
|
|
CASES = ["split-words", "rest-after-words", "printable-text", "durations", "integers", "ban-fields", "find-player",
|
|
"player-matches", "ban-limits"]
|
|
|
|
SOURCES = {
|
|
"source/game/players.c": ["player_name_character_ascii"],
|
|
"port/linux/game/server_status.c": ["server_status_player_name", "server_status_player_matches"],
|
|
"port/linux/game/server_commands.c": ["split_words", "rest_after_words", "printable_text", "parse_duration",
|
|
"parse_integer", "name_begins_with", "find_player", "ban_field",
|
|
"ban_duration_allowed"],
|
|
}
|
|
|
|
# a fault in the code, the function it is in, and the case that must catch it
|
|
NEGATIVE_CONTROLS = {
|
|
"quotes-split": (("if (*cursor == '\"')", "if (0)"), "split_words", "split-words"),
|
|
"words-unbounded": (("while (*cursor && count < maximum)", "while (*cursor)"), "split_words", "split-words"),
|
|
"tabs-in-words": (("while (*cursor == ' ' || *cursor == '\\t')", "while (*cursor == ' ')"), "split_words",
|
|
"split-words"),
|
|
"rest-ignores-quotes": (("if (*line == '\"')", "if (0)"), "rest_after_words", "rest-after-words"),
|
|
"quotes-kept": (("if (last > first && source[first] == '\"' && source[last] == '\"')", "if (0)"),
|
|
"printable_text", "printable-text"),
|
|
"control-characters-kept": (("if ((unsigned char)source[index] >= 0x20 && (unsigned char)source[index] < 0x7F)",
|
|
"if (1)"),
|
|
"printable_text", "printable-text"),
|
|
"room-overrun": (("length < size - 1", "length < size"), "printable_text", "printable-text"),
|
|
"hours-as-minutes": (("case 'h': unit = 60 * 60; break;", "case 'h': unit = 60; break;"), "parse_duration",
|
|
"durations"),
|
|
"suffix-unchecked": (("if (*end && end[1])\n\t\treturn -1;", ""), "parse_duration", "durations"),
|
|
"no-ceiling": (("if ((double)number * unit > 10.0 * 365 * 24 * 60 * 60)", "if (0)"), "parse_duration",
|
|
"durations"),
|
|
"forever-unknown": (('!csstrcasecmp(text, "forever") || ', ""), "parse_duration", "durations"),
|
|
"range-unchecked": (("if (*end || number < minimum || number > maximum)", "if (*end)"), "parse_integer",
|
|
"integers"),
|
|
"fields-by-their-end": (('snprintf(key, sizeof(key), "\\t%s=", name);', 'snprintf(key, sizeof(key), "%s=", name);'),
|
|
"ban_field", "ban-fields"),
|
|
"field-runs-on": (('length = (long)strcspn(at, "\\t");', "length = (long)strlen(at);"), "ban_field",
|
|
"ban-fields"),
|
|
"exact-name-not-first": (("if (exact_count == 1)", "if (exact_count == 1 && 0)"), "find_player", "find-player"),
|
|
"ambiguous-taken": (("if (matches > 1)", "if (0)"), "find_player", "find-player"),
|
|
"slots-unchecked": (("|| !network_player_is_valid(&game->players[number])", ""), "find_player", "find-player"),
|
|
"names-by-case": (("tolower((unsigned char)name[index]) != tolower((unsigned char)text[index])",
|
|
"name[index] != text[index]"), "name_begins_with", "find-player"),
|
|
"matches-any-case": (("return !strcmp(current, name);", "return !strcasecmp(current, name);"),
|
|
"server_status_player_matches", "player-matches"),
|
|
"names-uncut": (("&&\n\t\tindex < size - 1; index++)", "; index++)"), "server_status_player_name",
|
|
"player-matches"),
|
|
"moderator-forever": (("(seconds > 0 && seconds <= MODERATOR_BAN_SECONDS)", "(seconds <= MODERATOR_BAN_SECONDS)"),
|
|
"ban_duration_allowed", "ban-limits"),
|
|
"moderator-unlimited": (("(seconds > 0 && seconds <= MODERATOR_BAN_SECONDS)", "(seconds > 0)"),
|
|
"ban_duration_allowed", "ban-limits"),
|
|
"admin-limited": (("role >= _server_role_admin || ", ""), "ban_duration_allowed", "ban-limits"),
|
|
}
|
|
|
|
|
|
def generated(fault=None, faulty_function=None):
|
|
commands = read("port/linux/game/server_commands.c")
|
|
# (MAXIMUM_SLOTS is halo_port_limits.h's player count, which constant() does not follow into)
|
|
with_limits = commands + read("port/linux/include/halo_port_limits.h")
|
|
config = f"#define MAXIMUM_WORDS {constant(commands, 'MAXIMUM_WORDS')}\n"
|
|
config += f"#define MAXIMUM_SLOTS {constant(with_limits, 'MAXIMUM_SLOTS')}\n"
|
|
# (a product of numbers, which constant() does not take whole)
|
|
config += f"#define MODERATOR_BAN_SECONDS ({re.search(r'MODERATOR_BAN_SECONDS = ([^,]+),', commands).group(1)})\n"
|
|
config += enum_with(read("port/linux/src/server_console.h"), "_server_role_moderator") + "\n"
|
|
text = ""
|
|
for path, names in SOURCES.items():
|
|
source = read(path)
|
|
for name in names:
|
|
code = function(source, name)
|
|
if fault and name == faulty_function:
|
|
code = mutated(code, *fault)
|
|
text += code + "\n"
|
|
return (("config.inc", config), ("under_test.inc", text))
|
|
|
|
|
|
@pytest.mark.parametrize("case", CASES)
|
|
def test_case(case):
|
|
status, output = run(build("server_command_words", generated()), case)
|
|
assert status == 0, output
|
|
|
|
|
|
@pytest.mark.parametrize("control", NEGATIVE_CONTROLS)
|
|
def test_negative_control(control):
|
|
fault, faulty_function, case = NEGATIVE_CONTROLS[control]
|
|
status, output = run(build("server_command_words", generated(fault, faulty_function)), case)
|
|
assert status == CHECK_FAILED, f"the code with a fault ({control}) passed '{case}': the test cannot see it"
|