//! 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: `namescript`, 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, 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, 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, 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 namescript, 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")); } }