1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
//! The steps a proof-of-concept run executes.
//!
//! A real executor reads `.knotless/workflows/*.yml` through
//! `knotless::workflow`. The proof of concept keeps the same per-step
//! shape with a tab-separated file: `name<TAB>script`, one step a line.
//! Blank lines and lines starting with `#` are ignored.
use std::fs;
use std::path::Path;
use crate::cli::Options;
/// One step: its name in the check, and the script `sh -ec` runs.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Step {
pub name: String,
pub script: String,
}
/// The steps `options` asks for: `--script` as one step, or `--steps-file`.
pub fn load(options: &Options) -> Result<Vec<Step>, String> {
match (&options.script, &options.steps_file) {
(Some(script), None) => Ok(vec![Step {
name: "run".to_string(),
script: script.clone(),
}]),
(None, Some(path)) => parse_file(path),
(Some(_), Some(_)) => Err("give either --script or --steps-file, not both".to_string()),
(None, None) => Err("give --script or --steps-file".to_string()),
}
}
/// Reads and parses a tab-separated steps file.
pub fn parse_file(path: &Path) -> Result<Vec<Step>, String> {
let text = fs::read_to_string(path)
.map_err(|err| format!("reading {}: {err}", path.display()))?;
parse(&text).map_err(|err| format!("{}: {err}", path.display()))
}
/// Parses tab-separated steps.
pub fn parse(text: &str) -> Result<Vec<Step>, String> {
let mut steps = Vec::new();
for (index, line) in text.lines().enumerate() {
let number = index + 1;
if line.trim().is_empty() || line.trim_start().starts_with('#') {
continue;
}
let Some((name, script)) = line.split_once('\t') else {
return Err(format!(
"line {number}: expected name<TAB>script, found no tab"
));
};
let name = name.trim();
if name.is_empty() {
return Err(format!("line {number}: step name is empty"));
}
if script.trim().is_empty() {
return Err(format!("line {number}: step {name} has no script"));
}
steps.push(Step {
name: name.to_string(),
script: script.to_string(),
});
}
if steps.is_empty() {
return Err("no steps".to_string());
}
Ok(steps)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_steps() {
let steps = parse("# comment\n\nbuild\tcargo build\ntest\tcargo test\n").unwrap();
assert_eq!(
steps,
vec![
Step {
name: "build".into(),
script: "cargo build".into()
},
Step {
name: "test".into(),
script: "cargo test".into()
}
]
);
}
#[test]
fn rejects_bad_lines() {
assert!(parse("no tab here\n").unwrap_err().contains("no tab"));
assert!(parse("\tmake\n").unwrap_err().contains("empty"));
assert!(parse("build\t\n").unwrap_err().contains("no script"));
assert!(parse("\n").unwrap_err().contains("no steps"));
}
}