mirror of
https://github.com/ClassicWasTaken/HaloCE-SteamFrame.git
synced 2026-10-15 14:00:28 +02:00
231 lines
12 KiB
Python
231 lines
12 KiB
Python
"""Offline source-integration regressions; no game data or headset connection."""
|
|
import ast
|
|
import importlib.util
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import tomllib
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
spec = importlib.util.spec_from_file_location("experimental_engine_check", ROOT / "scripts/check-experimental-engine.py")
|
|
engine = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(engine)
|
|
|
|
|
|
TWO_PASS = """
|
|
static void bind_textures(struct nv2a_pixel_shader_key *key, float texture_scale[4][4])
|
|
{
|
|
GLenum gl_targets[D3DTSS_MAXSTAGES];
|
|
GLuint gl_textures[D3DTSS_MAXSTAGES];
|
|
int stage;
|
|
for (stage = 0; stage < D3DTSS_MAXSTAGES; stage++)
|
|
{
|
|
gl_textures[stage] = xgpu_texture_get(texture, palette, &gl_targets[stage], &description);
|
|
}
|
|
for (stage = 0; stage < D3DTSS_MAXSTAGES; stage++)
|
|
state_texture(stage, gl_targets[stage], gl_textures[stage]);
|
|
}
|
|
"""
|
|
|
|
|
|
def literal_assignment(path, name):
|
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
for node in tree.body:
|
|
if isinstance(node, ast.Assign) and any(isinstance(target, ast.Name) and target.id == name for target in node.targets):
|
|
return ast.literal_eval(node.value)
|
|
raise AssertionError("Missing " + name + " in " + str(path))
|
|
|
|
|
|
def test_installer_and_remote_metadata_share_the_exact_experimental_engine_pin():
|
|
for path, name in ((ROOT / "src/halo_frame_installer/install.py", "SOURCE_COMMIT"),
|
|
(ROOT / "resources/remote_install.py", "SOURCE_COMMIT"),
|
|
(ROOT / "resources/steam_shortcut.py", "ICON_SOURCE_COMMIT")):
|
|
assert literal_assignment(path, name) == engine.SOURCE_COMMIT
|
|
|
|
|
|
def test_two_pass_texture_binding_is_accepted():
|
|
engine.check_texture_binding(TWO_PASS)
|
|
|
|
|
|
@pytest.mark.parametrize("broken", [
|
|
TWO_PASS.replace("gl_textures[stage] = xgpu_texture_get", "state_texture(stage, gl_targets[stage], gl_textures[stage]); gl_textures[stage] = xgpu_texture_get"),
|
|
TWO_PASS.replace("for (stage = 0; stage < D3DTSS_MAXSTAGES; stage++)\n state_texture", "state_texture"),
|
|
TWO_PASS.replace("state_texture(stage, gl_targets[stage], gl_textures[stage])", "state_texture(stage, GL_TEXTURE_2D, 0)"),
|
|
])
|
|
def test_shader_upload_binding_regressions_are_rejected(broken):
|
|
with pytest.raises(engine.EngineCheckError, match="Texture binding|Final texture bindings"):
|
|
engine.check_texture_binding(broken)
|
|
|
|
|
|
@pytest.mark.parametrize("path", ["../outside.c", "/outside.c", "source/network/network_messages.c", "resources/remote_install.py"])
|
|
def test_engine_patch_cannot_hide_unreviewed_paths(path):
|
|
with pytest.raises(engine.EngineCheckError, match="outside the reviewed"):
|
|
engine.patch_files("diff --git a/" + path + " b/" + path + "\n")
|
|
|
|
|
|
def test_patch_path_allowlist_contains_both_new_header_and_existing_input_hooks():
|
|
text = (ROOT / "resources/frame-controls.patch").read_text(encoding="utf-8")
|
|
paths = engine.patch_files(text)
|
|
assert {"port/linux/src/vr_locomotion.h", "port/linux/src/vr.c", "port/linux/src/port_config.c", "source/game/player_control.c"} <= paths
|
|
|
|
|
|
def test_protocol_11_engine_cannot_be_mislabeled_as_matching_experimental_coop(tmp_path):
|
|
path = tmp_path / "port/linux/include/halo_port_limits.h"
|
|
path.parent.mkdir(parents=True)
|
|
path.write_text("#define HALO_PORT_NETWORK_VERSION 11\n")
|
|
with pytest.raises(engine.EngineCheckError, match="protocol 17"):
|
|
engine.check_upstream(tmp_path)
|
|
|
|
|
|
@pytest.fixture
|
|
def recipe_projection(tmp_path):
|
|
declaration = "import argparse\np = argparse.ArgumentParser()\n"
|
|
declaration += "\n".join("p.add_argument('" + option + "')" for option in ("--cc", "--ld", "--sdk"))
|
|
for path in engine.PATCH_FILES:
|
|
if path.startswith("tools/test_") and path.endswith(".py"):
|
|
candidate = tmp_path / path
|
|
candidate.parent.mkdir(exist_ok=True)
|
|
candidate.write_text(declaration, encoding="utf-8")
|
|
(tmp_path / "configure.py").write_text("# configure boundary\n", encoding="utf-8")
|
|
return tmp_path
|
|
|
|
|
|
def test_native_recipe_checks_every_shipped_test_in_the_projection(recipe_projection):
|
|
actual = engine.check_build_recipe(recipe_projection)
|
|
expected = {path for path in engine.PATCH_FILES if path.startswith("tools/test_") and path.endswith(".py")}
|
|
assert set(actual) == expected and len(actual) == len(expected)
|
|
|
|
|
|
@pytest.mark.parametrize("name", ["test_vr_scope.py", "test_vr_scope_render.py", "test_vr_scope_backend.py",
|
|
"test_vr_scope_hud.py", "test_vr_scope_link.py"])
|
|
def test_recipe_rejects_each_removed_scope_command(recipe_projection, name):
|
|
text = (ROOT / "resources/build-native.sh").read_text(encoding="utf-8")
|
|
text = text.replace("python3 configure.py", "python3 tools/" + name + " --cc clang\npython3 configure.py")
|
|
with pytest.raises(engine.EngineCheckError, match="does not match the shipped"):
|
|
engine.check_build_recipe(recipe_projection, text)
|
|
|
|
|
|
def test_recipe_rejects_missing_actual_test_file(recipe_projection):
|
|
(recipe_projection / "tools/test_vr_framebuffer.py").unlink()
|
|
with pytest.raises(engine.EngineCheckError, match="missing script: tools/test_vr_framebuffer.py"):
|
|
engine.check_build_recipe(recipe_projection)
|
|
|
|
|
|
@pytest.mark.parametrize("before,after,diagnostic", [
|
|
("--sdk /usr/include", "--missing-sdk /usr/include", "unsupported option"),
|
|
("python3 tools/test_vr_tracking.py --cc clang", "python3 tools/test_vr_tracking.py --cc clang\npython3 tools/test_vr_tracking.py --cc clang", "repeats a regression"),
|
|
("--release --vr --linux-arm64-cc clang", "--release --linux-arm64-cc clang", "release ARM64 VR target"),
|
|
("ninja -j4 linux_arm64", "ninja -j4 linux", "configured ARM64 target"),
|
|
("set -euo pipefail", "set -u", "stop on failed checks"),
|
|
])
|
|
def test_recipe_rejects_invalid_commands_and_failure_masking(recipe_projection, before, after, diagnostic):
|
|
text = (ROOT / "resources/build-native.sh").read_text(encoding="utf-8")
|
|
assert before in text
|
|
with pytest.raises(engine.EngineCheckError, match=diagnostic):
|
|
engine.check_build_recipe(recipe_projection, text.replace(before, after))
|
|
|
|
|
|
def test_recipe_accepts_and_checks_local_delegated_cli_options(recipe_projection):
|
|
(recipe_projection / "tools/test_vr_flashlight.py").write_text(
|
|
"import test_vr_projectile_reticle as reticle\nreticle.main()\n", encoding="utf-8")
|
|
assert "tools/test_vr_flashlight.py" in engine.check_build_recipe(recipe_projection)
|
|
(recipe_projection / "tools/test_vr_projectile_reticle.py").write_text("def main(): pass\n", encoding="utf-8")
|
|
with pytest.raises(engine.EngineCheckError, match="unsupported option"):
|
|
engine.check_build_recipe(recipe_projection)
|
|
|
|
|
|
def test_public_release_documentation_names_the_single_installer_download():
|
|
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
|
download = re.search(
|
|
r"https://github\.com/ClassicWasTaken/HaloSteamFrameMod/releases/download/[^\s\"<>)]*",
|
|
readme,
|
|
)
|
|
assert download is not None, "README is missing its primary installer download"
|
|
identity = re.fullmatch(
|
|
r"https://github\.com/ClassicWasTaken/HaloSteamFrameMod/releases/download/"
|
|
r"v(?P<version>\d+\.\d+\.\d+)/Halo-Steam-Frame-Mod-Setup-(?P=version)\.exe",
|
|
download.group(),
|
|
)
|
|
assert identity is not None, "README installer tag and filename must share a stable version"
|
|
version = identity["version"]
|
|
filename = f"Halo-Steam-Frame-Mod-Setup-{version}.exe"
|
|
package_version = literal_assignment(ROOT / "src/halo_frame_installer/__init__.py", "__version__")
|
|
build_version = literal_assignment(ROOT / "scripts/build_release.py", "VERSION")
|
|
assert package_version == build_version
|
|
if re.fullmatch(r"\d+\.\d+\.\d+", package_version):
|
|
assert version == package_version, "Stable builds must document their own release download"
|
|
assert tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))["project"]["version"] == version
|
|
# Private candidates retain the previous public download until publication.
|
|
for name in ("README.md", "docs/SETUP.md", "docs/RELEASE_NOTES.md"):
|
|
text = (ROOT / name).read_text(encoding="utf-8")
|
|
assert version in text and filename in text and download.group() in text
|
|
assert "has not been published" not in text and "local experimental preview" not in text
|
|
notes = (ROOT / "docs/RELEASE_NOTES.md").read_text(encoding="utf-8")
|
|
assert "cannot load old checkpoints" in notes and "save-v1.4" in notes
|
|
sd_guide = (ROOT / "docs/SD_CARD.md").read_text(encoding="utf-8")
|
|
assert "Halo: Combat Evolved VR (Native, SD card)" in sd_guide
|
|
assert "Refresh storage" in readme and "ext4 or f2fs" in sd_guide
|
|
assert "BUILD_PROVENANCE.md" in readme and "BUILD_PROVENANCE.md" in notes
|
|
|
|
|
|
@pytest.fixture
|
|
def release_documentation_projection(tmp_path, monkeypatch):
|
|
for relative in ("README.md", "docs/SETUP.md", "docs/RELEASE_NOTES.md", "docs/SD_CARD.md",
|
|
"src/halo_frame_installer/__init__.py", "scripts/build_release.py", "pyproject.toml"):
|
|
destination = tmp_path / relative
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
destination.write_bytes((ROOT / relative).read_bytes())
|
|
public_version = re.search(r"/releases/download/v(\d+\.\d+\.\d+)/", (tmp_path / "README.md").read_text(encoding="utf-8")).group(1)
|
|
for relative, name in (("src/halo_frame_installer/__init__.py", "__version__"),
|
|
("scripts/build_release.py", "VERSION")):
|
|
path = tmp_path / relative
|
|
old = literal_assignment(path, name)
|
|
path.write_text(path.read_text(encoding="utf-8").replace('"' + old + '"', '"' + public_version + '"'), encoding="utf-8")
|
|
project = tmp_path / "pyproject.toml"
|
|
old = tomllib.loads(project.read_text(encoding="utf-8"))["project"]["version"]
|
|
project.write_text(project.read_text(encoding="utf-8").replace('version = "' + old + '"',
|
|
'version = "' + public_version + '"', 1), encoding="utf-8")
|
|
monkeypatch.setitem(globals(), "ROOT", tmp_path)
|
|
return tmp_path
|
|
|
|
|
|
def test_public_release_documentation_rejects_a_stale_download_for_a_stable_build(release_documentation_projection):
|
|
readme = release_documentation_projection / "README.md"
|
|
version = literal_assignment(release_documentation_projection / "scripts/build_release.py", "VERSION")
|
|
text = readme.read_text(encoding="utf-8")
|
|
readme.write_text(text.replace("/v" + version + "/Halo-Steam-Frame-Mod-Setup-" + version + ".exe",
|
|
"/v0.0.1/Halo-Steam-Frame-Mod-Setup-0.0.1.exe"), encoding="utf-8")
|
|
with pytest.raises(AssertionError, match="Stable builds must document their own release"):
|
|
test_public_release_documentation_names_the_single_installer_download()
|
|
|
|
|
|
def test_private_candidate_keeps_the_existing_public_download(release_documentation_projection):
|
|
for relative, name in (("src/halo_frame_installer/__init__.py", "__version__"),
|
|
("scripts/build_release.py", "VERSION")):
|
|
path = release_documentation_projection / relative
|
|
version = literal_assignment(path, name)
|
|
path.write_text(path.read_text(encoding="utf-8").replace('"' + version + '"',
|
|
'"' + version + '-Test"'), encoding="utf-8")
|
|
test_public_release_documentation_names_the_single_installer_download()
|
|
|
|
|
|
def test_actual_exact_pinned_engine_and_shipped_patch_when_checkout_is_supplied():
|
|
source = os.environ.get("HALO_EXPERIMENTAL_ENGINE_SOURCE")
|
|
if not source:
|
|
pytest.skip("Set HALO_EXPERIMENTAL_ENGINE_SOURCE for the local offline engine integration check")
|
|
result = engine.check_source(Path(source))
|
|
assert result["ok"] and result["protocolVersion"] == 17
|
|
assert result["offlineArm64NinjaGraph"] and result["headDirectedMovementIntegration"]
|
|
assert result["buildRecipeIntegration"]
|
|
assert result["menuControllerRigVisibility"] and result["controllerReticleFallbackSuppressed"]
|
|
assert result["stereoSunGlowIntegration"] and result["vrInternetCampaignMenu"]
|
|
assert result["boundedSunGlowStrength"]
|
|
assert result["nominalProjectileReticleIntegration"]
|
|
assert result["renderRateProjectilePreviewIntegration"]
|
|
assert result["networkCampaignOnlyIntegration"]
|
|
assert result["vrTutorialIntegration"] and result["freshFullPoseTrackingIntegration"]
|
|
assert result["renderRateUnarmedFlashlightIntegration"]
|
|
assert not result["arm64LinkedBuild"] and not result["hardwareValidated"]
|