276 lines
7.4 KiB
Rust
276 lines
7.4 KiB
Rust
|
|
//! Property-based tests for the CI simulation.
|
||
|
|
//!
|
||
|
|
//! These verify invariants that should hold across all possible simulation configurations.
|
||
|
|
|
||
|
|
use swactor_ci::{EventType, WebhookEvent};
|
||
|
|
use simulation::ci::sim::{self, CiSimConfig};
|
||
|
|
|
||
|
|
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: Every webhook produces a terminal status ─────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn property_all_webhooks_terminate_single() {
|
||
|
|
let config = CiSimConfig {
|
||
|
|
name: "prop-single".into(),
|
||
|
|
num_rounds: 40,
|
||
|
|
ci_yaml: simple_yaml(),
|
||
|
|
webhook_schedule: vec![(1, push("main", "sha-1"))],
|
||
|
|
provision_latency: 1,
|
||
|
|
job_duration: 2,
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
let trace = sim::run_simulation(config);
|
||
|
|
assert!(
|
||
|
|
sim::check_all_webhooks_terminate(&trace),
|
||
|
|
"single webhook must terminate"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn property_all_webhooks_terminate_burst() {
|
||
|
|
// Burst of webhooks all at once.
|
||
|
|
let config = CiSimConfig {
|
||
|
|
name: "prop-burst".into(),
|
||
|
|
num_rounds: 60,
|
||
|
|
ci_yaml: simple_yaml(),
|
||
|
|
webhook_schedule: (1..=5)
|
||
|
|
.map(|i| (1, push("main", &format!("sha-burst-{i}"))))
|
||
|
|
.collect(),
|
||
|
|
provision_latency: 1,
|
||
|
|
job_duration: 2,
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
let trace = sim::run_simulation(config);
|
||
|
|
assert!(
|
||
|
|
sim::check_all_webhooks_terminate(&trace),
|
||
|
|
"burst of 5 webhooks must all terminate"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn property_all_webhooks_terminate_staggered() {
|
||
|
|
// Webhooks spread across rounds.
|
||
|
|
let config = CiSimConfig {
|
||
|
|
name: "prop-staggered".into(),
|
||
|
|
num_rounds: 60,
|
||
|
|
ci_yaml: simple_yaml(),
|
||
|
|
webhook_schedule: vec![
|
||
|
|
(1, push("main", "sha-s1")),
|
||
|
|
(5, push("main", "sha-s2")),
|
||
|
|
(10, push("main", "sha-s3")),
|
||
|
|
(15, push("main", "sha-s4")),
|
||
|
|
],
|
||
|
|
provision_latency: 2,
|
||
|
|
job_duration: 3,
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
let trace = sim::run_simulation(config);
|
||
|
|
assert!(
|
||
|
|
sim::check_all_webhooks_terminate(&trace),
|
||
|
|
"staggered webhooks must all terminate"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Property: DAG ordering is always respected ─────────────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn property_dag_ordering_always_respected() {
|
||
|
|
// Deep chain: a → b → c → d
|
||
|
|
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 = CiSimConfig {
|
||
|
|
name: "prop-dag-deep".into(),
|
||
|
|
num_rounds: 40,
|
||
|
|
ci_yaml: yaml.into(),
|
||
|
|
webhook_schedule: vec![(1, push("main", "sha-dag"))],
|
||
|
|
provision_latency: 1,
|
||
|
|
job_duration: 2,
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
let trace = sim::run_simulation(config);
|
||
|
|
assert!(
|
||
|
|
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 = CiSimConfig {
|
||
|
|
name: "prop-dag-diamond".into(),
|
||
|
|
num_rounds: 40,
|
||
|
|
ci_yaml: yaml.into(),
|
||
|
|
webhook_schedule: vec![(1, push("main", "sha-diamond"))],
|
||
|
|
provision_latency: 1,
|
||
|
|
job_duration: 2,
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
let trace = sim::run_simulation(config);
|
||
|
|
assert!(
|
||
|
|
sim::check_dag_ordering(&trace),
|
||
|
|
"diamond DAG ordering must be respected"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Property: No instance leaks ────────────────────────────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn property_no_instance_leaks_under_failures() {
|
||
|
|
let config = CiSimConfig {
|
||
|
|
name: "prop-no-leaks-fail".into(),
|
||
|
|
num_rounds: 40,
|
||
|
|
ci_yaml: simple_yaml(),
|
||
|
|
webhook_schedule: vec![
|
||
|
|
(1, push("main", "sha-leak1")),
|
||
|
|
(3, push("main", "sha-leak2")),
|
||
|
|
],
|
||
|
|
provision_latency: 1,
|
||
|
|
job_duration: 2,
|
||
|
|
job_failure_schedule: vec![(0, "fmt".into())],
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
let trace = sim::run_simulation(config);
|
||
|
|
assert!(
|
||
|
|
sim::check_no_instance_leaks(&trace),
|
||
|
|
"no instance leaks even when jobs fail"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn property_no_instance_leaks_under_interruption() {
|
||
|
|
let yaml = r#"
|
||
|
|
pipelines:
|
||
|
|
check:
|
||
|
|
triggers:
|
||
|
|
- event: push
|
||
|
|
branches: ["*"]
|
||
|
|
jobs:
|
||
|
|
test:
|
||
|
|
run: cargo test
|
||
|
|
"#;
|
||
|
|
let config = CiSimConfig {
|
||
|
|
name: "prop-no-leaks-interrupt".into(),
|
||
|
|
num_rounds: 40,
|
||
|
|
ci_yaml: yaml.into(),
|
||
|
|
webhook_schedule: vec![(1, push("main", "sha-int"))],
|
||
|
|
provision_latency: 1,
|
||
|
|
job_duration: 5,
|
||
|
|
instance_interrupt_schedule: vec![(4, "test".into())],
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
let trace = sim::run_simulation(config);
|
||
|
|
assert!(
|
||
|
|
sim::check_no_instance_leaks(&trace),
|
||
|
|
"interrupted instances must be terminated"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Property: Bounded state ────────────────────────────────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn property_bounded_state_under_rapid_pushes() {
|
||
|
|
let config = CiSimConfig {
|
||
|
|
name: "prop-bounded".into(),
|
||
|
|
num_rounds: 100,
|
||
|
|
ci_yaml: simple_yaml(),
|
||
|
|
webhook_schedule: (1..=20)
|
||
|
|
.map(|i| (i, push("main", &format!("sha-rapid-{i}"))))
|
||
|
|
.collect(),
|
||
|
|
provision_latency: 1,
|
||
|
|
job_duration: 2,
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
let trace = sim::run_simulation(config);
|
||
|
|
|
||
|
|
// With 20 pushes, each triggering 1 pipeline with 2 jobs, we should never
|
||
|
|
// have more than 20 active pipelines at once (and in practice much fewer).
|
||
|
|
assert!(
|
||
|
|
sim::check_bounded_state(&trace, 20),
|
||
|
|
"active pipelines should be bounded"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Property: Provisioner offline doesn't lose work ────────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn property_provisioner_offline_eventually_resolves() {
|
||
|
|
let config = CiSimConfig {
|
||
|
|
name: "prop-offline-resolve".into(),
|
||
|
|
num_rounds: 60,
|
||
|
|
ci_yaml: simple_yaml(),
|
||
|
|
webhook_schedule: vec![(3, push("main", "sha-offline"))],
|
||
|
|
provision_latency: 1,
|
||
|
|
job_duration: 2,
|
||
|
|
provisioner_offline_schedule: vec![(1, 15)],
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
let trace = sim::run_simulation(config);
|
||
|
|
assert!(
|
||
|
|
sim::check_all_webhooks_terminate(&trace),
|
||
|
|
"webhooks during provisioner outage must still terminate"
|
||
|
|
);
|
||
|
|
}
|