Files
horizon-airdef/tests/test_pr_ready.py
T
Ludovic Marie b15e5e893e Code mods are the client's mods feature: tests and probes skip wasmtime
horizon-mods' runtime is no longer a default feature, and its sandbox test
requires runtime and pack instead of a dev-dependency on itself (that
dev-dependency turned the runtime on in every workspace build with examples,
the parity probe build included). The client gets a mods feature that turns
the runtime on; without it game/mods.rs keeps content mods and logs why code
mods don't start, and every hook does nothing.

Release builds (client and server) pass the feature, CI's clippy and tests
pass it (and pack, for the sandbox test), and pr_ready does where horizon-mods
is checked. horizon_mods::find moves to manifest.rs: content mods use it
without the runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:18:01 +02:00

216 lines
14 KiB
Python

import argparse
import json
import unittest
from unittest import mock
from tools import pr_ready
PACKAGES = {"horizon-core": "crates/horizon-core", "horizon-client": "crates/horizon-client",
"horizon-theme": "crates/horizon-theme", "xtask": "xtask"}
LIBRARIES = set(PACKAGES) - {"xtask"}
DEPENDENCIES = {"horizon-client": {"horizon-core"}}
MODS = " --features horizon-mods/runtime,horizon-mods/pack,horizon-client/mods"
def args(host="local", quick=False):
return argparse.Namespace(host=host, quick=quick)
class PrReadyTests(unittest.TestCase):
def test_paths_map_to_the_crates_that_own_them(self):
self.assertEqual(pr_ready.touched_crates({"crates/horizon-core/src/jmath.rs", "crates/horizon-client/build.rs",
"xtask/src/main.rs", "tools/parity.py"}, PACKAGES),
["horizon-client", "horizon-core", "xtask"])
self.assertEqual(pr_ready.touched_crates({"crates/horizon-core/README.md"}, PACKAGES), [])
# The generated stylesheet is checked by horizon-theme's tests.
self.assertEqual(pr_ready.touched_crates({"website/src/tokens.css"}, PACKAGES), ["horizon-theme"])
# A crate whose name prefixes another's directory doesn't own it.
self.assertEqual(pr_ready.touched_crates({"crates/horizon-core-extra/x.rs"}, PACKAGES), [])
# The workspace manifest and lock touch every crate.
self.assertIsNone(pr_ready.touched_crates({"Cargo.lock", "crates/horizon-core/src/lib.rs"}, PACKAGES))
def test_which_workspace_builds_a_path(self):
self.assertTrue(pr_ready.workspace_rust("crates/horizon-theme/fonts/x.ttf"))
self.assertTrue(pr_ready.workspace_rust("Cargo.toml"))
self.assertFalse(pr_ready.workspace_rust("crates/horizon-client/README.md"))
self.assertFalse(pr_ready.workspace_rust("launcher/src/main.rs"))
self.assertTrue(pr_ready.launcher_rust("launcher/src/main.rs"))
self.assertTrue(pr_ready.launcher_rust("crates/horizon-prepare/src/lib.rs"))
self.assertTrue(pr_ready.launcher_rust("crates/horizon-theme/src/lib.rs"))
self.assertTrue(pr_ready.launcher_rust("crates/horizon-telemetry/src/lib.rs"))
self.assertFalse(pr_ready.launcher_rust("launcher/links.json"))
self.assertFalse(pr_ready.launcher_rust("launcher/README.md"))
self.assertFalse(pr_ready.launcher_rust("crates/horizon-client/src/game.rs"))
def test_crate_checks_do_not_link_unrelated_examples(self):
with mock.patch.object(pr_ready, "affected_here", return_value=[]):
steps = pr_ready.plan(args(quick=True), "origin/main", {"crates/horizon-client/src/game.rs"}, PACKAGES, LIBRARIES, DEPENDENCIES)
by_name = {step.name: step for step in steps}
self.assertEqual(by_name["clippy"].command,
"cargo clippy -p horizon-client --all-targets --locked -- -D warnings")
self.assertEqual(by_name["tests: horizon-client"].command,
"cargo test --locked -p horizon-client --tests")
self.assertEqual(by_name["doc tests"].command, "cargo test --locked -p horizon-client --doc")
self.assertIsNone(by_name["doc tests"].status)
def test_shared_rust_configuration_keeps_workspace_coverage(self):
for path in sorted(pr_ready.ROOT_RUST | {".cargo/config.toml", "crates/unknown/src/lib.rs"}):
with self.subTest(path=path), mock.patch.object(pr_ready, "affected_here", return_value=[]):
steps = pr_ready.plan(args(quick=True), "origin/main", {path}, PACKAGES, LIBRARIES, DEPENDENCIES)
by_name = {step.name: step for step in steps}
self.assertEqual(by_name["clippy"].command,
"cargo clippy --workspace" + MODS + " --all-targets --locked -- -D warnings")
self.assertEqual(by_name["tests: workspace"].command,
"cargo test --locked --workspace" + MODS + " --tests")
self.assertEqual(by_name["doc tests"].command, "cargo test --locked --workspace --doc")
def test_non_rust_changes_skip_workspace_checks(self):
for path in ("README.md", "tools/pr_ready.py", "launcher/src/main.rs"):
with self.subTest(path=path), mock.patch.object(pr_ready, "affected_here", return_value=[]):
steps = pr_ready.plan(args(quick=True), "origin/main", {path}, PACKAGES, LIBRARIES, DEPENDENCIES)
by_name = {step.name: step for step in steps}
for name in ("clippy", "tests", "doc tests"):
self.assertEqual(by_name[name].status, "skip")
def test_multiple_touched_crates_share_the_same_check_scope(self):
with mock.patch.object(pr_ready, "affected_here", return_value=[]):
steps = pr_ready.plan(args(), "origin/main",
{"crates/horizon-core/src/lib.rs", "crates/horizon-client/examples/probe.rs"}, PACKAGES, LIBRARIES, DEPENDENCIES)
for step in steps:
if step.name in ("clippy", "tests: horizon-client, horizon-core", "doc tests"):
self.assertIn("-p horizon-client -p horizon-core", step.command)
self.assertIsNone(step.status)
def test_binary_only_crates_skip_doc_tests(self):
with mock.patch.object(pr_ready, "affected_here", return_value=[]):
steps = pr_ready.plan(args(), "origin/main", {"xtask/src/main.rs"}, PACKAGES, LIBRARIES, DEPENDENCIES)
by_name = {step.name: step for step in steps}
self.assertEqual(by_name["tests: xtask"].command, "cargo test --locked -p xtask --tests")
self.assertIsNone(by_name["tests: xtask"].status)
self.assertEqual(by_name["doc tests"].status, "skip")
with mock.patch.object(pr_ready, "affected_here", return_value=[]):
steps = pr_ready.plan(args(), "origin/main",
{"xtask/src/main.rs", "crates/horizon-core/src/lib.rs"}, PACKAGES, LIBRARIES, DEPENDENCIES)
by_name = {step.name: step for step in steps}
self.assertEqual(by_name["doc tests"].command, "cargo test --locked -p horizon-core --doc")
def test_clippy_checks_transitive_consumers_but_tests_only_touched_crates(self):
packages = {name: f"crates/{name}" for name in ("core", "world", "client", "unrelated")}
dependencies = {"world": {"core"}, "client": {"world"}, "unrelated": set()}
with mock.patch.object(pr_ready, "affected_here", return_value=[]):
steps = pr_ready.plan(args(), "origin/main", {"crates/core/src/lib.rs"},
packages, set(packages), dependencies)
by_name = {step.name: step for step in steps}
self.assertEqual(by_name["clippy"].command,
"cargo clippy -p client -p core -p world --all-targets --locked -- -D warnings")
self.assertEqual(by_name["tests: core"].command, "cargo test --locked -p core --tests")
self.assertEqual(by_name["doc tests"].command, "cargo test --locked -p core --doc")
def test_dependent_scope_handles_cycles_and_multiple_changed_crates(self):
dependencies = {"a": {"b"}, "b": {"a"}, "c": {"b"}, "d": set()}
self.assertEqual(pr_ready.dependent_crates(["a"], dependencies), ["a", "b", "c"])
self.assertEqual(pr_ready.dependent_crates(["a", "d"], dependencies), ["a", "b", "c", "d"])
self.assertEqual(pr_ready.dependent_crates([], dependencies), [])
self.assertIsNone(pr_ready.dependent_crates(None, dependencies))
def test_metadata_includes_renamed_optional_platform_build_and_dev_consumers(self):
core = str(pr_ready.ROOT / "crates/core")
metadata = {"workspace_members": ["core", "optional", "build", "dev"], "packages": [
{"id": "core", "name": "core", "manifest_path": core + "/Cargo.toml",
"targets": [{"kind": ["lib"]}], "dependencies": []},
]}
for name, kind in (("optional", None), ("build", "build"), ("dev", "dev")):
metadata["packages"].append({
"id": name, "name": name, "manifest_path": str(pr_ready.ROOT / f"crates/{name}/Cargo.toml"),
"targets": [{"kind": ["bin"]}], "dependencies": [
{"name": "core", "rename": "alias", "path": core, "kind": kind,
"optional": name == "optional", "target": "cfg(windows)"},
{"name": "external", "path": None},
],
})
# A path dependency outside this workspace must not become a selectable package.
metadata["packages"].append({"id": "outside", "name": "outside", "manifest_path": "/outside/Cargo.toml"})
with mock.patch.object(pr_ready.subprocess, "run", return_value=mock.Mock(stdout=json.dumps(metadata))):
directories, libraries, dependencies = pr_ready.packages()
self.assertEqual(set(directories), {"core", "optional", "build", "dev"})
self.assertEqual(libraries, {"core"})
self.assertEqual(pr_ready.dependent_crates(["core"], dependencies), ["build", "core", "dev", "optional"])
def test_scope_warns_on_size_and_mixed_areas(self):
self.assertEqual(pr_ready.scope_warnings({"crates/horizon-client/src/game.rs", "tools/test_food_parity.py",
"mods/horizon/src/test/java/dev/horizonlab/parity/X.java",
"docs/native-client.md"}, 120), [])
self.assertEqual(pr_ready.scope_warnings({"crates/horizon-theme/src/lib.rs", "launcher/src/theme.rs"}, 10), [])
warnings = pr_ready.scope_warnings({"crates/horizon-client/src/game.rs", "launcher/src/main.rs",
".github/workflows/ci.yml"}, 900)
self.assertEqual(len(warnings), 2)
self.assertIn("900 lines of code: over 500", warnings[0])
self.assertEqual(warnings[1], "mixes CI, game, launcher")
def test_documentation_and_tests_are_not_counted_in_the_size(self):
self.assertTrue(pr_ready.reviewed("tools/pr_ready.py"))
self.assertTrue(pr_ready.reviewed("crates/horizon-client/src/game.rs"))
self.assertFalse(pr_ready.reviewed("CONTRIBUTING.md"))
self.assertFalse(pr_ready.reviewed("tests/test_pr_ready.py"))
self.assertFalse(pr_ready.reviewed("tools/test_food_parity.py"))
self.assertFalse(pr_ready.reviewed("crates/horizon-core/tests/jmath.rs"))
def test_cargo_lock_changes_only_with_a_manifest(self):
self.assertIsNone(pr_ready.lock_problem({"crates/horizon-core/src/lib.rs"}))
self.assertIsNone(pr_ready.lock_problem({"Cargo.lock", "crates/horizon-client/Cargo.toml"}))
self.assertIsNone(pr_ready.lock_problem({"launcher/Cargo.lock", "crates/horizon-theme/Cargo.toml"}))
self.assertIn("Cargo.lock", pr_ready.lock_problem({"Cargo.lock", "crates/horizon-core/src/lib.rs"}))
def test_label_follows_what_players_get(self):
self.assertEqual(pr_ready.suggested_label({"tools/pr_ready.py", "CONTRIBUTING.md"}), "`skip-release`")
self.assertEqual(pr_ready.suggested_label({"crates/horizon-world/examples/probe.rs"}), "`skip-release`")
self.assertIn("feature", pr_ready.suggested_label({"crates/horizon-client/src/game.rs"}))
self.assertIn("feature", pr_ready.suggested_label({"launcher/src/main.rs"}))
def test_affected_harnesses_come_from_the_parity_runner(self):
found = {"food": "tools/test_food_parity.py", "datafix#1": "tools/test_datafix_parity.py",
"datafix#2": "tools/test_datafix_parity.py"}
with mock.patch.object(pr_ready.parity, "harnesses", return_value=found), \
mock.patch.object(pr_ready.parity, "load_state", return_value={"harnesses": {
"food": {"dependencies": ["crates/horizon-world/src/food.rs"]},
"datafix#1": {"dependencies": ["crates/horizon-world/src/datafix.rs"]},
"datafix#2": {"dependencies": ["crates/horizon-world/src/datafix.rs"]}}}), \
mock.patch.object(pr_ready.parity, "ROOT", pr_ready.parity.Path(".")):
self.assertEqual(pr_ready.affected_here({"crates/horizon-world/src/datafix.rs"}), ["datafix"])
self.assertEqual(pr_ready.affected_here({"README.md"}), [])
self.assertEqual(pr_ready.affected_here({"Cargo.lock"}), ["food", "datafix"])
def test_harnesses_are_listed_when_they_cannot_run(self):
changed = {"crates/horizon-world/src/food.rs"}
with mock.patch.object(pr_ready, "affected_here", return_value=["food", "light"]):
steps = pr_ready.plan(args(quick=True), "origin/main", changed, {"horizon-world": "crates/horizon-world"}, {"horizon-world"}, {})
harness = steps[-1]
self.assertEqual(harness.status, "list")
self.assertIn("2 affected, to run in CI (--quick): food, light", harness.detail)
by_name = {step.name: step for step in steps}
self.assertIn("tests: horizon-world", by_name)
self.assertEqual(by_name["launcher clippy"].status, "skip")
self.assertIsNone(by_name["clippy"].status)
with mock.patch.object(pr_ready, "affected_here", return_value=["food"]):
steps = pr_ready.plan(args(host="macmini"), "origin/main", changed, {"horizon-world": "crates/horizon-world"}, {"horizon-world"}, {})
self.assertIn("--changed-files", steps[-1].command)
self.assertIn("crates/horizon-world/src/food.rs", steps[-1].command)
def test_summaries_of_check_output(self):
output = "test result: ok. 3 passed; 0 failed;\ntest result: FAILED. 4 passed; 1 failed;\n"
self.assertEqual(pr_ready.tests_passed(output), "7 passed, 1 failed")
self.assertEqual(pr_ready.unittest_passed("....\nRan 12 tests in 1.0s\n\nOK\n"), "12 passed")
self.assertEqual(pr_ready.unittest_passed("Ran 12 tests in 1.0s\n\nFAILED (failures=2)\n"),
"12 run, FAILED (failures=2)")
parity_output = ("3 files changed: 2 harnesses depend on them, 2 without a pass for their current inputs.\n"
"\n1 passed, 1 failed in 2.0 min.\n light: /x/.local/parity-runner/logs/light.log\n")
self.assertEqual(pr_ready.parity_summary(parity_output), "2 affected, 1 passed, failed: light")
self.assertEqual(pr_ready.parity_summary("2 files changed: 0 harnesses depend on them, 0 without a pass for "
"their current inputs.\nNothing to run: every harness passed."),
"no harness affected")
self.assertEqual(pr_ready.duration(125), "2 min 5 s")
if __name__ == "__main__":
unittest.main()