use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::process::Command; use avril_task::{Action, Cache, Execution, Graph, Report, RunOptions, Step, Task}; fn task(action: Action, dependencies: &[&str]) -> Task { Task { action, dependencies: dependencies.iter().map(|d| d.to_string()).collect(), ..Task::default() } } fn shell(command: &str, dependencies: &[&str]) -> Task { task(Action::Shell(command.into()), dependencies) } fn graph(tasks: &[(&str, Task)]) -> Graph { Graph { tasks: tasks .iter() .map(|(name, task)| (name.to_string(), task.clone())) .collect(), } } /// A minimal driver, standing in for the CLI and ext drivers. fn run(root: &Path, graph: Graph, target: &str, cache: bool) -> Report { let mut options = RunOptions::new(root.to_path_buf()); if cache { options.cache = Some(Cache::new(root.join("cache"))); } let mut execution = Execution::new(graph, target, options).unwrap(); loop { match execution.step().unwrap() { Step::Done(report) => return report, Step::Ready(batch) => { for ready in batch { let code = match &ready.action { Action::Shell(command) => Command::new("sh") .args(["-c", command]) .current_dir(root) .envs(&ready.env) .status() .unwrap() .code() .unwrap_or(1) as u8, Action::Argv(argv) => Command::new(&argv[0]) .args(&argv[1..]) .current_dir(root) .envs(&ready.env) .status() .unwrap() .code() .unwrap_or(1) as u8, other => panic!("test driver cannot run {other:?}"), }; execution.finish(&ready.name, code); } } } } } fn read(root: &Path, name: &str) -> String { std::fs::read_to_string(root.join(name)).unwrap_or_default() } #[test] fn dependencies_run_before_dependents_and_diamonds_run_once() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); let g = graph(&[ ("all", task(Action::None, &["a", "b"])), ("a", shell("printf a >> log; printf x >> count", &["base"])), ("b", shell("printf b >> log; printf x >> count", &["base"])), ("base", shell("printf . >> log; printf x >> count", &[])), ]); let report = run(root, g, "all", false); assert_eq!(report.code, 0, "{report:?}"); assert_eq!(read(root, "log"), ".ab"); // base ran once despite being a dependency of both a and b. assert_eq!(read(root, "count"), "xxx"); } #[test] fn sequential_failure_propagates_its_code_and_stops_the_chain() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); let g = graph(&[ ("all", task(Action::None, &["bad", "after"])), ("bad", shell("exit 3", &[])), ("after", shell("printf nope >> log", &[])), ]); let report = run(root, g, "all", false); assert_eq!(report.code, 3, "{report:?}"); assert_eq!(read(root, "log"), ""); } #[test] fn parallel_failure_collapses_to_one_and_skips_the_dependent_action() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); let mut all = task(Action::Shell("printf nope >> log".into()), &["bad", "ok"]); all.parallel = true; let g = graph(&[ ("all", all), ("bad", shell("exit 5", &[])), ("ok", shell("printf ok >> log", &[])), ]); let report = run(root, g, "all", false); assert_eq!(report.code, 1, "{report:?}"); assert_eq!(read(root, "log"), "ok"); assert_eq!(report.failed.len(), 1); assert_eq!(report.failed[0].name, "bad"); } #[test] fn cached_tasks_skip_until_an_output_disappears() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); std::fs::write(root.join("in.txt"), "input").unwrap(); let make = || { let mut t = shell("printf run >> log; printf out > out.txt", &[]); t.files = Some(vec!["in.txt".into()]); t.output = vec!["out.txt".into()]; graph(&[("build", t)]) }; let first = run(root, make(), "build", true); assert_eq!(first.code, 0); let second = run(root, make(), "build", true); assert_eq!(second.cached, ["build"]); assert_eq!(read(root, "log"), "run"); std::fs::remove_file(root.join("out.txt")).unwrap(); let third = run(root, make(), "build", true); assert!(third.cached.is_empty()); assert_eq!(read(root, "log"), "runrun"); } #[test] fn cycles_and_unknown_tasks_are_errors() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); let cyclic = graph(&[ ("a", task(Action::None, &["b"])), ("b", task(Action::None, &["a"])), ]); let mut options = RunOptions::new(root.to_path_buf()); options.cache = None; match Execution::new(cyclic, "a", options) { Ok(mut execution) => { let error = execution.step().unwrap_err().to_string(); assert!( error.contains("Task cycle detected: a -> b -> a"), "{error}" ); } Err(error) => { assert!(error.to_string().contains("cycle"), "{error}"); } } let missing = graph(&[("a", task(Action::None, &["nope"]))]); let mut options = RunOptions::new(root.to_path_buf()); options.cache = None; let mut execution = Execution::new(missing, "a", options).unwrap(); let error = execution.step().unwrap_err().to_string(); assert!(error.contains("Unknown task \"nope\""), "{error}"); } #[test] fn argv_actions_pass_arguments_without_a_shell() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); let g = graph(&[( "check", task( Action::Argv(vec!["test".into(), "a b".into(), "=".into(), "a b".into()]), &[], ), )]); let report = run(root, g, "check", false); assert_eq!(report.code, 0, "{report:?}"); } #[test] fn env_files_feed_the_task_environment() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); std::fs::write(root.join(".env"), "GREETING=hello\n").unwrap(); let mut t = shell("printf %s \"$GREETING\" > out.txt", &[]); t.env_file = Some(PathBuf::from(".env")); t.files = Some(vec![]); // `files` must be non-empty to cache in avril.conf, but the engine only // requires the patterns to match; use the env file itself as input. t.files = Some(vec![".env".into()]); t.output = vec!["out.txt".into()]; let report = run(root, graph(&[("build", t)]), "build", true); assert_eq!(report.code, 0, "{report:?}"); assert_eq!(read(root, "out.txt"), "hello"); } #[test] fn fingerprints_change_with_inputs() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); std::fs::write(root.join("in.txt"), "one").unwrap(); let mut t = shell("true", &[]); t.files = Some(vec!["in.txt".into()]); let g = graph(&[("build", t)]); let first = avril_task::graph_fingerprint(&g, root, "build", &[]).unwrap(); std::fs::write(root.join("in.txt"), "two!!").unwrap(); let second = avril_task::graph_fingerprint(&g, root, "build", &[]).unwrap(); assert_ne!(first, second); let mut by_name: BTreeMap = BTreeMap::new(); by_name.insert("fingerprint".into(), first); assert_eq!(by_name.len(), 1); }