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