use baraddur::config::{Config, OutputConfig, Step, SummarizeConfig, WatchConfig};
use baraddur::output::Display;
use baraddur::pipeline;
use baraddur::pipeline::StepResult;
#[derive(Default)]
struct RecordingDisplay {
events: Vec<String>,
}
impl Display for RecordingDisplay {
fn run_started(&mut self, step_names: &[String]) {
self.events
.push(format!("run_started:{}", step_names.join(",")));
}
fn step_running(&mut self, name: &str) {
self.events.push(format!("running:{name}"));
}
fn step_finished(&mut self, r: &StepResult) {
self.events
.push(format!("finished:{}:{}", r.name, r.success));
}
fn steps_skipped(&mut self, names: &[String]) {
for name in names {
self.events.push(format!("skipped:{name}"));
}
}
fn run_cancelled(&mut self) {
self.events.push("run_cancelled".into());
}
fn run_finished(&mut self, _results: &[StepResult]) {
self.events.push("run_finished".into());
}
}
fn make_config(steps: Vec<Step>) -> Config {
Config {
watch: WatchConfig {
extensions: vec!["rs".into()],
debounce_ms: 1000,
ignore: vec![],
},
output: OutputConfig::default(),
summarize: SummarizeConfig::default(),
steps,
}
}
#[tokio::test]
async fn sequential_stops_at_first_failure() {
let cfg = make_config(vec![
Step {
name: "first".into(),
cmd: "true".into(),
parallel: false,
},
Step {
name: "second".into(),
cmd: "false".into(),
parallel: false,
},
Step {
name: "third".into(),
cmd: "true".into(),
parallel: false,
},
]);
let mut display = RecordingDisplay::default();
let cwd = std::env::current_dir().unwrap();
let results = pipeline::run_pipeline(&cfg, &cwd, &mut display, None)
.await
.unwrap();
assert_eq!(results.len(), 2);
assert!(results[0].success);
assert!(!results[1].success);
assert!(display.events.contains(&"skipped:third".to_string()));
assert!(
!display
.events
.iter()
.any(|e| e.starts_with("running:third"))
);
}
#[tokio::test]
async fn sequential_all_pass() {
let cfg = make_config(vec![
Step {
name: "a".into(),
cmd: "true".into(),
parallel: false,
},
Step {
name: "b".into(),
cmd: "true".into(),
parallel: false,
},
Step {
name: "c".into(),
cmd: "true".into(),
parallel: false,
},
]);
let mut display = RecordingDisplay::default();
let cwd = std::env::current_dir().unwrap();
let results = pipeline::run_pipeline(&cfg, &cwd, &mut display, None)
.await
.unwrap();
assert_eq!(results.len(), 3);
assert!(results.iter().all(|r| r.success));
}
#[tokio::test]
async fn parallel_steps_all_run() {
let cfg = make_config(vec![
Step {
name: "a".into(),
cmd: "true".into(),
parallel: true,
},
Step {
name: "b".into(),
cmd: "true".into(),
parallel: true,
},
Step {
name: "c".into(),
cmd: "true".into(),
parallel: true,
},
]);
let mut display = RecordingDisplay::default();
let cwd = std::env::current_dir().unwrap();
let results = pipeline::run_pipeline(&cfg, &cwd, &mut display, None)
.await
.unwrap();
assert_eq!(results.len(), 3);
assert!(results.iter().all(|r| r.success));
let events = &display.events;
let running_indices: Vec<usize> = events
.iter()
.enumerate()
.filter(|(_, e)| e.starts_with("running:"))
.map(|(i, _)| i)
.collect();
let first_finished = events
.iter()
.position(|e| e.starts_with("finished:"))
.unwrap();
assert!(
running_indices.iter().all(|&i| i < first_finished),
"all steps should be marked running before any finish"
);
}
#[tokio::test]
async fn parallel_stage_runs_all_even_if_one_fails() {
let cfg = make_config(vec![
Step {
name: "pass".into(),
cmd: "true".into(),
parallel: true,
},
Step {
name: "fail".into(),
cmd: "false".into(),
parallel: true,
},
]);
let mut display = RecordingDisplay::default();
let cwd = std::env::current_dir().unwrap();
let results = pipeline::run_pipeline(&cfg, &cwd, &mut display, None)
.await
.unwrap();
assert_eq!(results.len(), 2);
assert!(results.iter().any(|r| r.success));
assert!(results.iter().any(|r| !r.success));
}
#[tokio::test]
async fn parallel_wall_clock_is_max_not_sum() {
let cfg = make_config(vec![
Step {
name: "slow_a".into(),
cmd: "sleep 0.3".into(),
parallel: true,
},
Step {
name: "slow_b".into(),
cmd: "sleep 0.3".into(),
parallel: true,
},
]);
let mut display = RecordingDisplay::default();
let cwd = std::env::current_dir().unwrap();
let start = std::time::Instant::now();
let results = pipeline::run_pipeline(&cfg, &cwd, &mut display, None)
.await
.unwrap();
let elapsed = start.elapsed();
assert_eq!(results.len(), 2);
assert!(
elapsed.as_secs_f64() < 0.55,
"parallel steps took {:.2}s — expected under 0.55s",
elapsed.as_secs_f64()
);
}
#[tokio::test]
async fn mixed_stages_sequential_then_parallel() {
let cfg = make_config(vec![
Step {
name: "seq".into(),
cmd: "true".into(),
parallel: false,
},
Step {
name: "par_a".into(),
cmd: "true".into(),
parallel: true,
},
Step {
name: "par_b".into(),
cmd: "true".into(),
parallel: true,
},
]);
let mut display = RecordingDisplay::default();
let cwd = std::env::current_dir().unwrap();
let results = pipeline::run_pipeline(&cfg, &cwd, &mut display, None)
.await
.unwrap();
assert_eq!(results.len(), 3);
assert!(results.iter().all(|r| r.success));
let events = &display.events;
let seq_finished = events
.iter()
.position(|e| e == "finished:seq:true")
.unwrap();
let par_a_running = events.iter().position(|e| e == "running:par_a").unwrap();
assert!(seq_finished < par_a_running);
}
#[tokio::test]
async fn stage_failure_skips_subsequent_stages() {
let cfg = make_config(vec![
Step {
name: "fail".into(),
cmd: "false".into(),
parallel: false,
},
Step {
name: "skip_a".into(),
cmd: "true".into(),
parallel: true,
},
Step {
name: "skip_b".into(),
cmd: "true".into(),
parallel: true,
},
]);
let mut display = RecordingDisplay::default();
let cwd = std::env::current_dir().unwrap();
let results = pipeline::run_pipeline(&cfg, &cwd, &mut display, None)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert!(!results[0].success);
assert!(display.events.contains(&"skipped:skip_a".to_string()));
assert!(display.events.contains(&"skipped:skip_b".to_string()));
}
#[tokio::test]
async fn captures_stdout_and_stderr_on_failure() {
let cfg = make_config(vec![Step {
name: "noisyfail".into(),
cmd: "sh -c 'echo out; echo err >&2; exit 1'".into(),
parallel: false,
}]);
let mut display = RecordingDisplay::default();
let cwd = std::env::current_dir().unwrap();
let results = pipeline::run_pipeline(&cfg, &cwd, &mut display, None)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert!(!results[0].success);
assert!(results[0].stdout.contains("out"));
assert!(results[0].stderr.contains("err"));
}