mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-29 04:01:10 +01:00
The engine's real functions, taken from the sources and compiled against a fake world, so a fix can carry a test CI runs without game assets. A test is tests/<name>.c (fake world and cases, CHECK) and tests/test_<name>.py (takes the code, enums and constants from the sources, builds, runs each case under pytest). Each test has negative controls: the code with a deliberate fault must fail a named CHECK. CI runs pytest on tools/harness, so new tests need no workflow change. - test_object_bounds_cache: collision's loop over a crowded fake world gathers the same features with the cache, as objects move and after invalidation; a stale cache is detected. - test_widget_pool: widgets.c's pool fills to its size and refuses (64 slots against 128 players' rifles, #145), frees and reuses, and frees a failed widget's slot.
90 lines
3.4 KiB
Python
90 lines
3.4 KiB
Python
"""Asset-free regression tests: the engine's real code compiled against a fake world (README.md)."""
|
|
|
|
import functools
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent.parent
|
|
HARNESS = Path(__file__).resolve().parent
|
|
|
|
|
|
def read(relative):
|
|
"""A source file of the repository, as the compiler reads it."""
|
|
return (ROOT / relative).read_text(encoding="latin-1")
|
|
|
|
|
|
def function(source, name):
|
|
"""A function's definition (static, inline or not), taken by matching braces; LookupError if not found."""
|
|
match = re.search(r"^(?:static |__inline )?[\w *]+?\b" + re.escape(name) + r"\(\s*[^;{]*\)\s*\{", source, re.M)
|
|
if not match:
|
|
raise LookupError(f"function not found in the sources: {name}")
|
|
end = source.index("{", match.start()) + 1
|
|
depth = 1
|
|
while depth:
|
|
depth += (source[end] == "{") - (source[end] == "}")
|
|
end += 1
|
|
return source[match.start():end]
|
|
|
|
|
|
def inline(source, name):
|
|
"""An __inline header function, as a static one."""
|
|
return function(source, name).replace("__inline", "static", 1)
|
|
|
|
|
|
def enum_with(source, member):
|
|
"""The whole enum that declares a member."""
|
|
at = source.index(member)
|
|
start = source.rindex("enum", 0, at)
|
|
return source[start:source.index("};", at) + 2]
|
|
|
|
|
|
def constant(source, name):
|
|
"""An enum constant's or #define's integer value, following a name it is set to (as the port's capacities are)."""
|
|
match = re.search(r"\b" + re.escape(name) + r"\s*=\s*(\w+)", source) or \
|
|
re.search(r"#define\s+" + re.escape(name) + r"\s+(\w+)", source)
|
|
if not match:
|
|
raise LookupError(f"constant not found in the sources: {name}")
|
|
value = match.group(1)
|
|
if re.fullmatch(r"0x[0-9A-Fa-f]+|\d+", value):
|
|
return int(value, 0)
|
|
return constant(source + read("port/linux/include/halo_port_capacity.h"), value)
|
|
|
|
|
|
def mutated(text, before, after):
|
|
"""The code with one deliberate fault (a negative control); LookupError once the code no longer has it."""
|
|
if text.count(before) != 1:
|
|
raise LookupError(f"negative control no longer applies (found {text.count(before)} times): {before!r}")
|
|
return text.replace(before, after, 1)
|
|
|
|
|
|
_directory = tempfile.TemporaryDirectory(prefix="halo-harness-")
|
|
|
|
|
|
CHECK_FAILED = 1 # a case's exit status when a CHECK fails (harness.h)
|
|
|
|
|
|
@functools.lru_cache(maxsize=None)
|
|
def build(test, generated):
|
|
"""tests/<test>.c compiled with the generated includes ((name, text), ...) for the game's 32-bit ABI."""
|
|
work = Path(_directory.name) / f"{test}-{abs(hash(generated)):x}"
|
|
work.mkdir(parents=True, exist_ok=True)
|
|
for name, text in generated:
|
|
(work / name).write_text(text)
|
|
executable = work / test
|
|
command = [os.environ.get("CC", "clang"), "-m32", "-std=gnu99", "-O2", "-Wall", "-Werror", "-Wno-unused-function",
|
|
"-Wno-unused-variable", "-I", str(HARNESS / "include"), "-I", str(work),
|
|
str(HARNESS / "tests" / f"{test}.c"), "-o", str(executable)]
|
|
result = subprocess.run(command, capture_output=True, text=True)
|
|
if result.returncode:
|
|
raise RuntimeError(f"{test}.c does not compile:\n{result.stderr}")
|
|
return executable
|
|
|
|
|
|
def run(executable, case):
|
|
"""Run one case: (exit status, output)."""
|
|
result = subprocess.run([str(executable), case], capture_output=True, text=True, timeout=60)
|
|
return result.returncode, (result.stdout + result.stderr).strip()
|