~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless-runner-k8s

0
0
100 lines · 3.2 KB Raw History
//! 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"));
    }
}