This is an automated email from the ASF dual-hosted git repository. striker pushed a commit to branch striker/speculative-actions in repository https://gitbox.apache.org/repos/asf/buildstream.git
commit fee64aa290a202f280214ab734bae68320b7dd89 Author: Sander Striker <[email protected]> AuthorDate: Sat Mar 21 23:14:31 2026 +0100 speculative-actions: Add tests for tiered mode system Add 6 tests verifying each mode produces the correct overlay types and makes the expected number of AC calls: - source-artifact mode: no ACTION overlays, zero AC calls - intra-element mode: ACTION overlays for within-element chains only, AC calls limited to own subactions (no dep seeding) - full mode: cross-element ACTION overlays from dep subactions - backward-compat: enum values exist and are distinct - AC call counting: verifies source-artifact makes 0 calls, intra-element makes exactly N calls (one per subaction) Also updated FakeArtifactCache to support lookup by artifact identity (used by _seed_dependency_outputs for cross-element ACTION overlays). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --- .../test_pipeline_integration.py | 262 ++++++++++++++++++++- 1 file changed, 260 insertions(+), 2 deletions(-) diff --git a/tests/speculative_actions/test_pipeline_integration.py b/tests/speculative_actions/test_pipeline_integration.py index afb0c8281..4869a78ef 100644 --- a/tests/speculative_actions/test_pipeline_integration.py +++ b/tests/speculative_actions/test_pipeline_integration.py @@ -257,11 +257,15 @@ class FakeArtifactCache: def __init__(self, cas, basedir): self.cas = cas self._basedir = basedir + self._by_artifact = {} # id(artifact) -> SpeculativeActions def store_speculative_actions(self, artifact, spec_actions, weak_key=None): # Store proto in CAS spec_actions_digest = self.cas.store_proto(spec_actions) + # Store by artifact identity (for get_speculative_actions without weak_key) + self._by_artifact[id(artifact)] = spec_actions + # Store weak key reference if weak_key: element = artifact._element @@ -273,7 +277,9 @@ class FakeArtifactCache: f.write(spec_actions.SerializeToString()) def get_speculative_actions(self, artifact, weak_key=None): - if weak_key: + if weak_key is not None: + if not weak_key: + return None element = artifact._element project = element._get_project() sa_ref = "{}/{}/speculative-{}".format(project.name, element.name, weak_key) @@ -283,7 +289,11 @@ class FakeArtifactCache: with open(sa_ref_path, mode="r+b") as f: spec_actions.ParseFromString(f.read()) return spec_actions - return None + return None + + # No weak_key provided: lookup by artifact identity + # (used by _seed_dependency_outputs which passes just the artifact) + return self._by_artifact.get(id(artifact)) # --------------------------------------------------------------------------- @@ -1335,3 +1345,251 @@ class TestCrossElementActionOverlays: new_root = cas.fetch_directory_proto(new_action.input_root_digest) assert new_root.files[0].name == "intermediate.h" assert new_root.files[0].digest.hash == new_digest.hash + + +# --------------------------------------------------------------------------- +# Speculative action mode tests +# --------------------------------------------------------------------------- + +class TestSpeculativeActionModes: + """Tests verifying that each mode generates the correct overlay types.""" + + def _build_compile_link_scenario(self, cas, ac_service): + """Build a compile→link scenario with source, artifact, and action overlays. + + Returns (element, dep_element, subaction_digests, dependencies) + """ + app_src = b'int main() { return dep(); }' + dep_header = b'int dep(void);' + main_o = b'main-object-code' + main_o_digest = _make_digest(main_o) + + source_root = _build_source_tree(cas, {"main.c": app_src}) + sources = FakeSources(FakeSourceDir(source_root)) + + dep_artifact_root = _build_source_tree(cas, {"include/dep.h": dep_header}) + dep_artifact = FakeArtifact(FakeSourceDir(dep_artifact_root)) + dep_element = FakeElement("dep.bst", artifact=dep_artifact) + + element = FakeElement("app.bst", sources=sources) + + # Compile: uses main.c + dep.h, produces main.o + compile_input = _build_source_tree(cas, { + "main.c": app_src, + "include/dep.h": dep_header, + }) + compile_digest = _build_action(cas, compile_input) + + compile_result = remote_execution_pb2.ActionResult() + out = compile_result.output_files.add() + out.path = "main.o" + out.digest.CopyFrom(main_o_digest) + ac_service.store_action_result(compile_digest, compile_result) + + # Link: uses main.o (output of compile) + link_input = _build_source_tree(cas, {"main.o": main_o}) + link_digest = _build_action(cas, link_input) + + return element, dep_element, [compile_digest, link_digest], [dep_element] + + def test_source_artifact_mode_no_action_overlays(self, tmp_path): + """source-artifact mode should produce only SOURCE and ARTIFACT overlays.""" + from buildstream.types import _SpeculativeActionMode + + cas = FakeCAS() + ac_service = FakeACService() + + element, dep_element, subaction_digests, dependencies = \ + self._build_compile_link_scenario(cas, ac_service) + + generator = SpeculativeActionsGenerator(cas, ac_service=ac_service) + spec_actions = generator.generate_speculative_actions( + element, subaction_digests, dependencies, + mode=_SpeculativeActionMode.SOURCE_ARTIFACT, + ) + + # Should have spec_actions for subactions with SOURCE/ARTIFACT overlays + assert len(spec_actions.actions) >= 1 + + # No ACTION overlays should exist in any spec_action + for sa in spec_actions.actions: + for overlay in sa.overlays: + assert overlay.type != speculative_actions_pb2.SpeculativeActions.Overlay.ACTION, \ + "source-artifact mode should not produce ACTION overlays" + + def test_intra_element_mode_has_action_overlays(self, tmp_path): + """intra-element mode should produce ACTION overlays for within-element chains.""" + from buildstream.types import _SpeculativeActionMode + + cas = FakeCAS() + ac_service = FakeACService() + + element, dep_element, subaction_digests, dependencies = \ + self._build_compile_link_scenario(cas, ac_service) + + generator = SpeculativeActionsGenerator(cas, ac_service=ac_service) + spec_actions = generator.generate_speculative_actions( + element, subaction_digests, dependencies, + mode=_SpeculativeActionMode.INTRA_ELEMENT, + ) + + # Should have 2 spec_actions (compile + link) + assert len(spec_actions.actions) == 2 + + # The link action should have an ACTION overlay for main.o + link_sa = spec_actions.actions[1] + action_overlays = [ + o for o in link_sa.overlays + if o.type == speculative_actions_pb2.SpeculativeActions.Overlay.ACTION + ] + assert len(action_overlays) == 1 + assert action_overlays[0].source_path == "main.o" + + # ACTION overlays should be intra-element only (source_element empty) + for sa in spec_actions.actions: + for overlay in sa.overlays: + if overlay.type == speculative_actions_pb2.SpeculativeActions.Overlay.ACTION: + assert overlay.source_element == "", \ + "intra-element mode should not produce cross-element ACTION overlays" + + def test_full_mode_has_cross_element_action_overlays(self, tmp_path): + """full mode should produce cross-element ACTION overlays from dep subactions.""" + from buildstream.types import _SpeculativeActionMode + + cas = FakeCAS() + ac_service = FakeACService() + artifactcache = FakeArtifactCache(cas, str(tmp_path)) + + # dep element has a subaction that produces intermediate.h + dep_intermediate = b'/* generated header */' + dep_intermediate_digest = _make_digest(dep_intermediate) + + dep_compile_input = _build_source_tree(cas, {"gen.c": b'void gen() {}'}) + dep_compile_digest = _build_action(cas, dep_compile_input) + + dep_result = remote_execution_pb2.ActionResult() + out = dep_result.output_files.add() + out.path = "intermediate.h" + out.digest.CopyFrom(dep_intermediate_digest) + ac_service.store_action_result(dep_compile_digest, dep_result) + + # Create dep artifact and store dep SA on it + dep_element_obj = FakeElement("dep.bst") + dep_artifact = FakeArtifact(element=dep_element_obj) + + dep_sa = speculative_actions_pb2.SpeculativeActions() + dep_spec = dep_sa.actions.add() + dep_spec.base_action_digest.CopyFrom(dep_compile_digest) + artifactcache.store_speculative_actions(dep_artifact, dep_sa) + + # Current element uses intermediate.h in its compile input + source_root = _build_source_tree(cas, {"main.c": b'#include "intermediate.h"'}) + sources = FakeSources(FakeSourceDir(source_root)) + element = FakeElement("app.bst", sources=sources) + + compile_input = _build_source_tree(cas, { + "main.c": b'#include "intermediate.h"', + "intermediate.h": dep_intermediate, + }) + compile_digest = _build_action(cas, compile_input) + + # dep_element must use the SAME artifact object so + # get_speculative_actions finds the stored SA + dep_element_obj._artifact = dep_artifact + dep_element = dep_element_obj + + generator = SpeculativeActionsGenerator( + cas, ac_service=ac_service, artifactcache=artifactcache + ) + spec_actions = generator.generate_speculative_actions( + element, [compile_digest], [dep_element], + mode=_SpeculativeActionMode.FULL, + ) + + assert len(spec_actions.actions) == 1 + + # Should have a cross-element ACTION overlay for intermediate.h + action_overlays = [ + o for o in spec_actions.actions[0].overlays + if o.type == speculative_actions_pb2.SpeculativeActions.Overlay.ACTION + ] + assert len(action_overlays) == 1 + assert action_overlays[0].source_element == "dep.bst" + assert action_overlays[0].source_path == "intermediate.h" + + def test_mode_backward_compat_bool(self): + """Boolean True/False should map to full/none modes.""" + from buildstream.types import _SpeculativeActionMode + + # Verify enum values exist and are distinct + assert _SpeculativeActionMode.NONE.value == "none" + assert _SpeculativeActionMode.PRIME_ONLY.value == "prime-only" + assert _SpeculativeActionMode.SOURCE_ARTIFACT.value == "source-artifact" + assert _SpeculativeActionMode.INTRA_ELEMENT.value == "intra-element" + assert _SpeculativeActionMode.FULL.value == "full" + + def test_source_artifact_mode_fewer_ac_calls(self, tmp_path): + """source-artifact mode should make zero AC calls during generation.""" + from buildstream.types import _SpeculativeActionMode + + cas = FakeCAS() + + # Use a counting AC service to verify zero calls + class CountingACService: + def __init__(self): + self.call_count = 0 + self._results = {} + def store_action_result(self, action_digest, action_result): + self._results[action_digest.hash] = action_result + def GetActionResult(self, request): + self.call_count += 1 + return self._results.get(request.action_digest.hash) + + ac_service = CountingACService() + + element, dep_element, subaction_digests, dependencies = \ + self._build_compile_link_scenario(cas, ac_service) + + # source-artifact mode: generator should NOT use ac_service + generator = SpeculativeActionsGenerator(cas, ac_service=ac_service) + spec_actions = generator.generate_speculative_actions( + element, subaction_digests, dependencies, + mode=_SpeculativeActionMode.SOURCE_ARTIFACT, + ) + + assert ac_service.call_count == 0, \ + f"source-artifact mode should make 0 AC calls, got {ac_service.call_count}" + + def test_intra_element_mode_limited_ac_calls(self, tmp_path): + """intra-element mode should only make AC calls for own subactions.""" + from buildstream.types import _SpeculativeActionMode + + cas = FakeCAS() + + class CountingACService: + def __init__(self): + self.call_count = 0 + self._results = {} + def store_action_result(self, action_digest, action_result): + self._results[action_digest.hash] = action_result + def GetActionResult(self, request): + self.call_count += 1 + return self._results.get(request.action_digest.hash) + + ac_service = CountingACService() + + element, dep_element, subaction_digests, dependencies = \ + self._build_compile_link_scenario(cas, ac_service) + + # intra-element mode: should call AC for own subactions only + # (2 subactions = 2 _record_subaction_outputs calls) + generator = SpeculativeActionsGenerator(cas, ac_service=ac_service) + spec_actions = generator.generate_speculative_actions( + element, subaction_digests, dependencies, + mode=_SpeculativeActionMode.INTRA_ELEMENT, + ) + + # Should be exactly N calls for N subactions (no dep seeding) + assert ac_service.call_count == len(subaction_digests), \ + f"intra-element mode should make {len(subaction_digests)} AC calls " \ + f"(one per own subaction), got {ac_service.call_count}"
