Files
Addison Bair 8ebca844a0 Asset-free regression tests: tools/harness
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.
2026-10-07 22:11:58 +00:00

49 lines
2.1 KiB
Python

"""The real widget pool (widgets.c) over fake objects: fills, refuses past its size, frees and reuses its slots."""
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, function, mutated, read, run # noqa: E402
CASES = ["finds-the-type", "fills-and-refuses", "frees-and-reuses", "failed-widget-frees-its-slot"]
# a fault in widgets.c, the function it is in, and the case that must catch it
NEGATIVE_CONTROLS = {
"keeps-a-failed-widgets-slot": (("datum_delete(widget_data, widget_index);", ";"), "widgets_new",
"failed-widget-frees-its-slot"),
"never-frees-on-delete": (("datum_delete(widget_data, widget_index);", ";"), "widgets_delete", "frees-and-reuses"),
}
def generated(fault=None, faulty_function=None):
widgets = read("source/objects/widgets/widgets.c")
groups = re.findall(r"^\s*'(\w{3}[\w!])',\s*$", widgets, re.M)
assert len(groups) == 5, f"widget types not found in widgets.c: {groups}"
config = "".join(f"#define GROUP_TAG_{index} '{group}'\n" for index, group in enumerate(groups))
config += f"#define MAXIMUM_WIDGETS_PER_MAP {constant(widgets, 'MAXIMUM_WIDGETS_PER_MAP')}\n"
text = ""
for name in ("widget_type_definition_get", "tag_group_to_widget_type", "widgets_new", "widgets_delete"):
code = function(widgets, 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("widget_pool", 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("widget_pool", generated(fault, faulty_function)), case)
assert status == CHECK_FAILED, f"widgets.c with a fault ({control}) passed '{case}': the test cannot see it"