Files
Cesarus85--ringfall-mjolnir-vr/tools/test_linux_port.py
T

342 lines
17 KiB
Python

"""Tests for the native Linux build tooling (port/linux, tools/linux_*.py)."""
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" / "a.h", "")
monkeypatch.chdir(tmp_path)
config = {"game": {"root": "source", "exclude": ["source/zlib/example.c"],
"defines": ["DEBUG"], "include_dirs": ["source", "source/saved games"]}}
assert linux_build.game_sources(config) == [Path("source/a.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", "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
assert element.get("platform") in (None, "desktop", "android"), 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