use std::cell::RefCell;
use std::collections::HashMap;
use slash_lang::parser::ast::Command;
use slash_lang::parser::parse;
use slash_lib::executor::{
CommandOutput, CommandRunner, Execute, ExecutionError, Executor, PipeValue,
};
struct CapturingRunner {
outcomes: HashMap<&'static str, (Option<Vec<u8>>, bool)>,
calls: RefCell<Vec<(String, bool)>>,
}
impl CapturingRunner {
fn new() -> Self {
Self {
outcomes: HashMap::new(),
calls: RefCell::new(vec![]),
}
}
fn with(mut self, name: &'static str, stdout: Option<&str>, success: bool) -> Self {
self.outcomes
.insert(name, (stdout.map(|s| s.as_bytes().to_vec()), success));
self
}
fn calls(&self) -> Vec<(String, bool)> {
self.calls.borrow().clone()
}
}
impl CommandRunner for CapturingRunner {
fn run(
&self,
cmd: &Command,
input: Option<&PipeValue>,
) -> Result<CommandOutput, ExecutionError> {
let had_input = input.is_some();
self.calls.borrow_mut().push((cmd.name.clone(), had_input));
let (stdout, success) = self
.outcomes
.get(cmd.name.as_str())
.cloned()
.unwrap_or((None, true));
Ok(CommandOutput {
stdout,
stderr: None,
success,
})
}
}
fn executor(runner: CapturingRunner) -> Executor<CapturingRunner> {
Executor::new(runner)
}
#[test]
fn single_command_is_dispatched() {
let runner = CapturingRunner::new().with("build", Some("ok"), true);
let ex = executor(runner);
let prog = parse("/build").unwrap();
let result = ex.execute(&prog).unwrap();
assert!(matches!(result, Some(PipeValue::Bytes(_))));
assert_eq!(ex.into_runner().calls(), vec![("build".to_string(), false)]);
}
#[test]
fn and_skips_second_pipeline_on_failure() {
let runner = CapturingRunner::new().with("lint", None, false);
let ex = executor(runner);
let prog = parse("/lint && /deploy").unwrap();
ex.execute(&prog).unwrap();
let calls: Vec<_> = ex
.into_runner()
.calls()
.into_iter()
.map(|(n, _)| n)
.collect();
assert_eq!(calls, vec!["lint"]);
}
#[test]
fn and_runs_second_pipeline_on_success() {
let runner = CapturingRunner::new().with("lint", None, true);
let ex = executor(runner);
let prog = parse("/lint && /deploy").unwrap();
ex.execute(&prog).unwrap();
let calls: Vec<_> = ex
.into_runner()
.calls()
.into_iter()
.map(|(n, _)| n)
.collect();
assert_eq!(calls, vec!["lint", "deploy"]);
}
#[test]
fn or_skips_second_pipeline_on_success() {
let runner = CapturingRunner::new().with("primary", None, true);
let ex = executor(runner);
let prog = parse("/primary || /fallback").unwrap();
ex.execute(&prog).unwrap();
let calls: Vec<_> = ex
.into_runner()
.calls()
.into_iter()
.map(|(n, _)| n)
.collect();
assert_eq!(calls, vec!["primary"]);
}
#[test]
fn or_runs_second_pipeline_on_failure() {
let runner = CapturingRunner::new().with("primary", None, false);
let ex = executor(runner);
let prog = parse("/primary || /fallback").unwrap();
ex.execute(&prog).unwrap();
let calls: Vec<_> = ex
.into_runner()
.calls()
.into_iter()
.map(|(n, _)| n)
.collect();
assert_eq!(calls, vec!["primary", "fallback"]);
}
#[test]
fn pipe_passes_stdout_to_next_command() {
let runner = CapturingRunner::new().with("lint", Some("report"), true);
let ex = executor(runner);
let prog = parse("/lint | /format").unwrap();
ex.execute(&prog).unwrap();
let calls = ex.into_runner().calls();
assert_eq!(
calls,
vec![("lint".to_string(), false), ("format".to_string(), true)]
);
}
#[test]
fn pipe_chain_threads_input() {
let runner =
CapturingRunner::new()
.with("a", Some("data"), true)
.with("b", Some("transformed"), true);
let ex = executor(runner);
let prog = parse("/a | /b | /c").unwrap();
ex.execute(&prog).unwrap();
let calls = ex.into_runner().calls();
assert_eq!(
calls,
vec![
("a".to_string(), false),
("b".to_string(), true),
("c".to_string(), true),
]
);
}
#[test]
fn optional_pipe_chain_parses() {
let prog = parse("/foo? | /bar? | /baz").unwrap();
let pipeline = &prog.pipelines[0];
assert_eq!(pipeline.commands.len(), 3);
assert!(pipeline.commands[0].optional);
assert!(pipeline.commands[1].optional);
assert!(!pipeline.commands[2].optional);
}
#[test]
fn optional_commands_accumulate_and_terminal_receives_context() {
let runner = CapturingRunner::new()
.with("foo", Some("foo-out"), true)
.with("bar", Some("bar-out"), true);
let ex = executor(runner);
let prog = parse("/foo? | /bar? | /baz").unwrap();
ex.execute(&prog).unwrap();
let calls = ex.into_runner().calls();
assert_eq!(calls.len(), 3);
assert_eq!(calls[0], ("foo".to_string(), false));
assert_eq!(calls[1], ("bar".to_string(), false));
assert_eq!(calls[2], ("baz".to_string(), true));
}
#[test]
fn optional_with_no_output_inserts_null_in_context() {
let runner = CapturingRunner::new().with("foo", None, true);
let ex = executor(runner);
let prog = parse("/foo? | /baz").unwrap();
ex.execute(&prog).unwrap();
let calls = ex.into_runner().calls();
assert_eq!(calls[1], ("baz".to_string(), true)); }
#[test]
fn three_pipeline_and_chain_stops_on_first_failure() {
let runner = CapturingRunner::new()
.with("a", None, true)
.with("b", None, false);
let ex = executor(runner);
let prog = parse("/a && /b && /c").unwrap();
ex.execute(&prog).unwrap();
let calls: Vec<_> = ex
.into_runner()
.calls()
.into_iter()
.map(|(n, _)| n)
.collect();
assert_eq!(calls, vec!["a", "b"]); }