325 lines
8.8 KiB
Rust
325 lines
8.8 KiB
Rust
|
|
//! Scenario tests for the local CI simulation.
|
||
|
|
//!
|
||
|
|
//! Each test tells a story: set up a scenario, run the simulation, verify outcomes.
|
||
|
|
|
||
|
|
use simulation::ci::local_sim::{self, LocalSimConfig, LocalSimEvent};
|
||
|
|
use swactor_ci::{EventType, WebhookEvent};
|
||
|
|
|
||
|
|
fn basic_ci_yaml() -> String {
|
||
|
|
r#"
|
||
|
|
pipelines:
|
||
|
|
check:
|
||
|
|
triggers:
|
||
|
|
- event: push
|
||
|
|
branches: ["*"]
|
||
|
|
exclude: ["master"]
|
||
|
|
jobs:
|
||
|
|
fmt:
|
||
|
|
run: cargo fmt -- --check
|
||
|
|
clippy:
|
||
|
|
run: cargo clippy
|
||
|
|
test:
|
||
|
|
needs: [fmt, clippy]
|
||
|
|
run: cargo test
|
||
|
|
full:
|
||
|
|
triggers:
|
||
|
|
- event: push
|
||
|
|
branches: ["master"]
|
||
|
|
jobs:
|
||
|
|
test:
|
||
|
|
run: cargo test --all-features
|
||
|
|
timeout: 600
|
||
|
|
bench:
|
||
|
|
needs: [test]
|
||
|
|
run: cargo bench
|
||
|
|
"#
|
||
|
|
.into()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn push_event(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,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Scenario: Single push → single job → passes ────────────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn single_push_runs_and_completes() {
|
||
|
|
let yaml = r#"
|
||
|
|
pipelines:
|
||
|
|
check:
|
||
|
|
triggers:
|
||
|
|
- event: push
|
||
|
|
branches: ["*"]
|
||
|
|
jobs:
|
||
|
|
test:
|
||
|
|
run: cargo test
|
||
|
|
"#;
|
||
|
|
let config = LocalSimConfig {
|
||
|
|
name: "single-push".into(),
|
||
|
|
num_rounds: 20,
|
||
|
|
ci_yaml: yaml.into(),
|
||
|
|
webhook_schedule: vec![(1, push_event("main", "sha1"))],
|
||
|
|
job_duration: 2,
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
|
||
|
|
let trace = local_sim::run_simulation(config);
|
||
|
|
|
||
|
|
let created = trace
|
||
|
|
.events
|
||
|
|
.iter()
|
||
|
|
.filter(|(_, e)| matches!(e, LocalSimEvent::PipelineCreated { .. }))
|
||
|
|
.count();
|
||
|
|
assert_eq!(created, 1);
|
||
|
|
|
||
|
|
let completed = trace
|
||
|
|
.events
|
||
|
|
.iter()
|
||
|
|
.filter(|(_, e)| matches!(e, LocalSimEvent::JobCompleted { passed: true, .. }))
|
||
|
|
.count();
|
||
|
|
assert_eq!(completed, 1);
|
||
|
|
|
||
|
|
assert!(local_sim::check_all_webhooks_terminate(&trace));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Scenario: Push A, push A again while queued → only latest runs ─────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn supersede_queued_same_branch() {
|
||
|
|
let yaml = r#"
|
||
|
|
pipelines:
|
||
|
|
check:
|
||
|
|
triggers:
|
||
|
|
- event: push
|
||
|
|
branches: ["*"]
|
||
|
|
jobs:
|
||
|
|
test:
|
||
|
|
run: cargo test
|
||
|
|
"#;
|
||
|
|
let config = LocalSimConfig {
|
||
|
|
name: "supersede-queued".into(),
|
||
|
|
num_rounds: 30,
|
||
|
|
ci_yaml: yaml.into(),
|
||
|
|
// Push A at round 1 occupies the runner. Push A' at round 2 queues.
|
||
|
|
// Push A'' at round 3 should supersede A'.
|
||
|
|
webhook_schedule: vec![
|
||
|
|
(1, push_event("feature", "sha-a1")),
|
||
|
|
(2, push_event("feature", "sha-a2")),
|
||
|
|
(3, push_event("feature", "sha-a3")),
|
||
|
|
],
|
||
|
|
job_duration: 5,
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
|
||
|
|
let trace = local_sim::run_simulation(config);
|
||
|
|
|
||
|
|
// sha-a2 should be superseded.
|
||
|
|
let superseded = trace
|
||
|
|
.events
|
||
|
|
.iter()
|
||
|
|
.filter(|(_, e)| matches!(e, LocalSimEvent::PipelineSuperseded { .. }))
|
||
|
|
.count();
|
||
|
|
assert!(superseded >= 1, "at least one pipeline should be superseded");
|
||
|
|
|
||
|
|
// sha-a2 should have an "error" status (superseded).
|
||
|
|
let a2_error = trace
|
||
|
|
.status_updates
|
||
|
|
.iter()
|
||
|
|
.any(|u| u.commit_sha == "sha-a2" && u.state == "error");
|
||
|
|
assert!(a2_error, "superseded pipeline should report error status");
|
||
|
|
|
||
|
|
// sha-a1 and sha-a3 should both reach terminal status.
|
||
|
|
let a1_terminal = trace
|
||
|
|
.status_updates
|
||
|
|
.iter()
|
||
|
|
.any(|u| u.commit_sha == "sha-a1" && (u.state == "success" || u.state == "failure"));
|
||
|
|
let a3_terminal = trace
|
||
|
|
.status_updates
|
||
|
|
.iter()
|
||
|
|
.any(|u| u.commit_sha == "sha-a3" && (u.state == "success" || u.state == "failure"));
|
||
|
|
assert!(a1_terminal, "first push should complete");
|
||
|
|
assert!(a3_terminal, "latest push should complete");
|
||
|
|
|
||
|
|
assert!(local_sim::check_superseded_no_running(&trace));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Scenario: Push A, push A while running → running completes, new queues ─
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn push_while_running_does_not_supersede_active() {
|
||
|
|
let yaml = r#"
|
||
|
|
pipelines:
|
||
|
|
check:
|
||
|
|
triggers:
|
||
|
|
- event: push
|
||
|
|
branches: ["*"]
|
||
|
|
jobs:
|
||
|
|
test:
|
||
|
|
run: cargo test
|
||
|
|
"#;
|
||
|
|
let config = LocalSimConfig {
|
||
|
|
name: "no-supersede-active".into(),
|
||
|
|
num_rounds: 30,
|
||
|
|
ci_yaml: yaml.into(),
|
||
|
|
// Push A at round 1 starts running immediately.
|
||
|
|
// Push A' at round 2 should queue (not cancel the running job).
|
||
|
|
webhook_schedule: vec![
|
||
|
|
(1, push_event("feature", "sha-run1")),
|
||
|
|
(2, push_event("feature", "sha-run2")),
|
||
|
|
],
|
||
|
|
job_duration: 4,
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
|
||
|
|
let trace = local_sim::run_simulation(config);
|
||
|
|
|
||
|
|
// Both should reach terminal status.
|
||
|
|
let run1_terminal = trace
|
||
|
|
.status_updates
|
||
|
|
.iter()
|
||
|
|
.any(|u| u.commit_sha == "sha-run1" && u.state == "success");
|
||
|
|
let run2_terminal = trace
|
||
|
|
.status_updates
|
||
|
|
.iter()
|
||
|
|
.any(|u| u.commit_sha == "sha-run2" && u.state == "success");
|
||
|
|
|
||
|
|
assert!(run1_terminal, "running pipeline should complete normally");
|
||
|
|
assert!(run2_terminal, "queued pipeline should run after");
|
||
|
|
|
||
|
|
assert!(local_sim::check_one_at_a_time(&trace));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Scenario: Push A, push B → both run in FIFO order ─────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn different_branches_run_fifo() {
|
||
|
|
let yaml = r#"
|
||
|
|
pipelines:
|
||
|
|
check:
|
||
|
|
triggers:
|
||
|
|
- event: push
|
||
|
|
branches: ["*"]
|
||
|
|
jobs:
|
||
|
|
test:
|
||
|
|
run: cargo test
|
||
|
|
"#;
|
||
|
|
let config = LocalSimConfig {
|
||
|
|
name: "fifo-branches".into(),
|
||
|
|
num_rounds: 30,
|
||
|
|
ci_yaml: yaml.into(),
|
||
|
|
webhook_schedule: vec![
|
||
|
|
(1, push_event("feature-a", "sha-fa")),
|
||
|
|
(1, push_event("feature-b", "sha-fb")),
|
||
|
|
],
|
||
|
|
job_duration: 3,
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
|
||
|
|
let trace = local_sim::run_simulation(config);
|
||
|
|
|
||
|
|
// Both should complete.
|
||
|
|
assert!(local_sim::check_all_webhooks_terminate(&trace));
|
||
|
|
|
||
|
|
// FIFO order respected.
|
||
|
|
assert!(local_sim::check_fifo_order(&trace));
|
||
|
|
|
||
|
|
// One at a time.
|
||
|
|
assert!(local_sim::check_one_at_a_time(&trace));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Scenario: Job failure → dependents skipped → next pipeline starts ──────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn job_failure_skips_dependents_and_advances() {
|
||
|
|
let config = LocalSimConfig {
|
||
|
|
name: "failure-skip-advance".into(),
|
||
|
|
num_rounds: 40,
|
||
|
|
ci_yaml: basic_ci_yaml(),
|
||
|
|
webhook_schedule: vec![
|
||
|
|
(1, push_event("feature-x", "sha-fail")),
|
||
|
|
(2, push_event("feature-y", "sha-next")),
|
||
|
|
],
|
||
|
|
job_duration: 2,
|
||
|
|
// fmt fails.
|
||
|
|
job_failure_schedule: vec![(0, "fmt".into())],
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
|
||
|
|
let trace = local_sim::run_simulation(config);
|
||
|
|
|
||
|
|
// test should be skipped in feature-x pipeline (needs fmt which fails).
|
||
|
|
let test_skipped = trace.events.iter().any(|(_, e)| {
|
||
|
|
matches!(e, LocalSimEvent::JobSkipped { job_id } if job_id.job_name == "test")
|
||
|
|
});
|
||
|
|
assert!(test_skipped, "test should be skipped when fmt fails");
|
||
|
|
|
||
|
|
// Both pipelines should reach terminal.
|
||
|
|
assert!(local_sim::check_all_webhooks_terminate(&trace));
|
||
|
|
|
||
|
|
// One at a time.
|
||
|
|
assert!(local_sim::check_one_at_a_time(&trace));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Scenario: Diamond DAG → jobs serialize respecting deps ─────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn diamond_dag_serialized_with_deps() {
|
||
|
|
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: "diamond-dag".into(),
|
||
|
|
num_rounds: 50,
|
||
|
|
ci_yaml: yaml.into(),
|
||
|
|
webhook_schedule: vec![(1, push_event("main", "sha-diamond"))],
|
||
|
|
job_duration: 2,
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
|
||
|
|
let trace = local_sim::run_simulation(config);
|
||
|
|
|
||
|
|
// All 4 jobs should complete.
|
||
|
|
let completed = trace
|
||
|
|
.events
|
||
|
|
.iter()
|
||
|
|
.filter(|(_, e)| matches!(e, LocalSimEvent::JobCompleted { .. }))
|
||
|
|
.count();
|
||
|
|
assert_eq!(completed, 4, "all 4 diamond jobs should complete");
|
||
|
|
|
||
|
|
// DAG ordering respected.
|
||
|
|
assert!(local_sim::check_dag_ordering(&trace));
|
||
|
|
|
||
|
|
// One at a time (serial).
|
||
|
|
assert!(local_sim::check_one_at_a_time(&trace));
|
||
|
|
|
||
|
|
// Pipeline should succeed.
|
||
|
|
let success = trace
|
||
|
|
.status_updates
|
||
|
|
.iter()
|
||
|
|
.any(|u| u.state == "success" && u.context == "ci/diamond");
|
||
|
|
assert!(success, "diamond pipeline should succeed");
|
||
|
|
}
|