2026-05-21 19:48:02 +00:00
|
|
|
|
use std::collections::BTreeSet;
|
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
use std::process::{Command, ExitCode};
|
2026-02-15 17:03:31 +00:00
|
|
|
|
use std::time::Instant;
|
|
|
|
|
|
|
2026-02-16 15:39:27 +00:00
|
|
|
|
use clap::{Parser, Subcommand};
|
|
|
|
|
|
|
|
|
|
|
|
// ── CLI ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Parser)]
|
|
|
|
|
|
#[command(name = "xtask", about = "Development task runner")]
|
|
|
|
|
|
struct Cli {
|
|
|
|
|
|
#[command(subcommand)]
|
2026-02-24 09:12:28 +00:00
|
|
|
|
command: Option<Cmd>,
|
|
|
|
|
|
|
|
|
|
|
|
/// (Legacy) Test group to run directly without subcommand
|
|
|
|
|
|
#[arg(hide = true)]
|
|
|
|
|
|
group: Option<String>,
|
|
|
|
|
|
|
|
|
|
|
|
/// Show all groups and the cargo commands they run
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
list: bool,
|
2026-02-16 15:39:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Subcommand)]
|
|
|
|
|
|
enum Cmd {
|
2026-02-24 09:12:28 +00:00
|
|
|
|
/// Run a local swactor node with datastore + distribution on localhost
|
2026-02-16 15:39:27 +00:00
|
|
|
|
Node {
|
2026-02-24 09:12:28 +00:00
|
|
|
|
/// Dashboard HTTP port
|
|
|
|
|
|
#[arg(long, default_value = "9090")]
|
|
|
|
|
|
dashboard_port: u16,
|
2026-02-16 15:39:27 +00:00
|
|
|
|
|
2026-02-24 09:12:28 +00:00
|
|
|
|
/// Pass extra args to the swactor binary
|
|
|
|
|
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
|
|
|
|
|
args: Vec<String>,
|
2026-02-16 15:39:27 +00:00
|
|
|
|
},
|
2026-05-21 19:48:02 +00:00
|
|
|
|
|
|
|
|
|
|
/// Run the simulator parity-bar check suite.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Drives the full TESTING_SPEC §2–§12 check list:
|
|
|
|
|
|
/// * lock check
|
|
|
|
|
|
/// * banned-API + parity-bar hygiene lints
|
|
|
|
|
|
/// * both feature builds (facade-prod / facade-sim, plus the
|
|
|
|
|
|
/// negative both-features / neither-feature builds)
|
|
|
|
|
|
/// * runtime-facade surface fingerprint
|
|
|
|
|
|
/// * detector binary in both configurations
|
|
|
|
|
|
/// * every parity-bar test
|
|
|
|
|
|
///
|
|
|
|
|
|
/// With `--phase 1` accepts engine-side `NotImplemented` failures
|
|
|
|
|
|
/// (and the other items in `expected_failures.txt`) as expected
|
|
|
|
|
|
/// pre-engine outcomes.
|
|
|
|
|
|
ParityBar {
|
|
|
|
|
|
/// Restrict acceptance to a phase (1 = pre-engine, 2 = full).
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
phase: Option<u8>,
|
|
|
|
|
|
|
|
|
|
|
|
/// Filter to a single test binary (e.g. `t_determinism`).
|
|
|
|
|
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
|
|
|
|
|
filter: Vec<String>,
|
|
|
|
|
|
},
|
2026-02-16 15:39:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 09:12:28 +00:00
|
|
|
|
// ── Test infrastructure ─────────────────────────────────────────────
|
2026-02-16 15:39:27 +00:00
|
|
|
|
|
2026-02-15 17:03:31 +00:00
|
|
|
|
struct TestStep {
|
|
|
|
|
|
label: &'static str,
|
|
|
|
|
|
args: &'static [&'static str],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
struct Group {
|
|
|
|
|
|
name: &'static str,
|
|
|
|
|
|
description: &'static str,
|
|
|
|
|
|
steps: &'static [TestStep],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const CORE: Group = Group {
|
|
|
|
|
|
name: "core",
|
|
|
|
|
|
description: "Actor runtime, message delivery, property tests",
|
|
|
|
|
|
steps: &[TestStep {
|
|
|
|
|
|
label: "actor runtime",
|
|
|
|
|
|
args: &["test", "-p", "swactor", "--features", "transport"],
|
|
|
|
|
|
}],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-24 09:12:28 +00:00
|
|
|
|
const SIMULATION: Group = Group {
|
|
|
|
|
|
name: "simulation",
|
2026-02-15 17:03:31 +00:00
|
|
|
|
description: "Deterministic cluster simulations",
|
|
|
|
|
|
steps: &[TestStep {
|
|
|
|
|
|
label: "cluster simulations",
|
|
|
|
|
|
args: &["test", "-p", "simulation"],
|
|
|
|
|
|
}],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-24 09:12:28 +00:00
|
|
|
|
const FORMAL_VERIFICATION: Group = Group {
|
|
|
|
|
|
name: "formal-verification",
|
|
|
|
|
|
description: "Kani proofs + Stateright model checking",
|
2026-02-15 17:03:31 +00:00
|
|
|
|
steps: &[
|
|
|
|
|
|
TestStep {
|
2026-02-24 09:12:28 +00:00
|
|
|
|
label: "kani proofs",
|
|
|
|
|
|
args: &["kani", "-p", "swactor-datastore"],
|
|
|
|
|
|
},
|
|
|
|
|
|
TestStep {
|
|
|
|
|
|
label: "gateway dispatch model check",
|
2026-02-15 17:03:31 +00:00
|
|
|
|
args: &[
|
|
|
|
|
|
"test",
|
|
|
|
|
|
"-p",
|
|
|
|
|
|
"swactor-datastore",
|
|
|
|
|
|
"--test",
|
2026-02-24 09:12:28 +00:00
|
|
|
|
"gateway_model_check",
|
2026-02-15 17:03:31 +00:00
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-24 09:12:28 +00:00
|
|
|
|
const GROUPS: &[&Group] = &[&CORE, &SIMULATION, &FORMAL_VERIFICATION];
|
|
|
|
|
|
|
2026-02-15 17:03:31 +00:00
|
|
|
|
fn groups_for(name: &str) -> Option<Vec<&'static Group>> {
|
|
|
|
|
|
match name {
|
|
|
|
|
|
"core" => Some(vec![&CORE]),
|
2026-02-24 09:12:28 +00:00
|
|
|
|
"simulation" => Some(vec![&SIMULATION]),
|
|
|
|
|
|
"formal-verification" => Some(vec![&FORMAL_VERIFICATION]),
|
|
|
|
|
|
"essential" => Some(vec![&CORE, &SIMULATION]),
|
|
|
|
|
|
"all" => Some(vec![&CORE, &SIMULATION, &FORMAL_VERIFICATION]),
|
2026-02-15 17:03:31 +00:00
|
|
|
|
_ => None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn run_step(group_name: &str, step: &TestStep) -> bool {
|
|
|
|
|
|
println!("\n=== {group_name}: {} ===", step.label);
|
|
|
|
|
|
println!(" cargo {}", step.args.join(" "));
|
|
|
|
|
|
println!();
|
|
|
|
|
|
|
2026-05-21 19:48:02 +00:00
|
|
|
|
let status = Command::new(cargo_bin()).args(step.args).status();
|
2026-02-15 17:03:31 +00:00
|
|
|
|
|
|
|
|
|
|
match status {
|
|
|
|
|
|
Ok(s) => s.success(),
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
eprintln!("Failed to execute cargo: {e}");
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn print_usage() {
|
|
|
|
|
|
println!(
|
|
|
|
|
|
"\
|
2026-02-24 09:12:28 +00:00
|
|
|
|
USAGE: cargo xtask <COMMAND|GROUP>
|
|
|
|
|
|
|
|
|
|
|
|
COMMANDS:
|
|
|
|
|
|
node Run a local swactor node (datastore + distribution, localhost)
|
2026-05-21 19:48:02 +00:00
|
|
|
|
parity-bar Run the simulator parity-bar check suite
|
2026-02-24 09:12:28 +00:00
|
|
|
|
|
|
|
|
|
|
TEST GROUPS:
|
|
|
|
|
|
core Actor runtime, message delivery, property tests
|
|
|
|
|
|
simulation Deterministic cluster simulations
|
|
|
|
|
|
formal-verification Kani proofs + Stateright model checking
|
|
|
|
|
|
essential core + simulation
|
|
|
|
|
|
all Every test group
|
2026-02-15 17:03:31 +00:00
|
|
|
|
|
2026-02-23 04:53:04 +00:00
|
|
|
|
FLAGS:
|
2026-02-24 09:12:28 +00:00
|
|
|
|
--list Show all groups and the cargo commands they run"
|
2026-02-15 17:03:31 +00:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn print_list() {
|
|
|
|
|
|
println!("Available test groups:\n");
|
|
|
|
|
|
|
2026-02-24 09:12:28 +00:00
|
|
|
|
for group in GROUPS {
|
|
|
|
|
|
println!(" {:<22}{}", group.name, group.description);
|
2026-02-15 17:03:31 +00:00
|
|
|
|
for step in group.steps {
|
2026-02-24 09:12:28 +00:00
|
|
|
|
println!(" → cargo {}", step.args.join(" "));
|
2026-02-15 17:03:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
println!();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 09:12:28 +00:00
|
|
|
|
println!(" {:<22}core + simulation", "essential");
|
|
|
|
|
|
println!(" {:<22}Every test group", "all");
|
2026-02-15 17:03:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 09:12:28 +00:00
|
|
|
|
// ── Node runner ─────────────────────────────────────────────────────
|
2026-02-15 17:03:31 +00:00
|
|
|
|
|
2026-02-24 09:12:28 +00:00
|
|
|
|
fn run_node(dashboard_port: u16, extra_args: &[String]) {
|
|
|
|
|
|
eprintln!("Building swactor node...");
|
2026-05-21 19:48:02 +00:00
|
|
|
|
let build = Command::new(cargo_bin())
|
2026-02-24 09:12:28 +00:00
|
|
|
|
.args(["build", "-p", "node"])
|
|
|
|
|
|
.status();
|
|
|
|
|
|
|
|
|
|
|
|
match build {
|
|
|
|
|
|
Ok(s) if s.success() => {}
|
|
|
|
|
|
Ok(s) => {
|
|
|
|
|
|
eprintln!("Build failed");
|
|
|
|
|
|
std::process::exit(s.code().unwrap_or(1));
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
eprintln!("Failed to execute cargo build: {e}");
|
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
|
}
|
2026-02-15 17:03:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 09:12:28 +00:00
|
|
|
|
let tmp = std::env::temp_dir().join("swactor-dev-node");
|
|
|
|
|
|
std::fs::create_dir_all(&tmp).expect("failed to create temp dir");
|
|
|
|
|
|
|
|
|
|
|
|
let identity_dir = tmp.join("identity");
|
|
|
|
|
|
let auth_dir = tmp.join("auth");
|
|
|
|
|
|
|
|
|
|
|
|
eprintln!();
|
|
|
|
|
|
|
|
|
|
|
|
let mut cmd = Command::new("target/debug/swactor");
|
|
|
|
|
|
cmd.args([
|
2026-05-21 19:48:02 +00:00
|
|
|
|
"--transport",
|
|
|
|
|
|
"iroh",
|
|
|
|
|
|
"--dashboard-port",
|
|
|
|
|
|
&dashboard_port.to_string(),
|
|
|
|
|
|
"--identity-dir",
|
|
|
|
|
|
&identity_dir.to_string_lossy(),
|
|
|
|
|
|
"--auth-dir",
|
|
|
|
|
|
&auth_dir.to_string_lossy(),
|
2026-02-24 09:12:28 +00:00
|
|
|
|
"--no-relay",
|
|
|
|
|
|
]);
|
|
|
|
|
|
cmd.env("HOME", tmp.to_string_lossy().as_ref());
|
|
|
|
|
|
cmd.args(extra_args);
|
|
|
|
|
|
|
|
|
|
|
|
let status = cmd.status();
|
|
|
|
|
|
match status {
|
|
|
|
|
|
Ok(s) => std::process::exit(s.code().unwrap_or(0)),
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
eprintln!("Failed to execute swactor: {e}");
|
2026-02-16 15:39:27 +00:00
|
|
|
|
std::process::exit(1);
|
|
|
|
|
|
}
|
2026-02-24 09:12:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-16 15:39:27 +00:00
|
|
|
|
|
2026-05-21 19:48:02 +00:00
|
|
|
|
// ── parity-bar ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const PARITY_TEST_BINARIES: &[&str] = &[
|
|
|
|
|
|
"t_determinism",
|
|
|
|
|
|
"t_replay",
|
|
|
|
|
|
"t_facade",
|
|
|
|
|
|
"t_same_binary",
|
|
|
|
|
|
"t_schema_coverage",
|
|
|
|
|
|
"t_round_trip",
|
|
|
|
|
|
"t_causality",
|
|
|
|
|
|
"t_equivariance",
|
|
|
|
|
|
"t_lifecycle",
|
|
|
|
|
|
"t_detector",
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
|
struct CheckResult {
|
|
|
|
|
|
label: String,
|
|
|
|
|
|
passed: bool,
|
|
|
|
|
|
detail: String,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl CheckResult {
|
|
|
|
|
|
fn pass(label: impl Into<String>, detail: impl Into<String>) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
label: label.into(),
|
|
|
|
|
|
passed: true,
|
|
|
|
|
|
detail: detail.into(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
fn fail(label: impl Into<String>, detail: impl Into<String>) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
label: label.into(),
|
|
|
|
|
|
passed: false,
|
|
|
|
|
|
detail: detail.into(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn run_parity_bar(phase: Option<u8>, filter: &[String]) -> ExitCode {
|
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
|
let workspace = workspace_root();
|
|
|
|
|
|
|
|
|
|
|
|
println!(
|
|
|
|
|
|
"parity-bar: phase={} filter={}",
|
|
|
|
|
|
phase.map(|p| p.to_string()).unwrap_or_else(|| "any".into()),
|
|
|
|
|
|
if filter.is_empty() {
|
|
|
|
|
|
"(all)".into()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
filter.join(" ")
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let mut results: Vec<CheckResult> = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
// The infrastructure checks all need to pass in any phase.
|
|
|
|
|
|
results.push(check_lock(&workspace));
|
|
|
|
|
|
results.push(check_banned_api_lint(&workspace));
|
|
|
|
|
|
results.push(check_parity_bar_lint(&workspace));
|
|
|
|
|
|
results.push(check_feature_build(&workspace, "facade-prod"));
|
|
|
|
|
|
results.push(check_feature_build(&workspace, "facade-sim"));
|
|
|
|
|
|
results.push(check_negative_build(
|
|
|
|
|
|
&workspace,
|
|
|
|
|
|
&["build", "--workspace", "--features", "facade-prod,facade-sim"],
|
|
|
|
|
|
"both-features rejected",
|
|
|
|
|
|
));
|
|
|
|
|
|
results.push(check_negative_build(
|
|
|
|
|
|
&workspace,
|
|
|
|
|
|
&["build", "--workspace", "--no-default-features"],
|
|
|
|
|
|
"neither-feature rejected",
|
|
|
|
|
|
));
|
|
|
|
|
|
results.push(check_surface_fingerprint(&workspace));
|
|
|
|
|
|
results.push(check_detector_build(&workspace, "facade-prod"));
|
|
|
|
|
|
results.push(check_detector_build(&workspace, "facade-sim"));
|
|
|
|
|
|
|
|
|
|
|
|
// Parity-bar tests last (most expensive). Skip if a prerequisite
|
|
|
|
|
|
// already failed — running the tests with a broken lint or lock is
|
|
|
|
|
|
// not informative.
|
|
|
|
|
|
if results.iter().all(|r| r.passed) {
|
|
|
|
|
|
results.push(check_parity_tests(&workspace, phase, filter));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
results.push(CheckResult::fail(
|
|
|
|
|
|
"parity-bar tests",
|
|
|
|
|
|
"skipped (prerequisite check failed)".to_string(),
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let elapsed = start.elapsed();
|
|
|
|
|
|
let mut failed = 0;
|
|
|
|
|
|
println!("\n── parity-bar summary ─────────────────────────────────");
|
|
|
|
|
|
for r in &results {
|
|
|
|
|
|
let tag = if r.passed { "ok " } else { "FAIL" };
|
|
|
|
|
|
println!("{tag} {:<32} {}", r.label, r.detail);
|
|
|
|
|
|
if !r.passed {
|
|
|
|
|
|
failed += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
println!(
|
|
|
|
|
|
"── {} check(s), {failed} failed, {:.1}s ────────────",
|
|
|
|
|
|
results.len(),
|
|
|
|
|
|
elapsed.as_secs_f64()
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if failed > 0 {
|
|
|
|
|
|
ExitCode::from(1)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ExitCode::SUCCESS
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── parity-bar checks ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
fn check_lock(workspace: &Path) -> CheckResult {
|
|
|
|
|
|
let script = workspace.join("scripts/check-parity-lock.sh");
|
|
|
|
|
|
let output = Command::new("bash")
|
|
|
|
|
|
.arg(&script)
|
|
|
|
|
|
.current_dir(workspace)
|
|
|
|
|
|
.output();
|
|
|
|
|
|
match output {
|
|
|
|
|
|
Ok(out) if out.status.success() => {
|
|
|
|
|
|
CheckResult::pass("parity-lock check", "hash matches")
|
|
|
|
|
|
}
|
|
|
|
|
|
Ok(out) => CheckResult::fail(
|
|
|
|
|
|
"parity-lock check",
|
|
|
|
|
|
String::from_utf8_lossy(&out.stderr).trim().to_string(),
|
|
|
|
|
|
),
|
|
|
|
|
|
Err(e) => CheckResult::fail("parity-lock check", e.to_string()),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn check_banned_api_lint(workspace: &Path) -> CheckResult {
|
|
|
|
|
|
let out = Command::new(cargo_bin())
|
|
|
|
|
|
.args([
|
|
|
|
|
|
"run",
|
|
|
|
|
|
"-p",
|
|
|
|
|
|
"simulation",
|
|
|
|
|
|
"--bin",
|
|
|
|
|
|
"lint-deterministic",
|
|
|
|
|
|
"--quiet",
|
|
|
|
|
|
"--",
|
|
|
|
|
|
"--workspace-root",
|
|
|
|
|
|
&workspace.display().to_string(),
|
|
|
|
|
|
"--quiet",
|
|
|
|
|
|
])
|
|
|
|
|
|
.current_dir(workspace)
|
|
|
|
|
|
.output();
|
|
|
|
|
|
match out {
|
|
|
|
|
|
Ok(o) if o.status.success() => {
|
|
|
|
|
|
CheckResult::pass("banned-API lint", "no violations")
|
|
|
|
|
|
}
|
|
|
|
|
|
Ok(o) => CheckResult::fail(
|
|
|
|
|
|
"banned-API lint",
|
|
|
|
|
|
String::from_utf8_lossy(&o.stdout)
|
|
|
|
|
|
.lines()
|
|
|
|
|
|
.chain(String::from_utf8_lossy(&o.stderr).lines())
|
|
|
|
|
|
.take(8)
|
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
|
.join("; "),
|
|
|
|
|
|
),
|
|
|
|
|
|
Err(e) => CheckResult::fail("banned-API lint", e.to_string()),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn check_parity_bar_lint(workspace: &Path) -> CheckResult {
|
|
|
|
|
|
let out = Command::new(cargo_bin())
|
|
|
|
|
|
.args([
|
|
|
|
|
|
"run",
|
|
|
|
|
|
"-p",
|
|
|
|
|
|
"simulation",
|
|
|
|
|
|
"--bin",
|
|
|
|
|
|
"lint-deterministic",
|
|
|
|
|
|
"--quiet",
|
|
|
|
|
|
"--",
|
|
|
|
|
|
"--workspace-root",
|
|
|
|
|
|
&workspace.display().to_string(),
|
|
|
|
|
|
"--check-parity-bar",
|
|
|
|
|
|
"--quiet",
|
|
|
|
|
|
])
|
|
|
|
|
|
.current_dir(workspace)
|
|
|
|
|
|
.output();
|
|
|
|
|
|
match out {
|
|
|
|
|
|
Ok(o) if o.status.success() => {
|
|
|
|
|
|
CheckResult::pass("parity-bar hygiene", "no violations")
|
|
|
|
|
|
}
|
|
|
|
|
|
Ok(o) => CheckResult::fail(
|
|
|
|
|
|
"parity-bar hygiene",
|
|
|
|
|
|
String::from_utf8_lossy(&o.stdout)
|
|
|
|
|
|
.lines()
|
|
|
|
|
|
.chain(String::from_utf8_lossy(&o.stderr).lines())
|
|
|
|
|
|
.take(8)
|
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
|
.join("; "),
|
|
|
|
|
|
),
|
|
|
|
|
|
Err(e) => CheckResult::fail("parity-bar hygiene", e.to_string()),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn check_feature_build(workspace: &Path, feature: &str) -> CheckResult {
|
|
|
|
|
|
let out = Command::new(cargo_bin())
|
|
|
|
|
|
.args([
|
|
|
|
|
|
"build",
|
|
|
|
|
|
"--workspace",
|
|
|
|
|
|
"--features",
|
|
|
|
|
|
feature,
|
|
|
|
|
|
"--quiet",
|
|
|
|
|
|
])
|
|
|
|
|
|
.current_dir(workspace)
|
|
|
|
|
|
.output();
|
|
|
|
|
|
match out {
|
|
|
|
|
|
Ok(o) if o.status.success() => CheckResult::pass(
|
|
|
|
|
|
format!("build --features {feature}"),
|
|
|
|
|
|
"compiled cleanly",
|
|
|
|
|
|
),
|
|
|
|
|
|
Ok(o) => CheckResult::fail(
|
|
|
|
|
|
format!("build --features {feature}"),
|
|
|
|
|
|
String::from_utf8_lossy(&o.stderr)
|
|
|
|
|
|
.lines()
|
|
|
|
|
|
.filter(|l| l.contains("error"))
|
|
|
|
|
|
.take(6)
|
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
|
.join("; "),
|
|
|
|
|
|
),
|
|
|
|
|
|
Err(e) => CheckResult::fail(format!("build --features {feature}"), e.to_string()),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn check_negative_build(workspace: &Path, args: &[&str], label: &str) -> CheckResult {
|
|
|
|
|
|
// The build is expected to fail. Per the Stage 1 gate (and
|
|
|
|
|
|
// TESTING_SPEC §4.2) the failure message must not contain the
|
|
|
|
|
|
// literal substring "compile_error" — we use `const _: () = panic!`
|
|
|
|
|
|
// for the diagnostic. Pass iff the failure is via the const-panic
|
|
|
|
|
|
// mechanism (or any non-compile_error path), fail iff the build
|
|
|
|
|
|
// succeeded or the diagnostic mentioned "compile_error".
|
|
|
|
|
|
let out = Command::new(cargo_bin())
|
|
|
|
|
|
.args(args)
|
|
|
|
|
|
.current_dir(workspace)
|
|
|
|
|
|
.output();
|
|
|
|
|
|
match out {
|
|
|
|
|
|
Ok(o) if o.status.success() => {
|
|
|
|
|
|
CheckResult::fail(label, "build unexpectedly succeeded")
|
|
|
|
|
|
}
|
|
|
|
|
|
Ok(o) => {
|
|
|
|
|
|
let stderr = String::from_utf8_lossy(&o.stderr);
|
|
|
|
|
|
if stderr.contains("compile_error") {
|
|
|
|
|
|
CheckResult::fail(
|
|
|
|
|
|
label,
|
|
|
|
|
|
"diagnostic mentioned `compile_error` — use const-panic",
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
CheckResult::pass(label, "rejected by const-panic")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => CheckResult::fail(label, e.to_string()),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn check_surface_fingerprint(workspace: &Path) -> CheckResult {
|
|
|
|
|
|
let out = Command::new(cargo_bin())
|
|
|
|
|
|
.args(["test", "-p", "simulation", "--test", "surface_locked", "--quiet"])
|
|
|
|
|
|
.current_dir(workspace)
|
|
|
|
|
|
.output();
|
|
|
|
|
|
match out {
|
|
|
|
|
|
Ok(o) if o.status.success() => {
|
|
|
|
|
|
CheckResult::pass("facade surface lock", "fingerprint matches")
|
|
|
|
|
|
}
|
|
|
|
|
|
Ok(o) => CheckResult::fail(
|
|
|
|
|
|
"facade surface lock",
|
|
|
|
|
|
String::from_utf8_lossy(&o.stdout)
|
|
|
|
|
|
.lines()
|
|
|
|
|
|
.chain(String::from_utf8_lossy(&o.stderr).lines())
|
|
|
|
|
|
.filter(|l| l.contains("FAIL") || l.contains("error"))
|
|
|
|
|
|
.take(6)
|
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
|
.join("; "),
|
|
|
|
|
|
),
|
|
|
|
|
|
Err(e) => CheckResult::fail("facade surface lock", e.to_string()),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn check_detector_build(workspace: &Path, feature: &str) -> CheckResult {
|
|
|
|
|
|
let out = Command::new(cargo_bin())
|
|
|
|
|
|
.args([
|
|
|
|
|
|
"build",
|
|
|
|
|
|
"-p",
|
|
|
|
|
|
"simulation",
|
|
|
|
|
|
"--bin",
|
|
|
|
|
|
"sim-detector",
|
|
|
|
|
|
"--features",
|
|
|
|
|
|
feature,
|
|
|
|
|
|
"--quiet",
|
|
|
|
|
|
])
|
|
|
|
|
|
.current_dir(workspace)
|
|
|
|
|
|
.output();
|
|
|
|
|
|
match out {
|
|
|
|
|
|
Ok(o) if o.status.success() => CheckResult::pass(
|
|
|
|
|
|
format!("sim-detector {feature}"),
|
|
|
|
|
|
"binary compiles",
|
|
|
|
|
|
),
|
|
|
|
|
|
Ok(o) => CheckResult::fail(
|
|
|
|
|
|
format!("sim-detector {feature}"),
|
|
|
|
|
|
String::from_utf8_lossy(&o.stderr)
|
|
|
|
|
|
.lines()
|
|
|
|
|
|
.filter(|l| l.contains("error"))
|
|
|
|
|
|
.take(6)
|
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
|
.join("; "),
|
|
|
|
|
|
),
|
|
|
|
|
|
Err(e) => CheckResult::fail(format!("sim-detector {feature}"), e.to_string()),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn check_parity_tests(workspace: &Path, phase: Option<u8>, filter: &[String]) -> CheckResult {
|
|
|
|
|
|
let expected = load_expected_failures(workspace);
|
|
|
|
|
|
let mut runs: Vec<TestRun> = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
for binary in PARITY_TEST_BINARIES {
|
|
|
|
|
|
if !filter.is_empty()
|
|
|
|
|
|
&& !filter
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.any(|f| binary == &f.as_str() || binary.contains(f.as_str()))
|
|
|
|
|
|
{
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
runs.push(run_one_test_binary(workspace, binary));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut all_failures: Vec<String> = Vec::new();
|
|
|
|
|
|
let mut accepted_failures: Vec<String> = Vec::new();
|
|
|
|
|
|
let mut passes: usize = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for run in &runs {
|
|
|
|
|
|
passes += run.passed.len();
|
|
|
|
|
|
for test in &run.failed {
|
|
|
|
|
|
let qualified = format!("{}::{}", run.binary, test);
|
|
|
|
|
|
if expected.contains(&qualified) {
|
|
|
|
|
|
accepted_failures.push(qualified);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
all_failures.push(qualified);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let label = "parity-bar tests";
|
|
|
|
|
|
let detail = format!(
|
|
|
|
|
|
"{passes} pass, {} expected-fail, {} unexpected-fail",
|
|
|
|
|
|
accepted_failures.len(),
|
|
|
|
|
|
all_failures.len()
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let phase_one_ok = phase == Some(1) && all_failures.is_empty();
|
|
|
|
|
|
let phase_two_ok = phase != Some(1) && all_failures.is_empty() && accepted_failures.is_empty();
|
|
|
|
|
|
|
|
|
|
|
|
if phase_one_ok || phase_two_ok {
|
|
|
|
|
|
CheckResult::pass(label, detail)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let head: Vec<String> = all_failures.iter().take(10).cloned().collect();
|
|
|
|
|
|
CheckResult::fail(
|
|
|
|
|
|
label,
|
|
|
|
|
|
format!(
|
|
|
|
|
|
"{detail}; unexpected: {}",
|
|
|
|
|
|
if head.is_empty() {
|
|
|
|
|
|
"(none — but accepted-fail count > 0 in phase ≥ 2)".to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
head.join(", ")
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
|
struct TestRun {
|
|
|
|
|
|
binary: String,
|
|
|
|
|
|
passed: Vec<String>,
|
|
|
|
|
|
failed: Vec<String>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn run_one_test_binary(workspace: &Path, binary: &str) -> TestRun {
|
|
|
|
|
|
let out = Command::new(cargo_bin())
|
|
|
|
|
|
.args([
|
|
|
|
|
|
"test",
|
|
|
|
|
|
"-p",
|
|
|
|
|
|
"simulation",
|
|
|
|
|
|
"--test",
|
|
|
|
|
|
binary,
|
|
|
|
|
|
"--no-fail-fast",
|
|
|
|
|
|
"--",
|
|
|
|
|
|
"--test-threads=1",
|
|
|
|
|
|
])
|
|
|
|
|
|
.current_dir(workspace)
|
|
|
|
|
|
.output()
|
|
|
|
|
|
.unwrap_or_else(|e| panic!("cargo test --test {binary}: {e}"));
|
|
|
|
|
|
|
|
|
|
|
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
|
|
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
|
|
|
|
let mut passed = Vec::new();
|
|
|
|
|
|
let mut failed = Vec::new();
|
|
|
|
|
|
for source in [stdout.as_ref(), stderr.as_ref()] {
|
|
|
|
|
|
for line in source.lines() {
|
|
|
|
|
|
let trimmed = line.trim_start();
|
|
|
|
|
|
if !trimmed.starts_with("test ") {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(rest) = trimmed.strip_prefix("test ") {
|
|
|
|
|
|
if let Some(name) = rest.strip_suffix(" ... ok") {
|
|
|
|
|
|
if !passed.contains(&name.to_string()) {
|
|
|
|
|
|
passed.push(name.to_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if let Some(name) = rest.strip_suffix(" ... FAILED") {
|
|
|
|
|
|
if !failed.contains(&name.to_string()) {
|
|
|
|
|
|
failed.push(name.to_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
TestRun {
|
|
|
|
|
|
binary: binary.to_string(),
|
|
|
|
|
|
passed,
|
|
|
|
|
|
failed,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn load_expected_failures(workspace: &Path) -> BTreeSet<String> {
|
|
|
|
|
|
let path = workspace
|
|
|
|
|
|
.join("crates/simulation/tests/parity-bar/expected_failures.txt");
|
|
|
|
|
|
let text = match std::fs::read_to_string(&path) {
|
|
|
|
|
|
Ok(t) => t,
|
|
|
|
|
|
Err(_) => return BTreeSet::new(),
|
|
|
|
|
|
};
|
|
|
|
|
|
let mut out = BTreeSet::new();
|
|
|
|
|
|
for raw in text.lines() {
|
|
|
|
|
|
let line = raw.split('#').next().unwrap_or("").trim();
|
|
|
|
|
|
if line.is_empty() {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
out.insert(line.to_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn workspace_root() -> PathBuf {
|
|
|
|
|
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
|
|
|
|
|
Path::new(manifest_dir)
|
|
|
|
|
|
.parent()
|
|
|
|
|
|
.map(Path::to_path_buf)
|
|
|
|
|
|
.unwrap_or_else(|| PathBuf::from(manifest_dir))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn cargo_bin() -> String {
|
|
|
|
|
|
option_env!("CARGO")
|
|
|
|
|
|
.map(str::to_string)
|
|
|
|
|
|
.unwrap_or_else(|| "cargo".to_string())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 09:12:28 +00:00
|
|
|
|
// ── Dispatch ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
fn run_test_groups(group_name: &str) {
|
|
|
|
|
|
let groups = match groups_for(group_name) {
|
2026-02-16 15:39:27 +00:00
|
|
|
|
Some(g) => g,
|
|
|
|
|
|
None => {
|
2026-02-24 09:12:28 +00:00
|
|
|
|
eprintln!("Unknown command or test group: {group_name}\n");
|
2026-02-15 17:03:31 +00:00
|
|
|
|
print_usage();
|
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
|
let mut passed = 0usize;
|
|
|
|
|
|
let mut failed = 0usize;
|
|
|
|
|
|
|
|
|
|
|
|
for group in &groups {
|
|
|
|
|
|
for step in group.steps {
|
|
|
|
|
|
if run_step(group.name, step) {
|
|
|
|
|
|
passed += 1;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
failed += 1;
|
|
|
|
|
|
let elapsed = start.elapsed();
|
|
|
|
|
|
println!(
|
|
|
|
|
|
"\n--- FAILED after {:.1}s ({passed} passed, {failed} failed) ---",
|
|
|
|
|
|
elapsed.as_secs_f64()
|
|
|
|
|
|
);
|
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let elapsed = start.elapsed();
|
|
|
|
|
|
println!(
|
|
|
|
|
|
"\n--- All {passed} step(s) passed in {:.1}s ---",
|
|
|
|
|
|
elapsed.as_secs_f64()
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-02-16 15:39:27 +00:00
|
|
|
|
|
2026-05-21 19:48:02 +00:00
|
|
|
|
fn main() -> ExitCode {
|
2026-02-24 09:12:28 +00:00
|
|
|
|
let cli = Cli::parse();
|
2026-02-16 15:39:27 +00:00
|
|
|
|
|
2026-02-24 09:12:28 +00:00
|
|
|
|
if cli.list {
|
|
|
|
|
|
print_list();
|
2026-05-21 19:48:02 +00:00
|
|
|
|
return ExitCode::SUCCESS;
|
2026-02-16 15:39:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 09:12:28 +00:00
|
|
|
|
if let Some(cmd) = cli.command {
|
2026-05-21 19:48:02 +00:00
|
|
|
|
return match cmd {
|
|
|
|
|
|
Cmd::Node {
|
|
|
|
|
|
dashboard_port,
|
|
|
|
|
|
args,
|
|
|
|
|
|
} => {
|
|
|
|
|
|
run_node(dashboard_port, &args);
|
|
|
|
|
|
ExitCode::SUCCESS
|
|
|
|
|
|
}
|
|
|
|
|
|
Cmd::ParityBar { phase, filter } => run_parity_bar(phase, &filter),
|
|
|
|
|
|
};
|
2026-02-16 15:39:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 09:12:28 +00:00
|
|
|
|
match cli.group {
|
2026-05-21 19:48:02 +00:00
|
|
|
|
Some(name) => {
|
|
|
|
|
|
run_test_groups(&name);
|
|
|
|
|
|
ExitCode::SUCCESS
|
|
|
|
|
|
}
|
2026-02-24 09:12:28 +00:00
|
|
|
|
None => {
|
|
|
|
|
|
print_usage();
|
2026-05-21 19:48:02 +00:00
|
|
|
|
ExitCode::from(1)
|
2026-02-16 15:39:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|