mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-25 09:00:35 +01:00
AGENTS.md asks for HALO_PORT_NETWORK_VERSION to be raised with any change to what the machines send each other; nothing checked it, and #95, #99 and and 18 caught up after them). tools/network_format.py takes the wire format from the sources: the game's message numbers, sizes and fields, the distributed netcode's message kinds and flag bits, its DISTRIBUTED_ constants and the structs it sends whole, as hashes. port/linux/network_format.txt records them for the version, and test_linux_port.py fails when the sources differ: an item changed or removed asks for the version to be raised (--record refuses it until then), an item added (a new kind or bit, which older builds may ignore) only to be recorded. It also fails when NETCODE.md says nothing of the version. Checked by recording each merge's parent and checking the merge: #95, #99 and #106 fail as changed structs; #91, votes to kick, voice chat and the integrated light bits as additions; #101, which raised the version, passes once recorded. A comment, or a struct the netcode keeps to itself, changes nothing.
478 lines
23 KiB
Python
478 lines
23 KiB
Python
"""Tests for the native Linux build tooling (port/linux, tools/linux_*.py)."""
|
|
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from tools import linux_build, linux_link_check, linux_msvc_semantics
|
|
|
|
|
|
# ---------- MSVC semantics header
|
|
|
|
|
|
def test_strip_cplusplus_keeps_c_branches_only():
|
|
text = "\n".join([
|
|
"a",
|
|
"#ifdef __cplusplus", "cpp1", "#else", "c1", "#endif",
|
|
"#if FOO", "b", "#else", "c", "#endif",
|
|
"#ifndef __cplusplus", "c2", "#else", "cpp2", "#endif",
|
|
"#if defined(__cplusplus)", "cpp3", "#if X", "cpp4", "#endif", "#endif",
|
|
"#ifdef _WIN64", "win64", "#endif",
|
|
"end",
|
|
])
|
|
assert linux_msvc_semantics.strip_cplusplus(text).split() == ["a", "c1", "b", "c", "c2", "end"]
|
|
|
|
|
|
def write(path: Path, text: str) -> Path:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(text, encoding="latin-1")
|
|
return path
|
|
|
|
|
|
def test_scan_finds_prototype_scope_tags_and_inline_functions(tmp_path):
|
|
header = write(tmp_path / "a.h", """
|
|
struct location;
|
|
void f(struct scenario *s, union color *c);
|
|
/* struct commented_out */
|
|
__inline long prototyped(long x) { return x; }
|
|
__inline long private_helper(long x) { return x; }
|
|
long prototyped(long x);
|
|
#ifdef __cplusplus
|
|
__inline long cpp_only(long x) { return x; }
|
|
#endif
|
|
""")
|
|
files = [header]
|
|
tags = linux_msvc_semantics.scan_tags(files)
|
|
assert tags == {"location": {"struct"}, "scenario": {"struct"}, "color": {"union"}}
|
|
assert linux_msvc_semantics.scan_inline_functions(files, all_inlines=False) == {"prototyped"}
|
|
assert linux_msvc_semantics.scan_inline_functions(files, all_inlines=True) == {
|
|
"prototyped", "private_helper",
|
|
}
|
|
|
|
|
|
def test_render_skips_tags_used_as_both_struct_and_union():
|
|
text = linux_msvc_semantics.render(
|
|
{"a": {"struct"}, "b": {"union"}, "mixed": {"struct", "union"}}, {"f"}
|
|
)
|
|
assert "struct a;" in text
|
|
assert "union b;" in text
|
|
assert "mixed" not in text
|
|
assert "#pragma weak f" in text
|
|
|
|
|
|
# ---------- Xbox SDK declarations (port/include/xdk)
|
|
|
|
|
|
XDK_INCLUDE = Path(__file__).resolve().parent.parent / "port" / "include" / "xdk"
|
|
|
|
|
|
def test_xdk_headers_use_the_sdk_spellings():
|
|
# each port gives the SDK's keywords and names its own meaning (its
|
|
# prefix header), so none of one port's must be written into them
|
|
for header in sorted(XDK_INCLUDE.glob("*.h")):
|
|
text = re.sub(r"/\*.*?\*/", "", header.read_text(encoding="utf-8"), flags=re.S)
|
|
assert "__attribute__" not in text, header.name
|
|
assert not re.findall(r"\bhalo_\w+", text), header.name
|
|
|
|
|
|
@pytest.mark.skipif(shutil.which("clang") is None, reason="clang is needed to compile the headers")
|
|
def test_xdk_headers_compile_for_the_game(tmp_path):
|
|
root = XDK_INCLUDE.parent.parent.parent
|
|
source = write(tmp_path / "unit.c", "".join(
|
|
f"#include <{name}>\n" for name in ("xtl.h", "xbdm.h", "xkbd.h", "d3d8perf.h")
|
|
))
|
|
flags = [flag for flag in linux_build.LINUX_ABI_FLAGS + linux_build.GAME_FLAGS if flag != "-w"]
|
|
for defines in ([], ["-DDEBUG_KEYBOARD"], ["-DNOD3D", "-DNODSOUND"]):
|
|
subprocess.run(
|
|
["clang", *flags, *defines, "-Werror", "-fsyntax-only",
|
|
"-include", str(root / "port/linux/include/halo_linux_prefix.h"),
|
|
"-I", str(root / "port/linux/include"), "-idirafter", str(XDK_INCLUDE), str(source)],
|
|
check=True,
|
|
)
|
|
|
|
|
|
def pdb_writer():
|
|
"""An xdk_headers Writer over an empty type table: basic types only."""
|
|
from tools import pdb200_types, xdk_headers
|
|
pdb = object.__new__(pdb200_types.Pdb)
|
|
pdb.types, pdb.aggregates = {}, {}
|
|
return xdk_headers, xdk_headers.Writer(pdb)
|
|
|
|
|
|
def test_xdk_headers_regroups_anonymous_members():
|
|
# the PDB lists an anonymous structure's or union's members in their
|
|
# container, at their offsets
|
|
xdk_headers, writer = pdb_writer()
|
|
member = xdk_headers.Member
|
|
ulong, int64 = 0x22, 0x13
|
|
union = writer.render(writer.group(
|
|
[member("LowPart", ulong, 0), member("HighPart", ulong, 4), member("QuadPart", int64, 0)], True), "", "u")
|
|
assert union == ["struct {", " unsigned long LowPart;", " unsigned long HighPart;", "};",
|
|
"__int64 QuadPart;"]
|
|
structure = writer.render(writer.group(
|
|
[member("Tag", ulong, 0), member("Low", ulong, 4), member("High", ulong, 8),
|
|
member("Whole", int64, 4), member("After", ulong, 12)], False), "", "s")
|
|
assert structure == ["unsigned long Tag;", "union {", " struct {", " unsigned long Low;",
|
|
" unsigned long High;", " };", " __int64 Whole;", "};",
|
|
"unsigned long After;"]
|
|
|
|
|
|
# ---------- weak reference link check
|
|
|
|
|
|
@pytest.mark.skipif(shutil.which("clang") is None or shutil.which("nm") is None,
|
|
reason="clang and nm are needed to build test objects")
|
|
def test_link_check_rejects_undefined_weak_references(tmp_path):
|
|
def compile_object(name: str, text: str) -> Path:
|
|
source = write(tmp_path / f"{name}.c", text)
|
|
output = tmp_path / f"{name}.o"
|
|
subprocess.run(["clang", "-c", "-o", str(output), str(source)], check=True)
|
|
return output
|
|
|
|
caller = compile_object("caller", "#pragma weak helper\nint helper(int);\nint call(void) { return helper(1); }\n")
|
|
local = compile_object("local", "static int helper(int x) { return x; }\nint use(void) { return helper(2); }\n")
|
|
provider = compile_object("provider", "#pragma weak helper\nint helper(int x) { return x; }\n")
|
|
|
|
def check(*objects: Path) -> int:
|
|
response = write(tmp_path / "objects.rsp", "\n".join(f"'{o}'" for o in objects))
|
|
return subprocess.run(
|
|
[sys.executable, linux_link_check.__file__, str(response)], capture_output=True
|
|
).returncode
|
|
|
|
# a file-local copy elsewhere does not satisfy the reference
|
|
assert check(caller, local) == 1
|
|
assert check(caller, local, provider) == 0
|
|
|
|
|
|
# ---------- game sources
|
|
|
|
|
|
def test_game_sources_leave_out_the_excluded(tmp_path, monkeypatch):
|
|
write(tmp_path / "source" / "a.c", "")
|
|
write(tmp_path / "source" / "zlib" / "b.c", "")
|
|
write(tmp_path / "source" / "zlib" / "example.c", "")
|
|
write(tmp_path / "source" / "tiff" / "c.c", "")
|
|
write(tmp_path / "source" / "tiff" / "deep" / "d.c", "")
|
|
write(tmp_path / "source" / "tiffx.c", "")
|
|
write(tmp_path / "source" / "a.h", "")
|
|
monkeypatch.chdir(tmp_path)
|
|
# (a folder is a name ending in "/": not a file that only starts like it)
|
|
config = {"game": {"root": "source", "exclude": ["source/zlib/example.c", "source/tiff/"],
|
|
"defines": ["DEBUG"], "include_dirs": ["source", "source/saved games"]}}
|
|
assert linux_build.game_sources(config) == [Path("source/a.c"), Path("source/tiffx.c"), Path("source/zlib/b.c")]
|
|
assert linux_build.game_defines_and_includes(config) == '-DDEBUG -Isource -I"source/saved games"'
|
|
|
|
|
|
# ---------- menus (port/assets/menus)
|
|
|
|
|
|
MENUS = Path(__file__).resolve().parent.parent / "port/assets/menus"
|
|
MENU_ATTRIBUTES = {
|
|
"menus": {"root"},
|
|
"bitmap": {"name", "width", "height", "frames", "platform"},
|
|
"frame": {"png", "map", "index", "width", "height", "x", "y", "platform"},
|
|
"strings": {"name", "platform"},
|
|
"string": {"text", "platform"},
|
|
"widget": {"name", "type", "controller", "flags", "bitmap", "text", "strings", "values", "setting", "font",
|
|
"color", "align", "description", "string_list", "list_flags", "text_flags", "header_bitmap",
|
|
"footer_bitmap", "header_bounds", "footer_bounds", "child_controller", "x", "y", "left", "top",
|
|
"width", "height", "text_x", "text_y", "auto_close", "auto_close_fade", "string_index", "platform"},
|
|
"child": {"widget", "controller", "x", "y", "platform"},
|
|
"on": {"event", "run", "script", "open", "replace", "close", "widget", "focus", "reload", "sound", "otherwise",
|
|
"label", "back", "branch", "platform"},
|
|
"data": {"input", "platform"},
|
|
"conditional": {"widget", "if_failed", "platform"},
|
|
"replace": {"search", "function", "platform"},
|
|
}
|
|
|
|
|
|
def c_strings(path: Path, start: str, end: str) -> list:
|
|
"""the string literals of a C file between two markers"""
|
|
text = path.read_text(encoding="latin-1")
|
|
text = text[text.index(start):]
|
|
return re.findall(r'"([^"\\]*)"', text[:text.index(end)])
|
|
|
|
|
|
# menu_files.c's attributes read as whole numbers (its tables' { name, NULL, &long })
|
|
MENU_INTEGER_ATTRIBUTES = {"auto_close", "auto_close_fade", "height", "index", "left", "string_index", "text_x",
|
|
"text_y", "top", "width", "x", "y"}
|
|
|
|
|
|
def test_menus_are_well_formed():
|
|
"""What port/linux/src/menu_files.c and port/linux/game/menu_tags.c check
|
|
when the game loads them, but the map's own names (paths with backslashes),
|
|
which only the map has."""
|
|
import json
|
|
import xml.etree.ElementTree as ElementTree
|
|
|
|
root = MENUS.parent.parent.parent
|
|
tags = root / "port/linux/game/menu_tags.c"
|
|
events = c_strings(tags, "event_names[] =", "};")
|
|
flags = c_strings(tags, "widget_flag_names[] =", "};")
|
|
functions = set(c_strings(root / "source/interface/ui_widget_event_handler_functions.c", '\t{\n\t\t"NULL",', "}"))
|
|
functions |= set(c_strings(tags, "port_function_names[] =", "};"))
|
|
inputs = set(c_strings(tags, "game_data_input_names[] =", "};"))
|
|
inputs |= set(c_strings(tags, "port_game_data_input_names[] =", "};"))
|
|
listed = json.loads((MENUS / "menus.json").read_text())["files"]
|
|
files = sorted(MENUS.rglob("*.xml"))
|
|
assert sorted(path.relative_to(MENUS).as_posix() for path in files) == \
|
|
sorted(name for name in listed if name.endswith(".xml"))
|
|
widgets, bitmaps, strings, references, roots = {}, {}, {}, [], []
|
|
for path in files:
|
|
tree = ElementTree.parse(path)
|
|
assert tree.getroot().tag == "menus", path
|
|
if tree.getroot().get("root"):
|
|
roots.append(tree.getroot().get("root"))
|
|
for element in tree.getroot().iter():
|
|
where = f"{path.name}: <{element.tag} {element.get('name', '')}>"
|
|
assert element.tag in MENU_ATTRIBUTES, where
|
|
assert set(element.attrib) <= MENU_ATTRIBUTES[element.tag], where
|
|
# (the platforms that show it: menu_files.c's MENU_PLATFORM)
|
|
assert element.get("platform") is None or \
|
|
set(element.get("platform").split()) <= {"desktop", "android", "web"}, where
|
|
# (menu_files.c's whole numbers, which the tags keep in shorts,
|
|
# its true/false attributes, and no text but whitespace outside
|
|
# <string>s)
|
|
for attribute in MENU_INTEGER_ATTRIBUTES & set(element.attrib):
|
|
value = element.get(attribute)
|
|
assert re.fullmatch(r"-?[0-9]+", value) and -32768 <= int(value) <= 32767, f"{where} {attribute}"
|
|
for attribute in ("back", "branch", "if_failed"):
|
|
assert element.get(attribute) in (None, "true", "false"), f"{where} {attribute}"
|
|
assert not (element.text or "").strip() and not (element.tail or "").strip(), where
|
|
if element.tag == "bitmap":
|
|
bitmaps[element.get("name")] = element
|
|
for frame in element.iter("frame"):
|
|
assert (frame.get("png") is None) != (frame.get("map") is None), where
|
|
if frame.get("map"):
|
|
assert "\\" in frame.get("map") and int(frame.get("index")) >= 0, where
|
|
continue
|
|
assert frame.get("png") in listed, where
|
|
with (MENUS / frame.get("png")).open("rb") as png:
|
|
header = png.read(24)
|
|
width, height = int.from_bytes(header[16:20], "big"), int.from_bytes(header[20:24], "big")
|
|
logical = int(frame.get("width")), int(frame.get("height"))
|
|
assert width % logical[0] == 0 and width // logical[0] == height // logical[1], where
|
|
elif element.tag == "strings":
|
|
strings[element.get("name")] = element
|
|
elif element.tag == "widget":
|
|
name = element.get("name")
|
|
assert name and name not in widgets, where
|
|
widgets[name] = element
|
|
assert element.get("type", "container") in ("container", "text", "spinner", "column_list"), where
|
|
assert set(element.get("flags", "").split()) <= set(flags), where
|
|
children = element.findall("widget") + element.findall("child")
|
|
if element.get("type") == "spinner":
|
|
assert len(children) in (0, 1, 3), where
|
|
if element.get("strings") or "items_from_strings" in element.get("list_flags", ""):
|
|
assert element.get("type") == "spinner" and not children, where
|
|
if element.get("setting"):
|
|
assert len(element.get("strings").split("|")) == len(element.get("values").split("|")), where
|
|
for attribute in ("bitmap", "header_bitmap", "footer_bitmap"):
|
|
references.append((where, bitmaps, element.get(attribute)))
|
|
references.append((where, strings, element.get("string_list")))
|
|
references.append((where, widgets, element.get("description")))
|
|
elif element.tag == "child" or element.tag == "conditional":
|
|
references.append((where, widgets, element.get("widget")))
|
|
elif element.tag == "on":
|
|
assert set(element.get("event").split()) <= set(events), where
|
|
run = element.get("run")
|
|
assert run is None or run in functions or run.startswith("unwired "), where
|
|
assert element.get("otherwise") is None or run, where
|
|
for attribute in ("open", "replace", "focus", "widget", "otherwise"):
|
|
references.append((where, widgets, element.get(attribute)))
|
|
elif element.tag == "data":
|
|
assert element.get("input") in inputs or element.get("input").startswith("unwired "), where
|
|
assert all(root_name in widgets for root_name in roots)
|
|
for where, names, name in references:
|
|
if name and "\\" not in name:
|
|
assert name in names, f"{where} {name}"
|
|
|
|
|
|
def test_menu_settings_exist():
|
|
"""Every setting a menu's spinner sets is one of config.toml's
|
|
(port/linux/src/port_config.c) or of the profile (menu_functions.c), and
|
|
the keyboard's controls are the same in the settings, in the controls'
|
|
screen and in the input code."""
|
|
root = MENUS.parent.parent.parent
|
|
config = (root / "port/linux/src/port_config.c").read_text()
|
|
functions = (root / "port/linux/game/menu_functions.c").read_text()
|
|
known = set(re.findall(r'^\t\{ "([a-z_]+\.[a-z_]+)", _config_', config, re.M))
|
|
profile = set(re.findall(r'\{ "(profile\.[a-z_]+)", \d', functions))
|
|
for path in (MENUS / "ce").glob("*.xml"):
|
|
for setting in re.findall(r'setting="([^"]+)"', path.read_text()):
|
|
assert setting in known | profile, f"{path.name}: {setting}"
|
|
controls = set(re.findall(r'"(controls\.[a-z_]+)"', (root / "port/linux/src/xinput_sdl.c").read_text()))
|
|
assert controls == {name for name in known if name.startswith("controls.")}
|
|
assert controls == set(re.findall(r'\{ "(controls\.[a-z_]+)", L"', functions))
|
|
|
|
|
|
def test_p2p_signatures_and_listings(tmp_path):
|
|
"""internet play's Ed25519 (RFC 8032), the X25519 key of a seed, and the
|
|
server browser's listings from host to browser (tools/p2p_lobby_check.c),
|
|
built with the flags ninja gives the platform layer"""
|
|
import shlex
|
|
|
|
if not shutil.which("clang") or not shutil.which("ninja") or not Path("build.ninja").is_file():
|
|
pytest.skip("needs clang, ninja and a configured build")
|
|
command = subprocess.run(["ninja", "-t", "commands", "build/linux/obj/port/linux/src/p2p_crypto.o"],
|
|
capture_output=True, text=True, check=True).stdout.strip().splitlines()[-1]
|
|
words = shlex.split(command)
|
|
# (the compiler, and whatever runs it, ccache in CI: up to the first flag)
|
|
while words and not words[0].startswith("-"):
|
|
words = words[1:]
|
|
flags = []
|
|
skip = False
|
|
for word in words:
|
|
if skip:
|
|
skip = False
|
|
elif word in ("-MF", "-o", "-c"):
|
|
skip = True
|
|
elif word == "-MMD" or word.startswith("-flto") or word.startswith("-fprofile-use"):
|
|
continue
|
|
else:
|
|
flags.append(word)
|
|
program = tmp_path / "p2p_lobby_check"
|
|
built = subprocess.run(["clang", *flags, "-O1", "-no-pie", "-Wl,--unresolved-symbols=ignore-all", "-o",
|
|
str(program), "tools/p2p_lobby_check.c", "port/linux/src/p2p_crypto.c",
|
|
"port/linux/src/p2p_lobby.c", "port/third_party/monocypher/monocypher.c",
|
|
"port/third_party/monocypher/monocypher-ed25519.c"],
|
|
capture_output=True, text=True)
|
|
assert built.returncode == 0, built.stderr[-4000:]
|
|
result = subprocess.run([str(program)], capture_output=True, text=True, timeout=60)
|
|
assert result.returncode == 0, result.stdout
|
|
assert "PASS" in result.stdout
|
|
|
|
|
|
# ---------- system link's stand-in machines (tools/system_link_bots.py)
|
|
|
|
|
|
def test_system_link_bots_messages_are_the_games():
|
|
"""the stand-in machines number their messages as network_messages.h does,
|
|
and send each at the size network_messages.c's fields decode (pad 0,
|
|
bytes 1, shorts 2, longs 4; an array a count byte and one element)"""
|
|
import struct
|
|
import time
|
|
import types
|
|
from tools import system_link_bots as bots
|
|
|
|
root = Path(__file__).resolve().parents[1]
|
|
header = (root / "source/networking/network_messages.h").read_text()
|
|
enum = header[header.index("enum network_game_message_type"):]
|
|
names = re.findall(r"\t_message_(\w+)", enum[:enum.index("};")])
|
|
for name, value in vars(bots).items():
|
|
if name.lower() in names:
|
|
assert names.index(name.lower()) == value, name
|
|
sizes = {}
|
|
source = (root / "source/networking/network_messages.c").read_text()
|
|
for fields, name in re.findall(r"\t\{\n((?:\t\t.*\n)*?)\t\},\n\tNETWORK_GAME_MESSAGE_DEFINITION\((\w+),", source):
|
|
sizes[name] = sum(int(count) * {"pad": 0, "bytes": 1, "shorts": 2, "longs": 4}.get(kind, 0) + (kind == "array")
|
|
for kind, count in re.findall(r"_data_packet_field_(\w+), (\d+)", fields))
|
|
|
|
sent = []
|
|
machine = bots.Machine(1, "127.0.0.2", "127.0.0.1", print)
|
|
machine.udp.close()
|
|
machine.send = sent.append
|
|
machine.udp = types.SimpleNamespace(sendto=lambda data, address: sent.append(data))
|
|
machine.joined()
|
|
machine.handle(bots.SERVER_MACHINE_ACCEPTED, struct.pack(">ih", 0, 1))
|
|
settings = bytes(0x24) + b"levels\\test\\bloodgulch\\bloodgulch".ljust(0x80, b"\0")
|
|
machine.handle(bots.SERVER_GAME_SETTINGS_UPDATE, struct.pack(">HHH", len(settings), 0, len(settings)) + settings)
|
|
machine.tick(time.monotonic())
|
|
machine.handle(bots.SERVER_BEGIN_GAME, b"")
|
|
machine.tick(time.monotonic() + 60)
|
|
machine.handle(bots.SERVER_GAME_UPDATE, struct.pack(">I", 1))
|
|
machine.tick(time.monotonic())
|
|
assert len(sent) == 6
|
|
for data in sent:
|
|
name = names[data[-1]]
|
|
assert len(data) - 4 == sizes[name], name
|
|
|
|
|
|
# ---------- the wire format (tools/network_format.py)
|
|
|
|
|
|
def test_network_format_is_the_recorded_one():
|
|
"""what the machines send each other is what port/linux/network_format.txt
|
|
records for HALO_PORT_NETWORK_VERSION, and NETCODE.md says what that
|
|
version changed"""
|
|
from tools import network_format
|
|
|
|
problems = network_format.check()
|
|
assert not problems, "\n".join(problems)
|
|
|
|
|
|
# ---------- the tag validator (port/linux/game/tag_validate.c)
|
|
|
|
RETAIL_MAPS = Path("assets/maps")
|
|
MAP_VALIDATE = Path("build/linux/map_validate")
|
|
|
|
|
|
def _retail_maps():
|
|
maps = sorted(RETAIL_MAPS.glob("*.map"))
|
|
if not maps or not MAP_VALIDATE.is_file():
|
|
pytest.skip("needs the retail maps in assets/maps and ninja linux's build/linux/map_validate")
|
|
return maps
|
|
|
|
|
|
def test_retail_maps_need_no_corrections():
|
|
"""every retail map in assets/maps passes the tag validator, each of its
|
|
structure bsps too, with no correction: the schemas (tag_schema_*.c)
|
|
say what the game's own maps hold"""
|
|
maps = _retail_maps()
|
|
result = subprocess.run([str(MAP_VALIDATE), "--strict", *map(str, maps)], capture_output=True, text=True,
|
|
timeout=1800)
|
|
assert result.returncode == 0, result.stdout[-6000:]
|
|
|
|
|
|
def test_tag_validator_survives_damaged_maps():
|
|
"""retail maps with a few of their tags' words changed at random: the
|
|
validator never crashes or hangs, and a map it lets through is clean
|
|
when checked again"""
|
|
maps = _retail_maps()
|
|
chosen = [path for path in maps if path.stem in ("bloodgulch", "a10", "ui")] or maps[:1]
|
|
result = subprocess.run([str(MAP_VALIDATE), "--fuzz", "300", "--seed", "1", *map(str, chosen)],
|
|
capture_output=True, text=True, timeout=1800)
|
|
assert result.returncode == 0, (result.stdout + result.stderr)[-6000:]
|
|
|
|
|
|
# ---------- Custom Edition maps (port/linux/game/cache_file_formats.c, custom_edition_cache.c)
|
|
|
|
def _custom_edition_maps():
|
|
"""the Custom Edition maps (HALO_CUSTOM_EDITION_MAPS, or the gitignored
|
|
assets/custom_edition), with the bitmaps.map, sounds.map and loc.map
|
|
they need beside them"""
|
|
configured = os.environ.get("HALO_CUSTOM_EDITION_MAPS")
|
|
folder = Path(configured) if configured else Path("assets/custom_edition")
|
|
maps = [path for path in sorted(folder.glob("*.map"))
|
|
if path.stem.lower() not in ("bitmaps", "sounds", "loc")]
|
|
if not maps or not (folder / "bitmaps.map").is_file() or not MAP_VALIDATE.is_file():
|
|
pytest.skip("needs Custom Edition maps (HALO_CUSTOM_EDITION_MAPS or assets/custom_edition) and "
|
|
"ninja linux's build/linux/map_validate")
|
|
return maps
|
|
|
|
|
|
def test_custom_edition_maps_are_loaded_and_checked():
|
|
"""every Custom Edition map is loaded, converted and passed by the tag
|
|
validator, each of its structure bsps too: none is refused (a community
|
|
map's own mistakes are corrected and logged)"""
|
|
maps = _custom_edition_maps()
|
|
result = subprocess.run([str(MAP_VALIDATE), *map(str, maps)], capture_output=True, text=True, timeout=3600)
|
|
assert result.returncode == 0, result.stdout[-6000:]
|
|
assert "refused" not in result.stdout, result.stdout[-6000:]
|
|
|
|
|
|
def test_custom_edition_loader_and_validator_survive_damaged_maps():
|
|
"""Custom Edition maps with a few of their file's words changed at random:
|
|
the loader, the conversion and the validator never crash or hang, and a
|
|
map they let through is clean when checked again"""
|
|
maps = _custom_edition_maps()
|
|
chosen = [path for path in maps if path.stem.lower() in ("bloodgulch", "hugeass", "ui")] or maps[:2]
|
|
result = subprocess.run([str(MAP_VALIDATE), "--fuzz", "100", "--seed", "1", *map(str, chosen)],
|
|
capture_output=True, text=True, timeout=3600)
|
|
assert result.returncode == 0, (result.stdout + result.stderr)[-6000:]
|