char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somextract avril-task crate + add avril:task module7f4058a

main
7.6 KiB219 linesraw
1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4
5use avril_task::{Action, Cache, Execution, Graph, Report, RunOptions, Step, Task};
6
7fn task(action: Action, dependencies: &[&str]) -> Task {
8    Task {
9        action,
10        dependencies: dependencies.iter().map(|d| d.to_string()).collect(),
11        ..Task::default()
12    }
13}
14
15fn shell(command: &str, dependencies: &[&str]) -> Task {
16    task(Action::Shell(command.into()), dependencies)
17}
18
19fn graph(tasks: &[(&str, Task)]) -> Graph {
20    Graph {
21        tasks: tasks
22            .iter()
23            .map(|(name, task)| (name.to_string(), task.clone()))
24            .collect(),
25    }
26}
27
28/// A minimal driver, standing in for the CLI and ext drivers.
29fn run(root: &Path, graph: Graph, target: &str, cache: bool) -> Report {
30    let mut options = RunOptions::new(root.to_path_buf());
31    if cache {
32        options.cache = Some(Cache::new(root.join("cache")));
33    }
34    let mut execution = Execution::new(graph, target, options).unwrap();
35    loop {
36        match execution.step().unwrap() {
37            Step::Done(report) => return report,
38            Step::Ready(batch) => {
39                for ready in batch {
40                    let code = match &ready.action {
41                        Action::Shell(command) => Command::new("sh")
42                            .args(["-c", command])
43                            .current_dir(root)
44                            .envs(&ready.env)
45                            .status()
46                            .unwrap()
47                            .code()
48                            .unwrap_or(1) as u8,
49                        Action::Argv(argv) => Command::new(&argv[0])
50                            .args(&argv[1..])
51                            .current_dir(root)
52                            .envs(&ready.env)
53                            .status()
54                            .unwrap()
55                            .code()
56                            .unwrap_or(1) as u8,
57                        other => panic!("test driver cannot run {other:?}"),
58                    };
59                    execution.finish(&ready.name, code);
60                }
61            }
62        }
63    }
64}
65
66fn read(root: &Path, name: &str) -> String {
67    std::fs::read_to_string(root.join(name)).unwrap_or_default()
68}
69
70#[test]
71fn dependencies_run_before_dependents_and_diamonds_run_once() {
72    let dir = tempfile::tempdir().unwrap();
73    let root = dir.path();
74    let g = graph(&[
75        ("all", task(Action::None, &["a", "b"])),
76        ("a", shell("printf a >> log; printf x >> count", &["base"])),
77        ("b", shell("printf b >> log; printf x >> count", &["base"])),
78        ("base", shell("printf . >> log; printf x >> count", &[])),
79    ]);
80    let report = run(root, g, "all", false);
81    assert_eq!(report.code, 0, "{report:?}");
82    assert_eq!(read(root, "log"), ".ab");
83    // base ran once despite being a dependency of both a and b.
84    assert_eq!(read(root, "count"), "xxx");
85}
86
87#[test]
88fn sequential_failure_propagates_its_code_and_stops_the_chain() {
89    let dir = tempfile::tempdir().unwrap();
90    let root = dir.path();
91    let g = graph(&[
92        ("all", task(Action::None, &["bad", "after"])),
93        ("bad", shell("exit 3", &[])),
94        ("after", shell("printf nope >> log", &[])),
95    ]);
96    let report = run(root, g, "all", false);
97    assert_eq!(report.code, 3, "{report:?}");
98    assert_eq!(read(root, "log"), "");
99}
100
101#[test]
102fn parallel_failure_collapses_to_one_and_skips_the_dependent_action() {
103    let dir = tempfile::tempdir().unwrap();
104    let root = dir.path();
105    let mut all = task(Action::Shell("printf nope >> log".into()), &["bad", "ok"]);
106    all.parallel = true;
107    let g = graph(&[
108        ("all", all),
109        ("bad", shell("exit 5", &[])),
110        ("ok", shell("printf ok >> log", &[])),
111    ]);
112    let report = run(root, g, "all", false);
113    assert_eq!(report.code, 1, "{report:?}");
114    assert_eq!(read(root, "log"), "ok");
115    assert_eq!(report.failed.len(), 1);
116    assert_eq!(report.failed[0].name, "bad");
117}
118
119#[test]
120fn cached_tasks_skip_until_an_output_disappears() {
121    let dir = tempfile::tempdir().unwrap();
122    let root = dir.path();
123    std::fs::write(root.join("in.txt"), "input").unwrap();
124    let make = || {
125        let mut t = shell("printf run >> log; printf out > out.txt", &[]);
126        t.files = Some(vec!["in.txt".into()]);
127        t.output = vec!["out.txt".into()];
128        graph(&[("build", t)])
129    };
130    let first = run(root, make(), "build", true);
131    assert_eq!(first.code, 0);
132    let second = run(root, make(), "build", true);
133    assert_eq!(second.cached, ["build"]);
134    assert_eq!(read(root, "log"), "run");
135    std::fs::remove_file(root.join("out.txt")).unwrap();
136    let third = run(root, make(), "build", true);
137    assert!(third.cached.is_empty());
138    assert_eq!(read(root, "log"), "runrun");
139}
140
141#[test]
142fn cycles_and_unknown_tasks_are_errors() {
143    let dir = tempfile::tempdir().unwrap();
144    let root = dir.path();
145    let cyclic = graph(&[
146        ("a", task(Action::None, &["b"])),
147        ("b", task(Action::None, &["a"])),
148    ]);
149    let mut options = RunOptions::new(root.to_path_buf());
150    options.cache = None;
151    match Execution::new(cyclic, "a", options) {
152        Ok(mut execution) => {
153            let error = execution.step().unwrap_err().to_string();
154            assert!(
155                error.contains("Task cycle detected: a -> b -> a"),
156                "{error}"
157            );
158        }
159        Err(error) => {
160            assert!(error.to_string().contains("cycle"), "{error}");
161        }
162    }
163
164    let missing = graph(&[("a", task(Action::None, &["nope"]))]);
165    let mut options = RunOptions::new(root.to_path_buf());
166    options.cache = None;
167    let mut execution = Execution::new(missing, "a", options).unwrap();
168    let error = execution.step().unwrap_err().to_string();
169    assert!(error.contains("Unknown task \"nope\""), "{error}");
170}
171
172#[test]
173fn argv_actions_pass_arguments_without_a_shell() {
174    let dir = tempfile::tempdir().unwrap();
175    let root = dir.path();
176    let g = graph(&[(
177        "check",
178        task(
179            Action::Argv(vec!["test".into(), "a b".into(), "=".into(), "a b".into()]),
180            &[],
181        ),
182    )]);
183    let report = run(root, g, "check", false);
184    assert_eq!(report.code, 0, "{report:?}");
185}
186
187#[test]
188fn env_files_feed_the_task_environment() {
189    let dir = tempfile::tempdir().unwrap();
190    let root = dir.path();
191    std::fs::write(root.join(".env"), "GREETING=hello\n").unwrap();
192    let mut t = shell("printf %s \"$GREETING\" > out.txt", &[]);
193    t.env_file = Some(PathBuf::from(".env"));
194    t.files = Some(vec![]);
195    // `files` must be non-empty to cache in avril.conf, but the engine only
196    // requires the patterns to match; use the env file itself as input.
197    t.files = Some(vec![".env".into()]);
198    t.output = vec!["out.txt".into()];
199    let report = run(root, graph(&[("build", t)]), "build", true);
200    assert_eq!(report.code, 0, "{report:?}");
201    assert_eq!(read(root, "out.txt"), "hello");
202}
203
204#[test]
205fn fingerprints_change_with_inputs() {
206    let dir = tempfile::tempdir().unwrap();
207    let root = dir.path();
208    std::fs::write(root.join("in.txt"), "one").unwrap();
209    let mut t = shell("true", &[]);
210    t.files = Some(vec!["in.txt".into()]);
211    let g = graph(&[("build", t)]);
212    let first = avril_task::graph_fingerprint(&g, root, "build", &[]).unwrap();
213    std::fs::write(root.join("in.txt"), "two!!").unwrap();
214    let second = avril_task::graph_fingerprint(&g, root, "build", &[]).unwrap();
215    assert_ne!(first, second);
216    let mut by_name: BTreeMap<String, String> = BTreeMap::new();
217    by_name.insert("fingerprint".into(), first);
218    assert_eq!(by_name.len(), 1);
219}