mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
Scope complete build identity to lifecycle gates
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "349a001593163710a6f498e0569187d3454d140f07cbcd54475c97eaa354841d", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "1c1d910d2f315ee514bcfe3255fabf251cb24c0abbc0af32d5c58ce412d4a4b9", "schema_version": 1}
|
||||
|
||||
@@ -561,7 +561,7 @@ def source_provenance():
|
||||
return subprocess.check_output(["git", "--no-optional-locks", "-c", "gc.auto=0", "-c", "maintenance.auto=false", *args], cwd=ROOT).decode().strip()
|
||||
manifest = ROOT / "upstream-sync.json"
|
||||
return {"revision": git("rev-parse", "HEAD"), "status": git("status", "--porcelain"),
|
||||
"inputs": source_input_snapshot(), "build_identity": lifecycle_shards.build_identity(ROOT),
|
||||
"inputs": source_input_snapshot(),
|
||||
"manifest_sha256": sha256(manifest), "dependencies": json.loads(manifest.read_text())["dependencies"],
|
||||
"runner_sha256": sha256(ROOT / "tools/test_runner.py"), "suite_runner_sha256": sha256(__file__),
|
||||
"pytest_runner_sha256": sha256(ROOT / "tools/ci/vehicle_pytest.py"),
|
||||
@@ -649,16 +649,19 @@ def run(suite, output, collect_only=False, shard_index=None, shard_count=4):
|
||||
try:
|
||||
plan["targets"] = targets_for(suite)
|
||||
plan["source"] = source_provenance()
|
||||
if suite == "aol-lifecycle":
|
||||
plan["source"]["build_identity"] = lifecycle_shards.build_identity(ROOT)
|
||||
plan.update(vehicle_inventory())
|
||||
plan["native"] = native_provenance()
|
||||
if suite != "interfaces" and not collect_only:
|
||||
plan["native"]["safety"] = build_safety_library(suite == "safety-release", output)
|
||||
if suite == "aol-lifecycle" and not collect_only:
|
||||
plan["native"]["aol_protocol"] = build_aol_protocol(output)
|
||||
build_input_digest = hashlib.sha256(json.dumps(plan["source"]["build_identity"]["inputs"], sort_keys=True).encode()).hexdigest()
|
||||
for name in ('safety', 'aol_protocol'):
|
||||
if name in plan["native"]:
|
||||
plan["native"][name]["build_inputs_sha256"] = build_input_digest
|
||||
if suite == "aol-lifecycle":
|
||||
build_input_digest = hashlib.sha256(json.dumps(plan["source"]["build_identity"]["inputs"], sort_keys=True).encode()).hexdigest()
|
||||
for name in ('safety', 'aol_protocol'):
|
||||
if name in plan["native"]:
|
||||
plan["native"][name]["build_inputs_sha256"] = build_input_digest
|
||||
phase = "collection"
|
||||
unittest_targets, pytest_targets = vehicle_pytest.split_targets(ROOT, plan["targets"])
|
||||
plan["runners"] = {"unittest": unittest_targets, "pytest": pytest_targets}
|
||||
@@ -698,6 +701,10 @@ def run(suite, output, collect_only=False, shard_index=None, shard_count=4):
|
||||
errors.extend(pytest_errors)
|
||||
phase = "source verification"
|
||||
plan["source_after"] = source_provenance()
|
||||
if suite == "aol-lifecycle":
|
||||
plan["source_after"]["build_identity"] = lifecycle_shards.build_identity(ROOT)
|
||||
if plan["source"]["build_identity"] != plan["source_after"]["build_identity"]:
|
||||
gate_errors.append("Lifecycle source/build identity changed during the suite")
|
||||
if plan["source"].get("inputs") != plan["source_after"].get("inputs"):
|
||||
gate_errors.append("Vehicle/control source inputs changed during the suite; rerun against stable source")
|
||||
except Exception:
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Keep lifecycle build identity mandatory without imposing its outputs on other gates."""
|
||||
from contextlib import ExitStack, redirect_stdout
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from tools.ci import run_vehicle_tests as vehicle
|
||||
from tools import test_runner
|
||||
|
||||
|
||||
class TestLifecycleProvenanceScope(unittest.TestCase):
|
||||
def test_general_source_provenance_does_not_require_lifecycle_outputs(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / 'upstream-sync.json').write_text('{"dependencies": {}}')
|
||||
for name in ('tools/test_runner.py', 'tools/ci/vehicle_pytest.py', 'tools/ci/lifecycle_shards.py'):
|
||||
path = root / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text('# fixture\n')
|
||||
with patch.object(vehicle, 'ROOT', root), patch.object(vehicle, 'source_input_snapshot', return_value={'original': True}), \
|
||||
patch.object(vehicle.subprocess, 'check_output', return_value=b'fixture'), \
|
||||
patch.object(vehicle.lifecycle_shards, 'build_identity', side_effect=AssertionError('unrelated artifact inventory')) as identity:
|
||||
source = vehicle.source_provenance()
|
||||
self.assertEqual(source['inputs'], {'original': True})
|
||||
self.assertNotIn('build_identity', source)
|
||||
identity.assert_not_called()
|
||||
|
||||
def test_missing_lifecycle_build_identity_still_denies_collection(self):
|
||||
with tempfile.TemporaryDirectory() as directory, \
|
||||
patch.object(vehicle, 'targets_for', return_value=['fixture.py']), \
|
||||
patch.object(vehicle, 'source_provenance', return_value={}), \
|
||||
patch.object(vehicle.lifecycle_shards, 'build_identity', side_effect=ValueError('missing declared artifact')), \
|
||||
patch.object(vehicle.test_runner, 'collect') as collect, redirect_stdout(io.StringIO()):
|
||||
self.assertEqual(vehicle.run('aol-lifecycle', directory, collect_only=True), 1)
|
||||
result = json.loads((Path(directory) / 'results.json').read_text())
|
||||
self.assertEqual(result['error_phase'], 'initialization')
|
||||
self.assertIn('missing declared artifact', result['collection_errors'][0])
|
||||
collect.assert_not_called()
|
||||
|
||||
def test_lifecycle_records_native_input_digest_and_rejects_full_identity_change(self):
|
||||
identity = {'inputs': {'source': 'original'}, 'declared_outputs': {'exact.so': {'sha256': 'a' * 64, 'mode': 420}}}
|
||||
changed = {'inputs': {'source': 'original'}, 'declared_outputs': {'exact.so': {'sha256': 'b' * 64, 'mode': 420}}}
|
||||
for after, expected in ((identity, 0), (changed, 1)):
|
||||
with self.subTest(changed=after != identity), tempfile.TemporaryDirectory() as directory, ExitStack() as stack:
|
||||
test = unittest.FunctionTestCase(lambda: None)
|
||||
stack.enter_context(patch.object(test, 'id', return_value='fixture.Case.test_pass'))
|
||||
for name, value in (('targets_for', ['fixture.py']), ('source_provenance', {'inputs': 'stable'}),
|
||||
('vehicle_inventory', {}), ('native_provenance', {}), ('build_safety_library', {}),
|
||||
('build_aol_protocol', {})):
|
||||
stack.enter_context(patch.object(vehicle, name, side_effect=(lambda *args, value=value: value.copy() if isinstance(value, dict) else value)))
|
||||
stack.enter_context(patch.object(vehicle.lifecycle_shards, 'build_identity', side_effect=[identity, after]))
|
||||
stack.enter_context(patch.object(vehicle.vehicle_pytest, 'split_targets', return_value=(['fixture.py'], [])))
|
||||
stack.enter_context(patch.object(vehicle.vehicle_pytest, 'collect', return_value=([], [])))
|
||||
stack.enter_context(patch.object(vehicle.vehicle_pytest, 'run', return_value=([], [])))
|
||||
stack.enter_context(patch.object(test_runner, 'collect', return_value=([test], [])))
|
||||
stack.enter_context(patch.object(test_runner, 'run_batch', return_value=[test_runner.make_record(test.id())]))
|
||||
stack.enter_context(redirect_stdout(io.StringIO()))
|
||||
self.assertEqual(vehicle.run('aol-lifecycle', directory), expected)
|
||||
coverage = json.loads((Path(directory) / 'coverage.json').read_text())
|
||||
self.assertEqual(coverage['source']['build_identity'], identity)
|
||||
self.assertEqual(coverage['source_after']['build_identity'], after)
|
||||
self.assertEqual(coverage['native']['safety']['build_inputs_sha256'], coverage['native']['aol_protocol']['build_inputs_sha256'])
|
||||
self.assertEqual(len(coverage['native']['safety']['build_inputs_sha256']), 64)
|
||||
self.assertEqual(bool(coverage['gate_errors']), expected == 1)
|
||||
Reference in New Issue
Block a user