Skip to main content

clitest_lib/
lib.rs

1//! This crate provides the core functionality for the `clitest` crate as a library.
2
3pub mod command;
4pub mod output;
5pub mod parser;
6pub mod script;
7pub mod term;
8pub mod util;
9
10use std::path::Path;
11
12use script::{ScriptFile, ScriptOutput, ScriptRunArgs, ScriptRunError};
13
14/// Error returned by [`try_run_captured`] and [`try_run_file_captured`].
15pub struct RunError {
16    pub error: String,
17    pub output: String,
18}
19
20impl std::fmt::Display for RunError {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        write!(f, "{}", self.error)
23    }
24}
25
26impl std::fmt::Debug for RunError {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(f, "{}", self.error)
29    }
30}
31
32fn make_args() -> ScriptRunArgs {
33    ScriptRunArgs {
34        quiet: true,
35        no_color: true,
36        simplified_output: true,
37        ..Default::default()
38    }
39}
40
41fn execute(parsed: &script::Script, output: ScriptOutput) -> Result<(), ScriptRunError> {
42    parsed.run_with_args(make_args(), output)
43}
44
45fn get_inline_file() -> ScriptFile {
46    ScriptFile::new(std::env::current_dir().unwrap().join("<inline>"))
47}
48
49/// Parse and run a clitest script string. Output goes to stdout. Panics on failure.
50pub fn run(script: &str) {
51    let file = get_inline_file();
52    let parsed =
53        parser::parse_script(file, script).unwrap_or_else(|e| panic!("clitest parse error: {e}"));
54    let output = ScriptOutput::no_color();
55    execute(&parsed, output).unwrap_or_else(|e| panic!("clitest failed: {e}"));
56}
57
58/// Parse and run a clitest script string. Output goes to stdout. Panics on failure.
59pub fn run_with_path(path: impl AsRef<Path>, script: &str) {
60    let file = ScriptFile::new(path);
61    let parsed =
62        parser::parse_script(file, script).unwrap_or_else(|e| panic!("clitest parse error: {e}"));
63    let output = ScriptOutput::no_color();
64    execute(&parsed, output).unwrap_or_else(|e| panic!("clitest failed: {e}"));
65}
66
67/// Parse and run a clitest script string. Returns captured output. Panics on failure.
68pub fn run_captured(script: &str) -> String {
69    match try_run_captured(script) {
70        Ok(output) => output,
71        Err(e) => panic!("clitest failed: {}\n\nOutput:\n{}", e.error, e.output),
72    }
73}
74
75/// Parse and run a clitest script string. Returns `Ok(output)` on success,
76/// or `Err(RunError)` with both the error message and captured output on failure.
77pub fn try_run_captured(script: &str) -> Result<String, RunError> {
78    let file = get_inline_file();
79    let parsed = match parser::parse_script(file, script) {
80        Ok(s) => s,
81        Err(e) => {
82            return Err(RunError {
83                error: e.to_string(),
84                output: String::new(),
85            });
86        }
87    };
88    let output = ScriptOutput::quiet(true);
89    match execute(&parsed, output.clone()) {
90        Ok(()) => Ok(output.take_buffer()),
91        Err(e) => Err(RunError {
92            error: e.to_string(),
93            output: output.take_buffer(),
94        }),
95    }
96}
97
98/// Parse and run a clitest script file. Output goes to stdout. Panics on failure.
99pub fn run_file(path: impl AsRef<Path>) {
100    let file = ScriptFile::new(path);
101    let parsed = parser::parse_script_file(None, file)
102        .unwrap_or_else(|e| panic!("clitest parse error: {:?}", e));
103    let output = ScriptOutput::no_color();
104    execute(&parsed, output).unwrap_or_else(|e| panic!("clitest failed: {e}"));
105}
106
107/// Parse and run a clitest script file. Returns captured output. Panics on failure.
108pub fn run_file_captured(path: impl AsRef<Path>) -> String {
109    match try_run_file_captured(path) {
110        Ok(output) => output,
111        Err(e) => panic!("clitest failed: {}\n\nOutput:\n{}", e.error, e.output),
112    }
113}
114
115/// Parse and run a clitest script file. Returns `Ok(output)` on success,
116/// or `Err(RunError)` with the error message and captured output on failure.
117pub fn try_run_file_captured(path: impl AsRef<Path>) -> Result<String, RunError> {
118    let file = ScriptFile::new(path);
119    let parsed = match parser::parse_script_file(None, file) {
120        Ok(s) => s,
121        Err(e) => {
122            let msg = e
123                .iter()
124                .map(|e| e.to_string())
125                .collect::<Vec<_>>()
126                .join("\n");
127            return Err(RunError {
128                error: msg,
129                output: String::new(),
130            });
131        }
132    };
133    let output = ScriptOutput::quiet(true);
134    match execute(&parsed, output.clone()) {
135        Ok(()) => Ok(output.take_buffer()),
136        Err(e) => Err(RunError {
137            error: e.to_string(),
138            output: output.take_buffer(),
139        }),
140    }
141}
142
143/// Generate `#[test]` functions from inline clitest scripts. The `PWD` for the
144/// script is set to the current directory, which for `cargo test` is the root
145/// of the crate.
146///
147/// ```rust
148/// use clitest_lib::clitest;
149///
150/// clitest!(my_test, r#"
151/// $ echo hello
152/// ! hello
153/// "#);
154/// ```
155#[macro_export]
156macro_rules! clitest {
157    ($name:ident, $script:expr) => {
158        #[test]
159        fn $name() {
160            $crate::run_with_path(std::env::current_dir().unwrap(), &format!("#!/usr/bin/env clitest --v0\n{}", $script));
161        }
162    };
163}