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 captured output. Panics on failure.
76pub fn run_with_path_captured(name: &str, path: impl AsRef<Path>, script: &str) -> String {
77    let file = ScriptFile::new(dunce::canonicalize(path.as_ref()).unwrap().join(name));
78    let parsed = match parser::parse_script(file, script) {
79        Ok(s) => s,
80        Err(e) => panic!("clitest parse error: {e}"),
81    };
82    let output = ScriptOutput::quiet(true);
83    match execute(&parsed, output.clone()) {
84        Ok(()) => output.take_buffer(),
85        Err(e) => panic!("clitest failed: {e}\n\nOutput:\n{}", output.take_buffer()),
86    }
87}
88
89/// Parse and run a clitest script string. Returns `Ok(output)` on success,
90/// or `Err(RunError)` with both the error message and captured output on failure.
91pub fn try_run_captured(script: &str) -> Result<String, RunError> {
92    let file = get_inline_file();
93    let parsed = match parser::parse_script(file, script) {
94        Ok(s) => s,
95        Err(e) => {
96            return Err(RunError {
97                error: e.to_string(),
98                output: String::new(),
99            });
100        }
101    };
102    let output = ScriptOutput::quiet(true);
103    match execute(&parsed, output.clone()) {
104        Ok(()) => Ok(output.take_buffer()),
105        Err(e) => Err(RunError {
106            error: e.to_string(),
107            output: output.take_buffer(),
108        }),
109    }
110}
111
112/// Parse and run a clitest script file. Output goes to stdout. Panics on failure.
113pub fn run_file(path: impl AsRef<Path>) {
114    let file = ScriptFile::new(path);
115    let parsed = parser::parse_script_file(None, file)
116        .unwrap_or_else(|e| panic!("clitest parse error: {:?}", e));
117    let output = ScriptOutput::no_color();
118    execute(&parsed, output).unwrap_or_else(|e| panic!("clitest failed: {e}"));
119}
120
121/// Parse and run a clitest script file. Returns captured output. Panics on failure.
122pub fn run_file_captured(path: impl AsRef<Path>) -> String {
123    match try_run_file_captured(path) {
124        Ok(output) => output,
125        Err(e) => panic!("clitest failed: {}\n\nOutput:\n{}", e.error, e.output),
126    }
127}
128
129/// Parse and run a clitest script file. Returns `Ok(output)` on success,
130/// or `Err(RunError)` with the error message and captured output on failure.
131pub fn try_run_file_captured(path: impl AsRef<Path>) -> Result<String, RunError> {
132    let file = ScriptFile::new(path);
133    let parsed = match parser::parse_script_file(None, file) {
134        Ok(s) => s,
135        Err(e) => {
136            let msg = e
137                .iter()
138                .map(|e| e.to_string())
139                .collect::<Vec<_>>()
140                .join("\n");
141            return Err(RunError {
142                error: msg,
143                output: String::new(),
144            });
145        }
146    };
147    let output = ScriptOutput::quiet(true);
148    match execute(&parsed, output.clone()) {
149        Ok(()) => Ok(output.take_buffer()),
150        Err(e) => Err(RunError {
151            error: e.to_string(),
152            output: output.take_buffer(),
153        }),
154    }
155}
156
157/// Generate `#[test]` functions from inline clitest scripts. The `PWD` for the
158/// script is set to the current directory, which for `cargo test` is the root
159/// of the crate.
160///
161/// ```rust
162/// use clitest_lib::clitest;
163///
164/// clitest!(my_test, r#"
165/// $ echo hello
166/// ! hello
167/// "#);
168/// ```
169#[macro_export]
170macro_rules! clitest {
171    ($name:ident, $script:expr) => {
172        #[test]
173        fn $name() {
174            let output = $crate::run_with_path_captured(stringify!($name), std::env::current_dir().unwrap(), &format!("#!/usr/bin/env clitest --v0\n{}", $script));
175            eprintln!("{output}");
176        }
177    };
178}
179
180clitest!(test_run_macro, r#"
181$ echo $PWD
182*
183"#);