758 lines
23 KiB
Rust
758 lines
23 KiB
Rust
use std::collections::BTreeSet;
|
||
use std::path::{Path, PathBuf};
|
||
use std::process::{Command, ExitCode};
|
||
use std::time::Instant;
|
||
|
||
use clap::{Parser, Subcommand};
|
||
|
||
// ── CLI ─────────────────────────────────────────────────────────────
|
||
|
||
#[derive(Parser)]
|
||
#[command(name = "xtask", about = "Development task runner")]
|
||
struct Cli {
|
||
#[command(subcommand)]
|
||
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,
|
||
}
|
||
|
||
#[derive(Subcommand)]
|
||
enum Cmd {
|
||
/// Run a local swactor node with datastore + distribution on localhost
|
||
Node {
|
||
/// Dashboard HTTP port
|
||
#[arg(long, default_value = "9090")]
|
||
dashboard_port: u16,
|
||
|
||
/// Pass extra args to the swactor binary
|
||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||
args: Vec<String>,
|
||
},
|
||
|
||
/// 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>,
|
||
},
|
||
}
|
||
|
||
// ── Test infrastructure ─────────────────────────────────────────────
|
||
|
||
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"],
|
||
}],
|
||
};
|
||
|
||
const SIMULATION: Group = Group {
|
||
name: "simulation",
|
||
description: "Deterministic cluster simulations",
|
||
steps: &[TestStep {
|
||
label: "cluster simulations",
|
||
args: &["test", "-p", "simulation"],
|
||
}],
|
||
};
|
||
|
||
const FORMAL_VERIFICATION: Group = Group {
|
||
name: "formal-verification",
|
||
description: "Kani proofs + Stateright model checking",
|
||
steps: &[
|
||
TestStep {
|
||
label: "kani proofs",
|
||
args: &["kani", "-p", "swactor-datastore"],
|
||
},
|
||
TestStep {
|
||
label: "gateway dispatch model check",
|
||
args: &[
|
||
"test",
|
||
"-p",
|
||
"swactor-datastore",
|
||
"--test",
|
||
"gateway_model_check",
|
||
],
|
||
},
|
||
],
|
||
};
|
||
|
||
const GROUPS: &[&Group] = &[&CORE, &SIMULATION, &FORMAL_VERIFICATION];
|
||
|
||
fn groups_for(name: &str) -> Option<Vec<&'static Group>> {
|
||
match name {
|
||
"core" => Some(vec![&CORE]),
|
||
"simulation" => Some(vec![&SIMULATION]),
|
||
"formal-verification" => Some(vec![&FORMAL_VERIFICATION]),
|
||
"essential" => Some(vec![&CORE, &SIMULATION]),
|
||
"all" => Some(vec![&CORE, &SIMULATION, &FORMAL_VERIFICATION]),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
fn run_step(group_name: &str, step: &TestStep) -> bool {
|
||
println!("\n=== {group_name}: {} ===", step.label);
|
||
println!(" cargo {}", step.args.join(" "));
|
||
println!();
|
||
|
||
let status = Command::new(cargo_bin()).args(step.args).status();
|
||
|
||
match status {
|
||
Ok(s) => s.success(),
|
||
Err(e) => {
|
||
eprintln!("Failed to execute cargo: {e}");
|
||
false
|
||
}
|
||
}
|
||
}
|
||
|
||
fn print_usage() {
|
||
println!(
|
||
"\
|
||
USAGE: cargo xtask <COMMAND|GROUP>
|
||
|
||
COMMANDS:
|
||
node Run a local swactor node (datastore + distribution, localhost)
|
||
parity-bar Run the simulator parity-bar check suite
|
||
|
||
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
|
||
|
||
FLAGS:
|
||
--list Show all groups and the cargo commands they run"
|
||
);
|
||
}
|
||
|
||
fn print_list() {
|
||
println!("Available test groups:\n");
|
||
|
||
for group in GROUPS {
|
||
println!(" {:<22}{}", group.name, group.description);
|
||
for step in group.steps {
|
||
println!(" → cargo {}", step.args.join(" "));
|
||
}
|
||
println!();
|
||
}
|
||
|
||
println!(" {:<22}core + simulation", "essential");
|
||
println!(" {:<22}Every test group", "all");
|
||
}
|
||
|
||
// ── Node runner ─────────────────────────────────────────────────────
|
||
|
||
fn run_node(dashboard_port: u16, extra_args: &[String]) {
|
||
eprintln!("Building swactor node...");
|
||
let build = Command::new(cargo_bin())
|
||
.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);
|
||
}
|
||
}
|
||
|
||
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([
|
||
"--transport",
|
||
"iroh",
|
||
"--dashboard-port",
|
||
&dashboard_port.to_string(),
|
||
"--identity-dir",
|
||
&identity_dir.to_string_lossy(),
|
||
"--auth-dir",
|
||
&auth_dir.to_string_lossy(),
|
||
"--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}");
|
||
std::process::exit(1);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── 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())
|
||
}
|
||
|
||
// ── Dispatch ────────────────────────────────────────────────────────
|
||
|
||
fn run_test_groups(group_name: &str) {
|
||
let groups = match groups_for(group_name) {
|
||
Some(g) => g,
|
||
None => {
|
||
eprintln!("Unknown command or test group: {group_name}\n");
|
||
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()
|
||
);
|
||
}
|
||
|
||
fn main() -> ExitCode {
|
||
let cli = Cli::parse();
|
||
|
||
if cli.list {
|
||
print_list();
|
||
return ExitCode::SUCCESS;
|
||
}
|
||
|
||
if let Some(cmd) = cli.command {
|
||
return match cmd {
|
||
Cmd::Node {
|
||
dashboard_port,
|
||
args,
|
||
} => {
|
||
run_node(dashboard_port, &args);
|
||
ExitCode::SUCCESS
|
||
}
|
||
Cmd::ParityBar { phase, filter } => run_parity_bar(phase, &filter),
|
||
};
|
||
}
|
||
|
||
match cli.group {
|
||
Some(name) => {
|
||
run_test_groups(&name);
|
||
ExitCode::SUCCESS
|
||
}
|
||
None => {
|
||
print_usage();
|
||
ExitCode::from(1)
|
||
}
|
||
}
|
||
}
|