swactor/crates/simulation/tests/local_ci_properties.rs
Zachery Aaron Shores-Chmielewski 0c6f9c9f39 feat: deploy CI pipeline with ci-relay and local-runner
Add ci-relay (VPS webhook receiver) and local-runner (Thinkpad CI
executor) crates that communicate over iroh. Includes .ci.yml smoke
test pipeline, simulation tests, and development docs.

Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
2026-02-16 22:47:28 +07:00

256 lines
7 KiB
Rust

//! Property-based tests for the local CI simulation.
//!
//! These verify invariants that should hold across all possible simulation configurations.
use simulation::ci::local_sim::{self, LocalSimConfig};
use swactor_ci::{EventType, WebhookEvent};
fn simple_yaml() -> String {
r#"
pipelines:
check:
triggers:
- event: push
branches: ["*"]
jobs:
fmt:
run: cargo fmt -- --check
test:
needs: [fmt]
run: cargo test
"#
.into()
}
fn push(branch: &str, sha: &str) -> WebhookEvent {
WebhookEvent {
event_type: EventType::Push,
repo_owner: "user".into(),
repo_name: "repo".into(),
branch: branch.into(),
commit_sha: sha.into(),
tag: None,
}
}
// ─── Property: One-at-a-time ────────────────────────────────────────────────
#[test]
fn property_one_at_a_time_single_push() {
let config = LocalSimConfig {
name: "prop-1at1-single".into(),
num_rounds: 30,
ci_yaml: simple_yaml(),
webhook_schedule: vec![(1, push("main", "sha-1"))],
job_duration: 2,
..Default::default()
};
let trace = local_sim::run_simulation(config);
assert!(
local_sim::check_one_at_a_time(&trace),
"at most one job running at a time"
);
}
#[test]
fn property_one_at_a_time_burst() {
let config = LocalSimConfig {
name: "prop-1at1-burst".into(),
num_rounds: 100,
ci_yaml: simple_yaml(),
webhook_schedule: (1..=10)
.map(|i| (1, push(&format!("branch-{i}"), &format!("sha-{i}"))))
.collect(),
job_duration: 2,
..Default::default()
};
let trace = local_sim::run_simulation(config);
assert!(
local_sim::check_one_at_a_time(&trace),
"at most one job running at a time under burst"
);
}
#[test]
fn property_one_at_a_time_staggered() {
let config = LocalSimConfig {
name: "prop-1at1-stagger".into(),
num_rounds: 80,
ci_yaml: simple_yaml(),
webhook_schedule: vec![
(1, push("a", "sha-a")),
(3, push("b", "sha-b")),
(5, push("c", "sha-c")),
(7, push("d", "sha-d")),
],
job_duration: 3,
..Default::default()
};
let trace = local_sim::run_simulation(config);
assert!(
local_sim::check_one_at_a_time(&trace),
"at most one job running at a time under stagger"
);
}
// ─── Property: Supersede correctness ────────────────────────────────────────
#[test]
fn property_superseded_pipelines_never_run() {
let config = LocalSimConfig {
name: "prop-supersede".into(),
num_rounds: 60,
ci_yaml: simple_yaml(),
// Same branch, rapid pushes while jobs are long.
webhook_schedule: (1..=8)
.map(|i| (i, push("feature", &format!("sha-ss-{i}"))))
.collect(),
job_duration: 4,
..Default::default()
};
let trace = local_sim::run_simulation(config);
assert!(
local_sim::check_superseded_no_running(&trace),
"superseded pipelines should never have a running job"
);
}
// ─── Property: Termination ──────────────────────────────────────────────────
#[test]
fn property_all_non_superseded_terminate() {
let config = LocalSimConfig {
name: "prop-terminate".into(),
num_rounds: 100,
ci_yaml: simple_yaml(),
webhook_schedule: vec![
(1, push("a", "sha-t1")),
(2, push("b", "sha-t2")),
(3, push("a", "sha-t3")),
(10, push("c", "sha-t4")),
],
job_duration: 3,
..Default::default()
};
let trace = local_sim::run_simulation(config);
assert!(
local_sim::check_termination(&trace),
"all non-superseded pipelines must reach terminal status"
);
}
#[test]
fn property_all_webhooks_terminate() {
let config = LocalSimConfig {
name: "prop-wh-terminate".into(),
num_rounds: 100,
ci_yaml: simple_yaml(),
webhook_schedule: (1..=5)
.map(|i| (i * 3, push("main", &format!("sha-wh-{i}"))))
.collect(),
job_duration: 2,
..Default::default()
};
let trace = local_sim::run_simulation(config);
assert!(
local_sim::check_all_webhooks_terminate(&trace),
"every webhook must produce a terminal status"
);
}
// ─── Property: DAG ordering ─────────────────────────────────────────────────
#[test]
fn property_dag_ordering_deep_chain() {
let yaml = r#"
pipelines:
deep:
triggers:
- event: push
branches: ["*"]
jobs:
a:
run: echo a
b:
needs: [a]
run: echo b
c:
needs: [b]
run: echo c
d:
needs: [c]
run: echo d
"#;
let config = LocalSimConfig {
name: "prop-dag-deep".into(),
num_rounds: 50,
ci_yaml: yaml.into(),
webhook_schedule: vec![(1, push("main", "sha-dag"))],
job_duration: 2,
..Default::default()
};
let trace = local_sim::run_simulation(config);
assert!(
local_sim::check_dag_ordering(&trace),
"deep DAG ordering must be respected"
);
}
#[test]
fn property_dag_ordering_diamond() {
let yaml = r#"
pipelines:
diamond:
triggers:
- event: push
branches: ["*"]
jobs:
root:
run: echo root
left:
needs: [root]
run: echo left
right:
needs: [root]
run: echo right
merge:
needs: [left, right]
run: echo merge
"#;
let config = LocalSimConfig {
name: "prop-dag-diamond".into(),
num_rounds: 50,
ci_yaml: yaml.into(),
webhook_schedule: vec![(1, push("main", "sha-dia"))],
job_duration: 2,
..Default::default()
};
let trace = local_sim::run_simulation(config);
assert!(
local_sim::check_dag_ordering(&trace),
"diamond DAG ordering must be respected"
);
}
// ─── Property: FIFO across branches ─────────────────────────────────────────
#[test]
fn property_fifo_across_branches() {
let config = LocalSimConfig {
name: "prop-fifo".into(),
num_rounds: 80,
ci_yaml: simple_yaml(),
webhook_schedule: vec![
(1, push("a", "sha-f1")),
(2, push("b", "sha-f2")),
(3, push("c", "sha-f3")),
],
job_duration: 3,
..Default::default()
};
let trace = local_sim::run_simulation(config);
assert!(
local_sim::check_fifo_order(&trace),
"branches must execute in FIFO queue order"
);
}