diff --git a/tests/test_interaction_lab.py b/tests/test_interaction_lab.py new file mode 100644 index 0000000000..db3c1a7bc5 --- /dev/null +++ b/tests/test_interaction_lab.py @@ -0,0 +1,22 @@ +from pathlib import Path +import unittest +from unittest import mock + +from tools import interaction_lab + + +class CargoExampleTests(unittest.TestCase): + def test_client_probe_uses_the_reusable_library_package(self): + with mock.patch.object(interaction_lab.subprocess, 'run') as run: + interaction_lab.cargo_example('horizon-client', 'menu_fixture', Path('cases.txt'), profile='fast') + run.assert_called_once_with( + ['cargo', 'run', '-q', '-j3', '--profile', 'fast', '-p', 'horizon-client-lib', + '--example', 'client_probe', '--', 'menu_fixture', 'cases.txt'], + check=True, cwd=interaction_lab.lab.ROOT) + + def test_other_probe_packages_are_unchanged(self): + with mock.patch.object(interaction_lab.subprocess, 'run') as run: + interaction_lab.cargo_example('horizon-world', 'loot_probe', 42) + run.assert_called_once_with( + ['cargo', 'run', '-q', '-j3', '-p', 'horizon-world', '--example', 'world_probe', '--', 'loot_probe', '42'], + check=True, cwd=interaction_lab.lab.ROOT) diff --git a/tests/test_parity_reach.py b/tests/test_parity_reach.py index 69709b4427..ed8758e2eb 100644 --- a/tests/test_parity_reach.py +++ b/tests/test_parity_reach.py @@ -138,6 +138,7 @@ class ReachTests(unittest.TestCase): '[[example]]\nname = "client_probe"\npath = "../a/examples/probe/main.rs"\n', "crates/a/src/shared.rs": "pub mod used;\n", "crates/a/src/lib.rs": "pub fn game_only() {}\n", + "crates/a/Cargo.toml": '[package]\nname = "a"\nautobins = false\nautoexamples = false\n', "crates/a/examples/probe/main.rs": "extern crate b as legacy;\nfn main() { legacy::used::helper(); }\n", "crates/b/src/main.rs": "compile_error!(\"disabled auto-bin\");\n", "crates/b/examples/ignored.rs": "compile_error!(\"disabled auto-example\");\n", @@ -148,6 +149,8 @@ class ReachTests(unittest.TestCase): self.assertIn((self.root / "crates/a/examples/probe/main.rs").resolve(), roots) self.assertNotIn((self.root / "crates/b/src/main.rs").resolve(), roots) self.assertTrue(graph.is_library("crates/a/src/shared.rs")) + self.assertEqual(graph.entries([["example", "client_probe", "dev", ["-p", "a"], True]], + {"crates/a/examples/probe/main.rs"}), {"crates/a/examples/probe/main.rs"}) reached = graph.reachable({"crates/a/examples/probe/main.rs"}, {"crates/a/examples/probe/main.rs", "crates/a/src/shared.rs", "crates/a/src/used.rs"}) self.assertIn("crates/a/src/used.rs", reached) diff --git a/tests/test_parity_runner.py b/tests/test_parity_runner.py index ed4128e96f..78c342e88b 100644 --- a/tests/test_parity_runner.py +++ b/tests/test_parity_runner.py @@ -30,6 +30,19 @@ class ParityRunnerTests(unittest.TestCase): self.assertEqual(parity.artifacts_of(["cargo", "build", "--release", "-p", "horizon-world", "--bin=horizon-world"]), [["bin", "horizon-world", "release", ["-p", "horizon-world"], False]]) + def test_prebuild_follows_a_moved_target_without_losing_features(self): + selection = ["-p", "horizon-client", "--no-default-features", "--features", "horizon-client/rng-trace"] + artifacts = [["example", "client_probe", "fast", selection, True], + ["bin", "horizon-ctl", "fast", selection, False]] + packages = {("example", "client_probe"): "horizon-client-lib", ("bin", "horizon-ctl"): "horizon-client-lib"} + commands = parity.prebuild_commands(artifacts, packages) + self.assertEqual(len(commands), 2) + for command in commands: + self.assertEqual(command[:5], ["cargo", "build", "--locked", "--profile", "fast"]) + self.assertEqual(command[5:5 + len(selection)], selection) + self.assertEqual(command[5 + len(selection):7 + len(selection)], ["-p", "horizon-client-lib"]) + self.assertEqual(artifacts[0][3], selection) + def test_prebuild_repeats_each_harness_selection(self): artifacts = [ *parity.artifacts_of(["cargo", "build", "--release", "-p", "horizon-world", "--example", "a"]), @@ -578,5 +591,16 @@ class WorkspaceManifestTests(unittest.TestCase): optional = {"crates/horizon-client/Cargo.toml": self.CLIENT + '\n[features]\nfast = ["wgpu/x"]\n'} self.assertEqual(self.verdict({"crates/horizon-client/Cargo.toml": self.CLIENT}, optional), set()) + def test_external_feature_sources_are_not_discarded(self): + with tempfile.TemporaryDirectory() as temp, mock.patch.object(parity, "ROOT", Path(temp)): + root = Path(temp) + (root / "crates/core").mkdir(parents=True) + (root / "shared").mkdir() + (root / "crates/core/Cargo.toml").write_text( + '[package]\nname = "core"\n[lib]\npath = "../../shared/lib.rs"\n' + '[features]\nextra = []\n') + (root / "shared/lib.rs").write_text('#[cfg(feature = "extra")]\nfn extra() {}\n') + self.assertFalse(parity.feature_sources_changed("core", "extra", {"crates/core/Cargo.toml"})) + def test_without_a_base_everything_counts(self): self.assertEqual(parity.workspace_only_manifests({"Cargo.lock"}, None), set()) diff --git a/tools/interaction_lab.py b/tools/interaction_lab.py index 894a6d1c4d..7d8c278a53 100644 --- a/tools/interaction_lab.py +++ b/tools/interaction_lab.py @@ -51,6 +51,8 @@ def cargo_example(package, example, *args, profile=None): if profile: command += ['--profile', profile] probe = package.removeprefix('horizon-') + '_probe' # each crate's probe example: client_probe, world_probe + if package == 'horizon-client': + package = 'horizon-client-lib' command += ['-p', package, '--example', probe, '--', example, *map(str, args)] subprocess.run(command, check=True, cwd=lab.ROOT) diff --git a/tools/parity.py b/tools/parity.py index 66714b4ac0..7fdc50e1df 100644 --- a/tools/parity.py +++ b/tools/parity.py @@ -644,6 +644,11 @@ def prebuild_commands(artifacts, packages=None): if len(artifact) < 5: continue kind, name, profile, selection, examples = artifact + owner = (packages or {}).get((kind, name)) + selected = [value for flag, value in zip(selection, selection[1:]) if flag == "-p"] + if owner and selected and owner not in selected: + # A recorded target may have moved packages; retain its feature flags and old selections. + selection = [*selection, "-p", owner] targets = groups.setdefault((profile, tuple(selection), examples), []) target = [f"--{kind}", name] if name else [f"--{kind}"] if target not in targets: @@ -1351,6 +1356,9 @@ def feature_sources_changed(crate, feature, changed): break else: return False + if any(not root.is_relative_to(manifest.parent.resolve()) for root in parity_reach.crate_roots(manifest.parent)): + # A directory-only scan cannot prove that shared external sources changed too. + return False table = re.search(r"^\[features\]\n(.*?)(?=^\[|\Z)", text, re.M | re.S) implies = {name: re.findall(r'"([^"]+)"', value) for name, value in re.findall(r'^([\w-]+)\s*=\s*\[([^\]]*)\]', table.group(1) if table else "", re.M)} diff --git a/tools/parity_reach.py b/tools/parity_reach.py index 166964659b..6b1da9b99f 100644 --- a/tools/parity_reach.py +++ b/tools/parity_reach.py @@ -260,6 +260,11 @@ class SourceGraph: packages = [value for flag, value in zip(selection, selection[1:]) if flag == "-p"] or \ [manifest.parent.name for manifest in sorted((self.root / "crates").glob("*/Cargo.toml"))] roots = set() + if name and not any(name in self.targets(self.root / "crates" / package) for package in packages): + owners = [manifest.parent.name for manifest in (self.root / "crates").glob("*/Cargo.toml") + if self.targets(manifest.parent).get(name, (None,))[0] == kind.rstrip("s")] + if len(owners) == 1: + packages = [*packages, owners[0]] for package in packages: crate = self.root / "crates" / package targets = self.targets(crate)