Files
rustfs/scripts/test_inspect_validation_runner.py
T
cxymds 4a63e53161 test(validation): run canonical cases and build pinned Heal tools (#8455)
* test(validation): add pinned auto-testing manual lane

* Install pinned ILM client for canonical validation lane

* Prepare Python client for pool validation cases

* Install validation client for replication cases

* test(validation): provide the Python client for Heal cases

* test(validation): build the candidate-bound Heal inspector artifact

* test(validation): expose tool artifacts through the existing manual lane

* test(validation): verify pinned Heal tools before fleet execution

* test(validation): verify Heal tools against released shard captures

* ci: audit retained validation environments before finalization

* test(validation): add optional native Heal regression tooling

* test(validation): route native tools through the existing driver

* Run validation tool resolution on hosted Linux without acquiring the fleet lock

* test(validation): bind C07 to pinned native authority evidence

* test(validation): build optional C10 fixture codec on hosted Linux

* test(ci): bind C10 execution to the verified shared codec

* feat(ci): expose read-only validation fleet inventory

* docs(ci): describe supported validation recovery

* test(ci): add isolated manager native validation builds

* test(ci): expose manager regressions through tools-only dispatch

* ci(validation): consume pinned manager native prerequisites

* fix(heal): preserve typed bucket errors for bounded retries

* fix(heal): preserve typed errors and SSE-C response headers

* ci(validation): publish isolated immutable candidate packages

* test(validation): enforce private candidate manifest schema

* test(validation): verify isolated package publication failures

* ci(validation): dispatch isolated packages through the driver

* fix(heal): deeply verify read repair payload failures

* test(ecstore): verify incarnation fence at repair commit

* ci(validation): verify C11 native storage evidence

* fix(ci): isolate validation package publication credentials

* test(ci): archive scoped F07 directory fsync evidence

* fix(ecstore): retain typed quorum errors for missing parts

* chore(ecstore): shrink typed-error ratchet baseline

* fix(ecstore): retain below-quorum data during no-remove heal

* fix(ci): allow recovery of completed cancelled validation runs

* fix(heal): retain exhausted admin batch repair receipts

* fix(observability): retain typed object error diagnostics

* fix(s3): map typed RPC unavailability to service refusal

* fix(filemeta): classify supported inline framing damage as corrupt

* test(ecstore): exercise heal during committed overwrite cleanup

* ci(validation): admit source-bound G01 native evidence before fleet execution

* test(ecstore): fence version races and bind cleanup slots

* ci(validation): require complete race native runtime preflight

* test(ecstore): fence marker and batch deletes during heal

* ci(validation): require G04 native and runtime admission

* test(ecstore): fence multipart continue complete and abort during heal

* test(ecstore): cover default heal owners and copy metadata races

* ci(validation): admit source-bound owner and copy race proofs

* test(scanner): assert typed corruption after metadata truncation

* fix(ci): require complete native admission for multipart races

* test(validation): wire I05 queued-successor native prerequisites

* fix(scanner): bind object heals to the scanning disk scope

* test(scanner): reuse storage boundary for scope regression

* style: format RPC digest precedence assertion

* fix(heal): query automatic tasks on their owning node

* test(admin): reuse error facade in heal query regression

* fix(heal): retain cancellation after successful bucket storage I/O

* fix(ci): retain validation evidence outside runner temp

* fix(heal): retain resolved object identity on terminal failures

* test(scanner): adapt scope regression to admission receipts

* ci(validation): inspect shared runner without fleet access

* ci(validation): prepare Python before creating client environments

* ci(validation): isolate each client virtual environment

* ci(validation): retain interrupted evidence without fleet recovery

* ci(validation): bind recovery to retained runner evidence

* ci(validation): wire persistence and lock source plans

* fix(validation): align read-only dispatch defaults

---------

Co-authored-by: Chris <anzhengchao@gmail.com>
2026-10-29 08:50:42 +08:00

124 lines
7.3 KiB
Python

"""Runner diagnostics must remain read-only when the shared fleet is unavailable."""
import json
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from unittest.mock import patch
import inspect_validation_runner as inspection
from check_test_wiring import yaml_block
class RunnerInspectionTests(unittest.TestCase):
def request(self):
return {"validation_runner_inspect_only": True, "validation_case": "",
"validation_tools_only": False, "i05_diagnostic_branch": "source-plan",
"validation_tools_run_attempt": "1"}
def test_workflow_default_allows_inspection_request(self):
workflow = Path(__file__).resolve().parents[1] / ".github/workflows/rustfs-functional-chain.yml"
block = yaml_block(workflow.read_text().splitlines(), "i05_diagnostic_branch", 6)
default = next(line.split(":", 1)[1].strip() for line in block if line.strip().startswith("default:"))
inspection.validate_request({**self.request(), "i05_diagnostic_branch": default})
def test_exclusive_request_rejects_every_execution_mode_before_api_access(self):
inspection.validate_request(self.request())
for key, value in [("validation_runner_inspect_only", False),
("validation_case", "environment.inspect"), ("build_run_id", "123"),
("validation_package_sha", "a" * 40), ("validation_tools_only", True),
("validation_native_tools", True), ("validation_shared_tools", True),
("validation_manager_tools", True), ("validation_cleanup_run_id", "123"),
("auto_testing_sha", "a" * 40), ("validation_tools_run_id", "123"),
("i05_diagnostic_branch", "queued-successor"),
("i05_diagnostic_branch", "timeout"),
("validation_tools_run_attempt", "2")]:
with self.subTest(key=key), self.assertRaises(ValueError):
inspection.validate_request({**self.request(), key: value})
def fixture(self, runners, *, unknown_jobs=False):
def api(path):
self.assertTrue(path.startswith("repos/rustfs/rustfs/actions/"))
if "/runners?" in path:
return {"runners": runners}
if "/runs?status=queued&" in path:
return {"workflow_runs": [{"id": 123, "name": "RustFS Functional Chain",
"status": "queued", "created_at": "2026-10-26T00:00:00Z", "html_url": "run-url"}]}
if "/runs?status=" in path:
return {"workflow_runs": []}
self.assertIn("/runs/123/jobs?", path)
return {"jobs": [] if unknown_jobs else [
{"id": 456, "name": "validation", "status": "queued", "runner_name": "",
"labels": ["smoke-testing"], "html_url": "job-url", "steps": [{"secret": "excluded"}]},
{"id": 457, "name": "native", "status": "in_progress", "labels": ["ubuntu-latest"]},
{"id": 458, "name": "prior", "status": "completed", "labels": ["smoke-testing"]}]}
return api
def test_offline_idle_busy_and_unregistered_are_distinct_with_only_fleet_jobs(self):
runner = {"id": 7, "name": "rustfs-smoke-testing", "status": "offline", "busy": False,
"labels": [{"name": "smoke-testing"}], "extra": {"secret": "excluded"}}
for state, busy, expected in [("offline", False, "offline"), ("online", True, "online-busy"),
("online", False, "online-idle"), (None, False, "unregistered")]:
with self.subTest(expected=expected):
rows = [{**runner, "status": state, "busy": busy}] if state else []
report = {}
with patch.object(inspection, "api", side_effect=self.fixture(rows)):
inspection.collect(report)
self.assertEqual(report["runnerAvailability"], expected)
self.assertTrue(report["complete"])
self.assertEqual([row["jobId"] for row in report["fleetJobs"]], [456])
self.assertNotIn("secret", json.dumps(report))
def test_jobless_workflow_is_retained_without_inventing_a_runner_assignment(self):
report = {}
with patch.object(inspection, "api", side_effect=self.fixture([], unknown_jobs=True)):
inspection.collect(report)
self.assertEqual(report["fleetJobs"], [])
self.assertEqual(report["runsWithoutJobs"][0]["runId"], 123)
def test_inventory_pagination_is_bounded_and_cannot_claim_complete_when_truncated(self):
row = {"id": 7}
with patch.object(inspection, "api", return_value={"runners": [row] * 100}) as api:
with self.assertRaisesRegex(ValueError, "bounded inspection"):
inspection.pages("repos/rustfs/rustfs/actions/runners", "runners")
self.assertEqual(api.call_count, 10)
def test_api_failure_keeps_partial_report_without_subprocess_diagnostics_or_fleet_claims(self):
for inputs, expected_calls in [(self.request(), 1), ({**self.request(), "validation_tools_only": True}, 0)]:
with self.subTest(inputs=inputs), tempfile.TemporaryDirectory() as directory:
output = Path(directory) / "report.json"
with patch.dict(os.environ, RUNNER_INSPECTION_INPUTS=json.dumps(inputs)), \
patch.object(inspection.sys, "argv", ["inspect", "--output", str(output)]), \
patch.object(inspection, "api", side_effect=subprocess.CalledProcessError(
1, ["gh", "api"], stderr=b"secret-token")) as api:
self.assertEqual(inspection.main(), 1)
report = json.loads(output.read_text())
self.assertFalse(report["complete"])
for field in ("mutated", "fleetAccessed", "cleanStateCertified",
"sharedWorkflowIdleCertified", "canonicalCasePassClaimed"):
self.assertIs(report[field], False)
self.assertNotIn("secret-token", output.read_text())
self.assertEqual(api.call_count, expected_calls)
def test_workflow_is_hosted_and_uses_a_separate_concurrency_group(self):
workflow = Path(__file__).resolve().parents[1] / ".github/workflows/rustfs-functional-chain.yml"
source = workflow.read_text()
job = source.split(" validation-runner-inspection:\n", 1)[1].split(" validation-package-request:", 1)[0]
self.assertIn("runs-on: ubuntu-latest", job)
self.assertIn("timeout-minutes: 10", job)
self.assertIn("RUNNER_INSPECTION_INPUTS: ${{ toJSON(inputs) }}", job)
self.assertIn("if: ${{ always() }}", job)
self.assertNotIn("RUSTFS_SECRET_KEY", job)
concurrency = source.split("concurrency:\n", 1)[1].split("jobs:\n", 1)[0]
self.assertLess(concurrency.index("validation_runner_inspect_only"), concurrency.index("validation_package_sha"))
self.assertIn("rustfs-functional-validation-runner-inspection", concurrency)
for name in ("validation-package-request", "validation-package", "prepare", "validation"):
job = source.split(" " + name + ":\n", 1)[1].split("\n runs-on:", 1)[0].split("\n uses:", 1)[0]
self.assertIn("!inputs.validation_runner_inspect_only", job)
if __name__ == "__main__":
unittest.main()