Files
rustfs/scripts/test_validation_registry.py
T
hector e494dc2b4b ci(validation): sweep the migrated canonical validation cases nightly (#9107)
* ci(validation): sweep the migrated canonical validation cases nightly

The canonical release-validation cases migrated into auto-testing by
rustfs/auto-testing#113 (66 implemented, all non-heal) currently run
only through the manual validation flow; the last fleet executions
stopped on 2026-10-27 and nothing schedules them. Add a scheduled
validation sweep that closes the gap:

- nightly at 19:30 UTC (after the functional chain settles), rotating
  windows of the implemented catalog cases so the whole catalog is
  covered roughly weekly, with a bounded batch per night
- workflow_dispatch for a single case (--case), one module (--module),
  or an explicit batch (--limit), mirroring how the lanes are testable
  standalone
- fleet admission through functional_environment enter/finish with the
  same quarantine discipline as the lanes, inter-case node cleanup,
  per-case logs + reports retained as a sweep artifact
- a markdown sweep report uploaded to rustfs/dashboard under
  functional-reports/validation/ so the cases appear next to the lane
  reports, plus an alert-on-failure job for scheduled runs
- heal.* cases stay on the interactive validation flow: many need the
  reviewed native/tool collectors built by separate validation-tools
  runs before fleet execution

The registry gains the workflow responsibility (monitor: workflow,
max-age 60h so the watchdog notices a dead sweep) and the wiring test
baseline count moves with it.

Verified locally: check_wiring, check_test_wiring, test_validation_registry
(9/9), test_functional_chain + test_functional_chain_health +
test_security_workflow (99/99).

* ci(validation): group the sweep report by catalog module

The validation cases cover distinct areas (pool lifecycle, ILM
expiration, bucket/site replication, kms, data services, identity,
s3 compatibility), so a flat case table hides the category breakdown.
Render one section per catalog module with its own case table and a
Subtotal line, and keep the single grand - Total:/- PASS:/- FAIL:
block for the dashboard overview parser. Subtotals deliberately use
a different bullet prefix so the overview regexes cannot mistake a
module subtotal for the grand total.

* ci(validation): sweep the eight planned cases whose executors exist

The planned catalog entries are not all unimplemented: all eight
(kms.sse-kms-round-trip, tiering.transition-and-restore,
events.delivery, audit.request-correlation, iam.policy-enforcement,
oidc.login-and-session, s3.basic-object-round-trip and
scanner.usage-convergence) have dedicated executors routed in
run_validation_case.sh and live PASS evidence recorded in the
migration map. Widen the sweep filter to include them: the rotating
nightly batch now covers 74 cases (66 implemented + these 8), and the
fresh evidence each sweep produces feeds their acceptance review.
heal.* procedure-ready cases remain excluded - their executors need
the reviewed native/tool collectors built by separate
validation-tools runs before fleet execution.

---------

Co-authored-by: Hauser Zhi <housemecn@gmail.com>
2026-11-01 21:53:00 +08:00

159 lines
8.6 KiB
Python

#!/usr/bin/env python3
"""Selection, ownership and environment failure boundaries for periodic suites."""
import copy
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest import mock
import functional_environment as environment
import functional_chain_evidence as evidence
import validation_registry as registry
class RegistryTests(unittest.TestCase):
def setUp(self):
self.registry = registry.load_registry()
def test_all_workflow_responsibilities_and_suite_exits_are_wired(self):
self.assertTrue(registry.check_wiring(self.registry))
self.assertEqual(len(self.registry["suites"]), 12)
self.assertEqual(sum(item["origin"] == "baseline" for item in self.registry["workflows"]), 61)
self.assertEqual(len(self.registry["workflows"]), len(list((registry.ROOT / ".github/workflows").glob("*.yml"))))
def test_manual_and_periodic_plan_choose_identical_responsibilities(self):
for cadence in ("nightly", "weekly", "monthly"):
automatic = registry.select_plan(self.registry, cadence, event="schedule")
manual = registry.select_plan(self.registry, cadence, event="workflow_dispatch")
self.assertEqual(automatic, manual)
self.assertEqual(automatic["timezone"], "UTC")
self.assertTrue(all(registry.workflow_entry(path)["policy"] == "active"
for path in automatic["periodic_workflows"]))
def test_single_suite_has_no_implicit_successor_or_optional_escape(self):
for suite in registry.suite_ids():
plan = registry.select_plan(self.registry, requested=suite)
self.assertEqual(plan["suites"], [suite])
self.assertEqual(plan["optional_suites"], [])
for invalid in ("s3,", "s3,s3", "unknown", " s3", "s3, kms"):
with self.assertRaises(ValueError):
registry.select_plan(self.registry, requested=invalid)
with self.assertRaises(ValueError):
registry.select_plan(self.registry, requested="s3", event="schedule")
def test_disabled_and_cyclic_responsibilities_cannot_be_scheduled(self):
value = copy.deepcopy(self.registry)
path = value["suites"][0]["workflow"]
registry.workflow_entry(path, value)["policy"] = "disabled"
with self.assertRaisesRegex(ValueError, "disabled"):
registry.select_plan(value)
value = copy.deepcopy(self.registry)
value["suites"][0]["dependencies"] = ["s3"]
value["suites"][1]["dependencies"] = ["upgrade"]
with self.assertRaisesRegex(ValueError, "cyclic"):
registry.select_plan(value)
def test_watchdog_uses_registry_scope_source_and_alert_ownership(self):
item = registry.workflow_entry(".github/workflows/audit.yml")
event = {"repository": {"full_name": "rustfs/rustfs", "default_branch": "main"},
"workflow_run": {"path": item["workflow"], "name": item["name"], "event": "schedule",
"head_branch": "main", "head_repository": {"full_name": "rustfs/rustfs"},
"conclusion": "cancelled"}}
self.assertTrue(registry.watchdog_event(event))
for field, value in (("event", "workflow_dispatch"), ("head_branch", "other"),
("path", ".github/workflows/cla.yml"), ("conclusion", "success"),
("conclusion", "failure"), ("head_repository", {"full_name": "other/repo"})):
changed = copy.deepcopy(event)
changed["workflow_run"][field] = value
self.assertFalse(registry.watchdog_event(changed))
disabled = copy.deepcopy(self.registry)
registry.workflow_entry(item["workflow"], disabled)["policy"] = "disabled"
self.assertFalse(registry.watchdog_event(event, disabled))
class SelectionEvidenceTests(unittest.TestCase):
def test_nonfirst_single_suite_is_verified_and_extraneous_results_are_rejected(self):
chain = {"run_id": 1, "attempt": 2}
plan = registry.select_plan(registry.load_registry(), requested="kms")
needs = {name: {"result": "success" if name == "kms" else "skipped"} for name in evidence.SUITES}
record = {"schema": 1, "suite": "kms", "chain": chain, "valid": True, "report_sha256": "a" * 64,
"counts": {"PASS": 1, "FAIL": 0, "RUNNING": 0, "UNSUPPORTED": 0, "SKIP": 0}}
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
(root / "kms.json").write_text(json.dumps(record))
result = evidence.aggregate(chain, root, needs, plan=plan)
self.assertEqual([item["suite"] for item in result["suites"]], ["kms"])
for status in ("failure", "cancelled", "skipped", "missing"):
with self.assertRaises(ValueError):
evidence.aggregate(chain, root, {**needs, "kms": {"result": status}}, plan=plan)
with self.assertRaises(ValueError):
evidence.aggregate(chain, root, {**needs, "s3": {"result": "success"}}, plan=plan)
with self.assertRaises(ValueError):
evidence.aggregate(chain, root, needs, allow_skipped=["kms"], plan=plan)
record["chain"]["attempt"] = 1
(root / "kms.json").write_text(json.dumps(record))
with self.assertRaises(ValueError):
evidence.aggregate({"run_id": 1, "attempt": 2}, root, needs, plan=plan)
class EnvironmentTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.env = {"GITHUB_RUN_ID": "123", "GITHUB_RUN_ATTEMPT": "1", "RUSTFS_NODES": "node1 node2",
"FUNCTIONAL_ENVIRONMENT_RECEIPT": str(self.root / "receipt.json")}
def execute(self, node, user, mode, identity):
import base64
home = self.root / node
home.mkdir(exist_ok=True)
encoded = base64.b64encode(json.dumps(identity).encode()).decode()
subprocess.run([sys.executable, "-c", environment.REMOTE, mode, encoded],
env={**os.environ, "HOME": str(home)}, check=True, capture_output=True, text=True)
def test_failed_or_skipped_cleanup_quarantines_the_entire_next_attempt(self):
environment.transition("enter", "s3", self.env, execute=self.execute)
for outcome in ("failure", "cancelled", "skipped", ""):
with self.assertRaises(ValueError):
environment.transition("finish", "s3", self.env, outcome, self.execute)
self.assertEqual(json.loads((self.root / "receipt.json").read_text())["state"], "quarantined")
with self.assertRaises(subprocess.CalledProcessError):
environment.transition("enter", "kms", {**self.env, "GITHUB_RUN_ID": "124"}, execute=self.execute)
environment.transition("finish", "s3", self.env, "success", self.execute)
receipt = environment.transition("enter", "kms", {**self.env, "GITHUB_RUN_ID": "124"}, execute=self.execute)
self.assertEqual(receipt["state"], "owned")
def test_foreign_attempt_cannot_release_and_partial_admission_is_not_reused(self):
calls = []
def interrupted(node, user, mode, identity):
calls.append((node, mode))
if node == "node2" and mode == "enter":
raise OSError("node unavailable")
self.execute(node, user, mode, identity)
with self.assertRaises(OSError):
environment.transition("enter", "s3", self.env, execute=interrupted)
self.assertEqual(calls[:2], [("node1", "check"), ("node2", "check")])
with self.assertRaises(subprocess.CalledProcessError):
environment.transition("enter", "kms", self.env, execute=self.execute)
with self.assertRaises(subprocess.CalledProcessError):
environment.transition("finish", "s3", {**self.env, "GITHUB_RUN_ATTEMPT": "2"}, "success", self.execute)
self.assertTrue((self.root / "node1/.rustfs-functional-environment/active.json").is_file())
environment.transition("recover", "s3", self.env, "success", self.execute)
environment.transition("enter", "kms", self.env, execute=self.execute)
def test_node_identity_is_validated_before_ssh(self):
for name in ("node;uname", "-oProxyCommand=anything", "node1 node1"):
execute = mock.Mock()
with self.assertRaises(ValueError):
environment.transition("enter", "s3", {**self.env, "RUSTFS_NODES": name}, execute=execute)
execute.assert_not_called()
if __name__ == "__main__":
unittest.main()