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