clitest_lib/
script.rs

1use std::{
2    collections::{HashMap, VecDeque},
3    path::Path,
4    process::ExitStatus,
5    sync::{Arc, Mutex, atomic::AtomicBool},
6    thread::ScopedJoinHandle,
7    time::{Duration, Instant},
8};
9
10use grok::Grok;
11use keepcalm::SharedMut;
12use serde::{Serialize, ser::SerializeMap};
13use termcolor::{Color, ColorChoice, WriteColor};
14
15use crate::{
16    command::{CommandLine, CommandResult},
17    util::{NicePathBuf, NiceTempDir},
18};
19use crate::{cwrite, cwriteln, cwriteln_rule};
20use crate::{output::*, util::ShellBit};
21
22const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
23
24#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
25pub struct ScriptLocation {
26    pub file: ScriptFile,
27    pub line: usize,
28}
29
30impl ScriptLocation {
31    pub fn new(file: ScriptFile, line: usize) -> Self {
32        Self { file, line }
33    }
34}
35
36impl std::fmt::Display for ScriptLocation {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        write!(f, "{}:{}", self.file, self.line)
39    }
40}
41
42#[derive(derive_more::Debug, derive_more::Display, Clone, Serialize, PartialEq, Eq)]
43#[display("{}", file)]
44pub struct ScriptFile {
45    pub file: Arc<NicePathBuf>,
46}
47
48impl ScriptFile {
49    pub fn new(file: impl AsRef<Path>) -> Self {
50        Self {
51            file: Arc::new(NicePathBuf::new(file)),
52        }
53    }
54}
55
56#[derive(derive_more::Debug, Serialize)]
57pub struct Script {
58    pub commands: Vec<ScriptBlock>,
59    pub file: ScriptFile,
60    #[debug(skip)]
61    #[serde(skip)]
62    pub grok: Grok,
63}
64
65#[derive(Debug, Clone, Default)]
66pub struct ScriptRunArgs {
67    pub delay_steps: Option<u64>,
68    pub ignore_exit_codes: bool,
69    pub ignore_matches: bool,
70    pub simplified_output: bool,
71    pub show_line_numbers: bool,
72    pub runner: Option<String>,
73    pub quiet: bool,
74    pub verbose: bool,
75    pub global_timeout: Option<Duration>,
76    pub no_color: bool,
77}
78
79#[derive(derive_more::Debug, Clone)]
80pub struct ScriptOutput {
81    #[debug(skip)]
82    stream: SharedMut<Box<dyn WriteColorAny>>,
83}
84
85trait WriteColorAny: WriteColor + Send + Sync + std::any::Any + 'static + std::fmt::Debug {
86    /// Workaround for lack of upcasting
87    fn take_buffer(self: Box<Self>) -> Result<termcolor::Buffer, String>;
88    fn clone_buffer(&self) -> Result<termcolor::Buffer, String>;
89}
90
91impl WriteColorAny for termcolor::StandardStream {
92    fn take_buffer(self: Box<Self>) -> Result<termcolor::Buffer, String> {
93        Err("not a buffer".to_string())
94    }
95    fn clone_buffer(&self) -> Result<termcolor::Buffer, String> {
96        Err("not a buffer".to_string())
97    }
98}
99
100impl WriteColorAny for termcolor::Buffer {
101    fn take_buffer(self: Box<Self>) -> Result<termcolor::Buffer, String> {
102        Ok(*self)
103    }
104    fn clone_buffer(&self) -> Result<termcolor::Buffer, String> {
105        Ok(self.clone())
106    }
107}
108
109impl ScriptOutput {
110    pub fn no_color() -> Self {
111        let stm = termcolor::StandardStream::stdout(ColorChoice::Never);
112        Self {
113            stream: SharedMut::new(Box::new(stm) as _),
114        }
115    }
116
117    pub fn quiet(no_color: bool) -> Self {
118        let stm = if no_color {
119            termcolor::Buffer::no_color()
120        } else {
121            termcolor::Buffer::ansi()
122        };
123        Self {
124            stream: SharedMut::new(Box::new(stm) as _),
125        }
126    }
127
128    pub fn take_buffer(self) -> String {
129        let stream = match SharedMut::try_unwrap(self.stream) {
130            Ok(stream) => stream.take_buffer().expect("wrong stream type"),
131            Err(shared) => shared.read().clone_buffer().expect("wrong stream type"),
132        };
133        String::from_utf8_lossy(&stream.into_inner()).to_string()
134    }
135}
136
137impl Default for ScriptOutput {
138    fn default() -> Self {
139        let stm = termcolor::StandardStream::stdout(ColorChoice::Auto);
140        Self {
141            stream: SharedMut::new(Box::new(stm) as _),
142        }
143    }
144}
145
146impl std::io::Write for ScriptOutputLock<'_> {
147    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
148        self.stream.write(buf)
149    }
150    fn flush(&mut self) -> std::io::Result<()> {
151        self.stream.flush()
152    }
153}
154
155impl termcolor::WriteColor for ScriptOutputLock<'_> {
156    fn supports_color(&self) -> bool {
157        self.stream.supports_color()
158    }
159    fn set_color(&mut self, spec: &termcolor::ColorSpec) -> std::io::Result<()> {
160        self.stream.set_color(spec)
161    }
162    fn reset(&mut self) -> std::io::Result<()> {
163        self.stream.reset()
164    }
165    fn is_synchronous(&self) -> bool {
166        self.stream.is_synchronous()
167    }
168    fn set_hyperlink(&mut self, _link: &termcolor::HyperlinkSpec) -> std::io::Result<()> {
169        self.stream.set_hyperlink(_link)
170    }
171    fn supports_hyperlinks(&self) -> bool {
172        self.stream.supports_hyperlinks()
173    }
174}
175
176struct ScriptOutputLock<'a> {
177    stream: keepcalm::SharedWriteLock<'a, Box<dyn WriteColorAny>>,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181enum ScriptMode {
182    Normal,
183    Deferred,
184    Background,
185}
186
187#[derive(derive_more::Debug)]
188pub struct ScriptRunContext {
189    pub args: ScriptRunArgs,
190    timeout: Duration,
191    env_vars: HashMap<String, String>,
192    background: ScriptMode,
193    #[debug(skip)]
194    kill: ScriptKillReceiver,
195    #[debug(skip)]
196    kill_sender: ScriptKillSender,
197    output: ScriptOutput,
198}
199
200impl Default for ScriptRunContext {
201    fn default() -> Self {
202        let kill = Arc::new(AtomicBool::new(false));
203        Self {
204            args: ScriptRunArgs::default(),
205            timeout: DEFAULT_TIMEOUT,
206            env_vars: HashMap::new(),
207            background: ScriptMode::Normal,
208            kill: ScriptKillReceiver::new(kill.clone()),
209            kill_sender: ScriptKillSender::new(kill.clone()),
210            output: ScriptOutput::default(),
211        }
212    }
213}
214
215impl ScriptRunContext {
216    pub fn new_background(&self) -> Self {
217        let kill = Arc::new(AtomicBool::new(false));
218        Self {
219            args: self.args.clone(),
220            // Background processes are not subject to timeouts
221            timeout: Duration::MAX,
222            env_vars: self.env_vars.clone(),
223            background: ScriptMode::Background,
224            kill: ScriptKillReceiver::new(kill.clone()),
225            kill_sender: ScriptKillSender::new(kill.clone()),
226            output: if self.args.verbose {
227                self.output.clone()
228            } else {
229                ScriptOutput::quiet(self.args.no_color)
230            },
231        }
232    }
233
234    pub fn new_deferred(&self) -> Self {
235        Self {
236            args: self.args.clone(),
237            timeout: self.timeout,
238            env_vars: self.env_vars.clone(),
239            background: ScriptMode::Deferred,
240            kill: self.kill.clone(),
241            kill_sender: self.kill_sender.clone(),
242            output: self.output.clone(),
243        }
244    }
245
246    pub fn pwd(&self) -> NicePathBuf {
247        self.env_vars
248            .get("PWD")
249            .cloned()
250            .map(NicePathBuf::from)
251            .unwrap_or_else(NicePathBuf::cwd)
252    }
253
254    pub fn get_env(&self, name: &str) -> Option<&str> {
255        self.env_vars.get(name).map(|s| s.as_str())
256    }
257
258    pub fn set_env(&mut self, name: impl Into<String>, value: impl Into<String>) {
259        let name = name.into();
260        if name == "PWD" {
261            self.set_pwd(value.into());
262        } else {
263            self.env_vars.insert(name, value.into());
264        }
265    }
266
267    pub fn set_pwd(&mut self, pwd: impl Into<NicePathBuf>) {
268        let pwd = pwd.into().env_string();
269        self.env_vars.insert("PWD".to_string(), pwd);
270    }
271
272    pub fn take_output(self) -> String {
273        self.output.take_buffer()
274    }
275
276    fn expand(&self, value: &ShellBit) -> Result<String, ScriptRunError> {
277        match value {
278            ShellBit::Literal(s) => Ok(s.clone()),
279            ShellBit::Quoted(s) => self.expand_str(s),
280        }
281    }
282
283    /// Perform shell expansion on a string.
284    fn expand_str(&self, value: impl AsRef<str>) -> Result<String, ScriptRunError> {
285        enum State {
286            Normal,
287            EscapeNext,
288            InCurly,
289            Dollar,
290            InDollar,
291        }
292
293        let value = value.as_ref();
294
295        // "\" triggers escaping
296        // ${A} expands to the value of A
297        // $A expands to the value of A (variable ends on first non-alphanumeric character)
298
299        let mut state = State::Normal;
300        let mut variable = String::new();
301        let mut expanded = String::new();
302
303        for c in value.chars() {
304            match state {
305                State::Normal => {
306                    if c == '$' {
307                        state = State::Dollar;
308                        continue;
309                    }
310                    if c == '\\' {
311                        state = State::EscapeNext;
312                        continue;
313                    }
314                    expanded.push(c);
315                }
316                State::EscapeNext => {
317                    expanded.push(c);
318                    state = State::Normal;
319                }
320                State::InCurly => {
321                    if c == '}' {
322                        if let Some(value) = self.get_env(&std::mem::take(&mut variable)) {
323                            expanded.push_str(value);
324                        } else {
325                            return Err(ScriptRunError::ExpansionError(format!(
326                                "undefined variable in ${{...}}: {:?} (in {value:?})",
327                                variable
328                            )));
329                        }
330                        state = State::Normal;
331                    } else {
332                        variable.push(c);
333                    }
334                }
335                State::Dollar => {
336                    if c.is_alphanumeric() || c == '_' {
337                        state = State::InDollar;
338                        variable.push(c);
339                    } else if c == '{' {
340                        state = State::InCurly;
341                    } else {
342                        return Err(ScriptRunError::ExpansionError(format!(
343                            "invalid variable: {:?} (in {value:?})",
344                            c
345                        )));
346                    }
347                }
348                State::InDollar => {
349                    if c.is_alphanumeric() || c == '_' {
350                        variable.push(c);
351                    } else {
352                        if let Some(value) = self.get_env(&std::mem::take(&mut variable)) {
353                            expanded.push_str(value);
354                        } else {
355                            return Err(ScriptRunError::ExpansionError(format!(
356                                "undefined variable in $...: {:?} (in {value:?})",
357                                variable
358                            )));
359                        }
360                        expanded.push(c);
361                        state = State::Normal;
362                    }
363                }
364            }
365        }
366        match state {
367            State::InDollar => {
368                if let Some(value) = self.get_env(&variable) {
369                    expanded.push_str(value);
370                } else {
371                    return Err(ScriptRunError::ExpansionError(format!(
372                        "undefined variable: {}",
373                        variable
374                    )));
375                }
376            }
377            State::Dollar => {
378                return Err(ScriptRunError::ExpansionError(
379                    "incomplete variable".to_string(),
380                ));
381            }
382            State::InCurly => {
383                return Err(ScriptRunError::ExpansionError(format!(
384                    "unclosed variable: {}",
385                    variable
386                )));
387            }
388            State::Normal => {}
389            State::EscapeNext => {
390                return Err(ScriptRunError::ExpansionError(
391                    "unclosed backslash".to_string(),
392                ));
393            }
394        }
395        Ok(expanded)
396    }
397
398    /// Get a mutable reference to the output stream.
399    pub fn stream(&self) -> impl termcolor::WriteColor + use<'_> {
400        ScriptOutputLock {
401            stream: self.output.stream.write(),
402        }
403    }
404}
405
406#[derive(Clone)]
407pub struct ScriptKillReceiver {
408    kill_receiver: Arc<AtomicBool>,
409}
410
411impl ScriptKillReceiver {
412    pub fn new(kill_receiver: Arc<AtomicBool>) -> Self {
413        Self { kill_receiver }
414    }
415
416    pub fn is_killed(&self) -> bool {
417        self.kill_receiver.load(std::sync::atomic::Ordering::SeqCst)
418    }
419
420    pub fn run_with<T>(&self, kill: impl FnOnce() + Send, wait: impl FnOnce() -> T) -> T {
421        std::thread::scope(|s| {
422            let done = Arc::new(AtomicBool::new(false));
423            let done_clone = done.clone();
424            let t = s.spawn(move || {
425                while !done_clone.load(std::sync::atomic::Ordering::SeqCst) {
426                    if self.is_killed() {
427                        kill();
428                        break;
429                    }
430                    std::thread::sleep(Duration::from_millis(10));
431                }
432            });
433            let res = wait();
434            done.store(true, std::sync::atomic::Ordering::SeqCst);
435            t.join().unwrap();
436            res
437        })
438    }
439
440    #[cfg(windows)]
441    pub fn run_cmd(
442        &self,
443        output: std::process::Child,
444        warn_time: Duration,
445    ) -> std::io::Result<ExitStatus> {
446        use std::os::windows::io::AsRawHandle;
447        use win32job::Job;
448
449        fn map_job_error(e: win32job::JobError) -> std::io::Error {
450            match e {
451                win32job::JobError::AssignFailed(e) => e,
452                win32job::JobError::CreateFailed(e) => e,
453                win32job::JobError::GetInfoFailed(e) => e,
454                win32job::JobError::SetInfoFailed(e) => e,
455                _ => std::io::Error::new(std::io::ErrorKind::Other, "Unknown error"),
456            }
457        }
458
459        // Create a new Job object
460        let job = Job::create().map_err(map_job_error)?;
461
462        // Configure the job to terminate all child processes when the job is closed
463        let mut info = job.query_extended_limit_info().map_err(map_job_error)?;
464        info.limit_kill_on_job_close();
465        job.set_extended_limit_info(&info).map_err(map_job_error)?;
466        job.assign_process(output.as_raw_handle() as _)?;
467
468        // Resume the main thread for the process
469        let id = output.id();
470        for thread_entry in tlhelp32::Snapshot::new_thread()? {
471            if thread_entry.owner_process_id == id {
472                use windows_sys::Win32::Foundation::CloseHandle;
473                use windows_sys::Win32::System::Threading::*;
474
475                // TODO: error handling
476                unsafe {
477                    let thread = OpenThread(THREAD_SUSPEND_RESUME, 0, thread_entry.thread_id);
478                    ResumeThread(thread);
479                    CloseHandle(thread);
480                }
481            }
482        }
483
484        let job = Mutex::new(Some(job));
485        let output = Mutex::new(output);
486        self.run_with(
487            || {
488                _ = job.lock().unwrap().take();
489                _ = output.lock().unwrap().kill();
490            },
491            || {
492                let start = std::time::Instant::now();
493                let mut warned = false;
494                loop {
495                    let res = output.lock().unwrap().try_wait()?;
496                    if let Some(status) = res {
497                        return Ok::<_, std::io::Error>(status);
498                    }
499                    if start.elapsed() > warn_time {
500                        if !warned {
501                            let child = output.lock().unwrap().id();
502                            eprintln!("Process #{child} taking too long to finish.");
503                            warned = true;
504                        }
505                    }
506                    std::thread::sleep(Duration::from_millis(10));
507                }
508            },
509        )
510    }
511
512    #[cfg(unix)]
513    pub fn run_cmd(
514        &self,
515        output: std::process::Child,
516        warn_time: Duration,
517    ) -> std::io::Result<ExitStatus> {
518        let output = Mutex::new(output);
519        self.run_with(
520            || {
521                use signal_child::{signal, signal::*};
522                let id = output.lock().unwrap().id() as i32;
523                _ = signal(-id, SIGINT);
524                std::thread::sleep(Duration::from_millis(10));
525                _ = signal(-id, SIGTERM);
526            },
527            || {
528                let start = std::time::Instant::now();
529                let mut warned = false;
530                loop {
531                    let res = output.lock().unwrap().try_wait()?;
532                    if let Some(status) = res {
533                        return Ok::<_, std::io::Error>(status);
534                    }
535                    if start.elapsed() > warn_time && !warned {
536                        let child = output.lock().unwrap().id();
537                        eprintln!("Process #{child} taking too long to finish.");
538                        warned = true;
539                    }
540                    std::thread::sleep(Duration::from_millis(10));
541                }
542            },
543        )
544    }
545}
546
547#[derive(Clone)]
548pub struct ScriptKillSender {
549    kill_sender: Arc<AtomicBool>,
550}
551
552impl ScriptKillSender {
553    pub fn new(kill_sender: Arc<AtomicBool>) -> Self {
554        Self { kill_sender }
555    }
556
557    pub fn kill(&self) {
558        self.kill_sender
559            .store(true, std::sync::atomic::Ordering::SeqCst);
560    }
561}
562
563impl ScriptRunContext {
564    pub fn new(args: ScriptRunArgs, script_path: impl AsRef<Path>, output: ScriptOutput) -> Self {
565        let mut env_vars = HashMap::new();
566
567        macro_rules! target {
568            ($env:ident, $var:ident, [$($vals:expr),*]) => {
569                $(
570                if cfg!($var = $vals) {
571                    env_vars.insert(stringify!($env).to_string(), $vals.to_string());
572                }
573                )*
574            };
575        }
576
577        target!(
578            TARGET_OS,
579            target_os,
580            ["windows", "linux", "macos", "ios", "android"]
581        );
582        target!(TARGET_FAMILY, target_family, ["windows", "unix", "wasm"]);
583        target!(
584            TARGET_ARCH,
585            target_arch,
586            ["x86", "x86_64", "arm", "aarch64"]
587        );
588
589        // Set the current working directory as a special variable "PWD"
590        env_vars.insert(
591            "PWD".to_string(),
592            NicePathBuf::from(script_path.as_ref().parent().unwrap()).env_string(),
593        );
594        // Save the initial PWD as INITIAL_PWD so it can easily be restored
595        env_vars.insert("INITIAL_PWD".to_string(), env_vars["PWD"].clone());
596
597        let kill = Arc::new(AtomicBool::new(false));
598
599        Self {
600            timeout: args.global_timeout.unwrap_or(DEFAULT_TIMEOUT),
601            args,
602            env_vars,
603            background: ScriptMode::Normal,
604            kill: ScriptKillReceiver::new(kill.clone()),
605            kill_sender: ScriptKillSender::new(kill.clone()),
606            output,
607        }
608    }
609}
610
611#[derive(Clone, Debug, PartialEq, Eq)]
612pub struct ScriptLine {
613    pub location: ScriptLocation,
614    text: String,
615}
616
617impl ScriptLine {
618    pub fn new(file: ScriptFile, line: usize, text: impl AsRef<str>) -> Self {
619        Self {
620            location: ScriptLocation::new(file, line),
621            text: text.as_ref().to_string(),
622        }
623    }
624
625    pub fn parse(file: ScriptFile, text: impl AsRef<str>) -> Vec<Self> {
626        text.as_ref()
627            .lines()
628            .enumerate()
629            .map(|(line, text)| Self {
630                location: ScriptLocation::new(file.clone(), line + 1),
631                text: text.to_string(),
632            })
633            .collect()
634    }
635
636    pub fn starts_with(&self, text: &str) -> bool {
637        self.text.trim().starts_with(text)
638    }
639
640    pub fn first_char(&self) -> Option<char> {
641        self.text.trim().chars().next()
642    }
643
644    pub fn text(&self) -> &str {
645        self.text.trim()
646    }
647
648    pub fn text_untrimmed(&self) -> &str {
649        &self.text
650    }
651
652    pub fn is_empty(&self) -> bool {
653        self.text.trim().is_empty()
654    }
655
656    pub fn strip_prefix(&self, prefix: &str) -> Option<&str> {
657        self.text.strip_prefix(prefix)
658    }
659}
660
661#[derive(Debug, thiserror::Error, derive_more::Display)]
662#[display("{error} at {location}{}", associated_data.as_deref().map_or("".to_string(), |d| format!(": {d}")))]
663pub struct ScriptError {
664    pub error: ScriptErrorType,
665    pub location: ScriptLocation,
666    pub associated_data: Option<String>,
667}
668
669impl ScriptError {
670    pub fn new(error: ScriptErrorType, location: ScriptLocation) -> Self {
671        if std::env::var("PANIC_ON_ERROR").is_ok() {
672            panic!("ScriptError: {error} at {location}");
673        }
674        Self {
675            error,
676            location,
677            associated_data: None,
678        }
679    }
680
681    pub fn new_with_data(
682        error: ScriptErrorType,
683        location: ScriptLocation,
684        associated_data: String,
685    ) -> Self {
686        if std::env::var("PANIC_ON_ERROR").is_ok() {
687            panic!("ScriptError: {error} at {location}: {associated_data}");
688        }
689        Self {
690            error,
691            location,
692            associated_data: Some(associated_data),
693        }
694    }
695}
696
697#[derive(Debug, thiserror::Error, Eq, PartialEq)]
698pub enum ScriptErrorType {
699    #[error("background process not allowed")]
700    BackgroundProcessNotAllowed,
701    #[error("unclosed quote")]
702    UnclosedQuote,
703    #[error("unclosed backslash")]
704    UnclosedBackslash,
705    #[error("illegal shell command format")]
706    IllegalShellCommand,
707    #[error("unsupported redirection")]
708    UnsupportedRedirection,
709    #[error("invalid pattern definition")]
710    InvalidPatternDefinition,
711    #[error("invalid pattern")]
712    InvalidPattern,
713    #[error("invalid meta command")]
714    InvalidMetaCommand,
715    #[error("invalid pattern at global level (only reject or ignore allowed here)")]
716    InvalidGlobalPattern,
717    #[error("invalid block type")]
718    InvalidBlockType,
719    #[error("invalid block arguments")]
720    InvalidBlockArgs,
721    #[error("unsupported command position")]
722    UnsupportedCommandPosition,
723    #[error("invalid trailing pattern after *")]
724    InvalidAnyPattern,
725    #[error("invalid exit status")]
726    InvalidExitStatus,
727    #[error("invalid set variable")]
728    InvalidSetVariable,
729    #[error("invalid version")]
730    InvalidVersion,
731    #[error("invalid internal command")]
732    InvalidInternalCommand,
733    #[error("missing command lines")]
734    MissingCommandLines,
735    #[error(
736        "block end without matching block start, too many closing braces or braces not properly nested"
737    )]
738    InvalidBlockEnd,
739    #[error("invalid if condition")]
740    InvalidIfCondition,
741}
742
743#[derive(Debug, thiserror::Error)]
744pub enum ScriptRunError {
745    #[error("{0}")]
746    Pattern(#[from] OutputPatternMatchFailure),
747    #[error("{0}")]
748    Exit(CommandResult),
749    #[error("expected failure, but passed")]
750    ExpectedFailure,
751    #[error("{0}")]
752    ExpansionError(String),
753    #[error("{0}")]
754    IO(#[from] std::io::Error),
755    #[error("killed")]
756    Killed,
757    #[error("background process took too long to finish")]
758    BackgroundProcessTookTooLong,
759    #[error("retry took too long to finish")]
760    RetryTookTooLong,
761}
762
763impl ScriptRunError {
764    #[allow(unused)]
765    pub fn short(&self) -> String {
766        match self {
767            Self::Pattern(_) => "Pattern".to_string(),
768            Self::Exit(status) => format!("Exit({})", status),
769            Self::ExpectedFailure => "ExpectedFailure".to_string(),
770            Self::IO(e) => format!("IO({:?})", e.kind()),
771            Self::Killed => "Killed".to_string(),
772            Self::BackgroundProcessTookTooLong => "BackgroundProcessTookTooLong".to_string(),
773            Self::ExpansionError(e) => "ExpansionError".to_string(),
774            Self::RetryTookTooLong => "RetryTookTooLong".to_string(),
775        }
776    }
777}
778
779impl Script {
780    pub fn run(&self, context: &mut ScriptRunContext) -> Result<(), ScriptRunError> {
781        let v = ScriptBlock::run_blocks(context, &self.commands)?;
782        assert!(v.is_empty(), "script did not run to completion: {v:?}");
783        Ok(())
784    }
785
786    pub fn run_with_args(
787        &self,
788        args: ScriptRunArgs,
789        output: ScriptOutput,
790    ) -> Result<(), ScriptRunError> {
791        let start = Instant::now();
792        let script_path = &*self.file.file;
793        let mut context = ScriptRunContext::new(args, script_path, output);
794
795        // Write "Running..." message with colors
796        cwrite!(context.stream(), "Running ");
797        cwrite!(context.stream(), fg = Color::Cyan, "{}", script_path);
798        cwriteln!(context.stream(), " ...");
799        cwriteln!(context.stream());
800
801        let result = self.run(&mut context);
802
803        // Handle success and error output
804        if let Err(ref e) = result {
805            cwrite!(context.stream(), fg = Color::Cyan, "{} ", script_path);
806            cwrite!(context.stream(), fg = Color::Red, "FAILED");
807            if !context.args.simplified_output {
808                cwriteln!(context.stream(), " ({:.2}s)", start.elapsed().as_secs_f32());
809            } else {
810                cwriteln!(context.stream());
811            }
812            cwrite!(context.stream(), fg = Color::Red, "Error: ");
813            cwriteln!(context.stream(), "{}", e);
814            cwriteln!(context.stream());
815        } else {
816            cwrite!(context.stream(), fg = Color::Cyan, "{} ", script_path);
817            cwrite!(context.stream(), fg = Color::Green, "PASSED");
818            if !context.args.simplified_output {
819                cwriteln!(context.stream(), " ({:.2}s)", start.elapsed().as_secs_f32());
820            } else {
821                cwriteln!(context.stream());
822            }
823        }
824
825        result
826    }
827}
828
829#[derive(Debug, Default, Serialize)]
830pub enum CommandExit {
831    #[default]
832    Success,
833    Failure(i32),
834    Timeout,
835    Any,
836    AnyFailure,
837}
838
839impl CommandExit {
840    pub fn matches(&self, status: CommandResult) -> bool {
841        match (self, status) {
842            (CommandExit::Success, CommandResult::Exit(status)) => status.success(),
843            (CommandExit::Failure(code), CommandResult::Exit(status)) => {
844                *code == status.code().unwrap_or(-1)
845            }
846            (CommandExit::Timeout, CommandResult::TimedOut) => true,
847            (CommandExit::Any, _) => true,
848            (CommandExit::AnyFailure, CommandResult::Exit(status)) => !status.success(),
849            (CommandExit::AnyFailure, _) => true,
850            _ => false,
851        }
852    }
853
854    pub fn is_success(&self) -> bool {
855        matches!(self, CommandExit::Success)
856    }
857}
858
859#[derive(derive_more::Debug)]
860pub enum ScriptBlock {
861    Command(ScriptCommand),
862    InternalCommand(InternalCommand),
863    Background(Vec<ScriptBlock>),
864    Defer(Vec<ScriptBlock>),
865    If(IfCondition, Vec<ScriptBlock>),
866    For(ForCondition, Vec<ScriptBlock>),
867    Retry(Vec<ScriptBlock>),
868}
869
870impl ScriptBlock {
871    pub fn run_blocks(
872        context: &mut ScriptRunContext,
873        blocks: &[ScriptBlock],
874    ) -> Result<Vec<ScriptResult>, ScriptRunError> {
875        enum Deferred<'a> {
876            Scripts(&'a [ScriptBlock]),
877            Internal(
878                Box<
879                    dyn FnOnce(&mut ScriptRunContext) -> Result<(), ScriptRunError>
880                        + Send
881                        + Sync
882                        + 'a,
883                >,
884            ),
885            Background(
886                ScopedJoinHandle<'a, Result<Vec<ScriptResult>, ScriptRunError>>,
887                ScriptKillSender,
888            ),
889        }
890
891        let mut results = Vec::new();
892        std::thread::scope(|s| {
893            let mut defer_blocks = VecDeque::new();
894            let mut pending_error = None;
895            for block in blocks {
896                if context.kill.is_killed() {
897                    return Err(ScriptRunError::Killed);
898                }
899                match block {
900                    ScriptBlock::Background(blocks) => {
901                        let mut context = context.new_background();
902                        let kill_sender = context.kill_sender.clone();
903                        let handle = s.spawn(move || Self::run_blocks(&mut context, blocks));
904                        defer_blocks.push_front(Deferred::Background(handle, kill_sender));
905                    }
906                    ScriptBlock::Defer(blocks) => {
907                        // Insert at the front of the queue by extending and
908                        // then rotating
909                        defer_blocks.push_front(Deferred::Scripts(blocks));
910                    }
911                    ScriptBlock::InternalCommand(command) => {
912                        if context.background == ScriptMode::Deferred {
913                            cwrite!(context.stream(), dimmed = true, "(deferred) ");
914                        }
915                        if let Some(f) = command.run(context)? {
916                            defer_blocks.push_front(Deferred::Internal(f));
917                        }
918                    }
919                    _ => match block.run(context) {
920                        Ok(res) => results.extend(res),
921                        Err(e) => {
922                            pending_error = Some(e);
923                            break;
924                        }
925                    },
926                }
927            }
928            for block in defer_blocks {
929                match block {
930                    Deferred::Scripts(blocks) => {
931                        let mut context = context.new_deferred();
932                        ScriptBlock::run_blocks(&mut context, blocks)?;
933                    }
934                    Deferred::Internal(block) => {
935                        cwrite!(context.stream(), dimmed = true, "(cleanup) ");
936                        block(context)?;
937                    }
938                    Deferred::Background(handle, kill_sender) => {
939                        kill_sender.kill();
940                        let start = std::time::Instant::now();
941                        let mut warned = false;
942
943                        let timeout = context.timeout;
944                        let warn_at = timeout * 8 / 10;
945
946                        let results = loop {
947                            if handle.is_finished() {
948                                break handle.join().unwrap()?;
949                            }
950                            std::thread::sleep(std::time::Duration::from_millis(10));
951                            if !warned && start.elapsed() > warn_at {
952                                cwriteln!(
953                                    context.stream(),
954                                    fg = Color::Yellow,
955                                    "Background process is taking too long to finish."
956                                );
957                                warned = true;
958                            }
959                            if start.elapsed() > timeout {
960                                cwriteln!(
961                                    context.stream(),
962                                    fg = Color::Red,
963                                    "Background process took too long to finish."
964                                );
965                                return Err(ScriptRunError::BackgroundProcessTookTooLong);
966                            }
967                        };
968                        for result in results {
969                            cwrite!(context.stream(), dimmed = true, "(background) ");
970                            for line in result.command.command.split('\n') {
971                                cwriteln!(context.stream(), fg = Color::Green, "{}", line);
972                            }
973                            if context.args.simplified_output {
974                                cwriteln!(context.stream(), dimmed = true, "---");
975                            } else {
976                                cwriteln_rule!(
977                                    context.stream(),
978                                    fg = Color::Cyan,
979                                    "{}",
980                                    result.command.location
981                                );
982                            }
983                            for line in &result.output {
984                                cwriteln!(context.stream(), "{}", line);
985                            }
986                            if result.output.is_empty() {
987                                cwriteln!(context.stream(), dimmed = true, "(no output)");
988                            }
989                            if context.args.simplified_output {
990                                cwriteln!(context.stream(), dimmed = true, "---");
991                            } else {
992                                cwriteln_rule!(context.stream());
993                            }
994                            result.evaluate(context)?;
995                        }
996                    }
997                }
998            }
999            if let Some(error) = pending_error {
1000                return Err(error);
1001            }
1002            Ok(results)
1003        })
1004    }
1005
1006    pub fn run(&self, context: &mut ScriptRunContext) -> Result<Vec<ScriptResult>, ScriptRunError> {
1007        let pwd = context.pwd();
1008        let res = pwd.exists();
1009        if !matches!(res, Ok(true)) {
1010            cwriteln!(
1011                context.stream(),
1012                fg = Color::Red,
1013                "$PWD {pwd:?} doesn't exist. Run `cd $INITIAL_PWD` to fix.",
1014            );
1015            return Err(ScriptRunError::IO(std::io::Error::new(
1016                std::io::ErrorKind::NotFound,
1017                format!("PWD does not exist: {pwd:?}"),
1018            )));
1019        }
1020
1021        match self {
1022            ScriptBlock::Command(command) => {
1023                if context.background == ScriptMode::Deferred {
1024                    cwrite!(context.stream(), dimmed = true, "(deferred) ");
1025                }
1026                let result = command.run(context)?;
1027                if context.background != ScriptMode::Background {
1028                    result.evaluate(context)?;
1029                    Ok(vec![])
1030                } else {
1031                    Ok(vec![result])
1032                }
1033            }
1034            ScriptBlock::If(condition, blocks) => {
1035                let condition = condition.expand(context)?;
1036                if condition.matches(context) {
1037                    Self::run_blocks(context, blocks)
1038                } else {
1039                    Ok(vec![])
1040                }
1041            }
1042            ScriptBlock::For(ForCondition::Env(env, values), blocks) => {
1043                let mut results = Vec::new();
1044                for value in values {
1045                    context.set_env(env, context.expand(value)?);
1046                    results.extend(Self::run_blocks(context, blocks)?);
1047                }
1048                Ok(results)
1049            }
1050            ScriptBlock::Retry(blocks) => {
1051                let start = Instant::now();
1052                let mut backoff = Duration::from_millis(100);
1053
1054                cwrite!(context.stream(), fg = Color::Green, "retry: ");
1055                cwriteln!(context.stream(), "running...");
1056
1057                loop {
1058                    let mut nested_context = context.new_background();
1059                    if let Ok(results) = Self::run_blocks(&mut nested_context, blocks) {
1060                        let mut all_ok = true;
1061                        for result in results {
1062                            if result.evaluate(&mut nested_context).is_err() {
1063                                all_ok = false;
1064                                break;
1065                            }
1066                        }
1067                        if all_ok {
1068                            let output = nested_context.take_output();
1069                            cwrite!(context.stream(), fg = Color::Green, "retry: ");
1070                            cwriteln!(context.stream(), "success");
1071                            cwriteln!(context.stream());
1072                            cwriteln!(context.stream(), "{output}");
1073                            return Ok(vec![]);
1074                        }
1075                    }
1076
1077                    if start.elapsed() > context.timeout {
1078                        let output = nested_context.take_output();
1079                        cwrite!(context.stream(), fg = Color::Green, "retry: ");
1080                        cwriteln!(context.stream(), fg = Color::Red, "timed out");
1081                        cwriteln!(context.stream());
1082                        cwriteln!(context.stream(), "{output}");
1083                        cwriteln_rule!(context.stream());
1084                        return Err(ScriptRunError::RetryTookTooLong);
1085                    }
1086                    std::thread::sleep(backoff);
1087                    backoff *= 2;
1088                }
1089            }
1090            _ => unreachable!("Unexpected block type: {self:?}"),
1091        }
1092    }
1093}
1094
1095impl Serialize for ScriptBlock {
1096    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1097    where
1098        S: serde::Serializer,
1099    {
1100        match self {
1101            ScriptBlock::Command(command) => command.serialize(serializer),
1102            ScriptBlock::InternalCommand(command) => command.serialize(serializer),
1103            ScriptBlock::Background(blocks) => {
1104                let mut ser = serializer.serialize_map(Some(1))?;
1105                ser.serialize_entry("background", blocks)?;
1106                ser.end()
1107            }
1108            ScriptBlock::Defer(blocks) => {
1109                let mut ser = serializer.serialize_map(Some(1))?;
1110                ser.serialize_entry("defer", blocks)?;
1111                ser.end()
1112            }
1113            ScriptBlock::If(condition, blocks) => {
1114                let mut ser = serializer.serialize_map(Some(2))?;
1115                ser.serialize_entry("if", condition)?;
1116                ser.serialize_entry("blocks", blocks)?;
1117                ser.end()
1118            }
1119            ScriptBlock::For(condition, blocks) => {
1120                let mut ser = serializer.serialize_map(Some(2))?;
1121                ser.serialize_entry("for", condition)?;
1122                ser.serialize_entry("blocks", blocks)?;
1123                ser.end()
1124            }
1125            ScriptBlock::Retry(blocks) => {
1126                let mut ser = serializer.serialize_map(Some(1))?;
1127                ser.serialize_entry("retry", blocks)?;
1128                ser.end()
1129            }
1130        }
1131    }
1132}
1133
1134#[derive(Debug, Clone, Serialize)]
1135pub enum InternalCommand {
1136    UsingTempdir,
1137    UsingDir(ShellBit, bool),
1138    ChangeDir(ShellBit),
1139    Set(String, ShellBit),
1140}
1141
1142impl InternalCommand {
1143    pub fn run(
1144        &self,
1145        context: &mut ScriptRunContext,
1146    ) -> Result<
1147        Option<Box<dyn FnOnce(&mut ScriptRunContext) -> Result<(), ScriptRunError> + Send + Sync>>,
1148        ScriptRunError,
1149    > {
1150        match self.clone() {
1151            InternalCommand::UsingTempdir => {
1152                let current_pwd = context.pwd();
1153                let tempdir = NiceTempDir::new();
1154                cwrite!(context.stream(), fg = Color::Yellow, "using tempdir: ");
1155                cwriteln!(context.stream(), "{}", tempdir);
1156                cwriteln!(context.stream());
1157                context.set_pwd(&tempdir);
1158                let pwd = context.pwd();
1159                if !pwd.exists()? {
1160                    return Err(ScriptRunError::IO(std::io::Error::new(
1161                        std::io::ErrorKind::NotFound,
1162                        format!("newly created tempdir does not exist: {pwd:?}"),
1163                    )));
1164                }
1165                Ok(Some(Box::new(move |context: &mut ScriptRunContext| {
1166                    cwriteln!(
1167                        context.stream(),
1168                        fg = Color::Yellow,
1169                        "removing {} && cd {}",
1170                        tempdir,
1171                        current_pwd
1172                    );
1173                    cwriteln!(context.stream());
1174                    if !tempdir.exists()? {
1175                        cwriteln!(
1176                            context.stream(),
1177                            fg = Color::Red,
1178                            "tempdir does not exist: {tempdir}"
1179                        );
1180                    }
1181                    if let Err(e) = tempdir.remove_dir_all() {
1182                        cwriteln!(
1183                            context.stream(),
1184                            fg = Color::Red,
1185                            "error removing tempdir: {e:?}"
1186                        );
1187                    }
1188                    Ok::<_, ScriptRunError>(())
1189                })))
1190            }
1191            InternalCommand::UsingDir(dir, new) => {
1192                let current_pwd = context.pwd();
1193                let dir = context.expand(&dir)?;
1194                let new_pwd = current_pwd.join(dir);
1195                if new {
1196                    cwrite!(context.stream(), fg = Color::Yellow, "using new dir: ");
1197                } else {
1198                    cwrite!(context.stream(), fg = Color::Yellow, "using dir: ");
1199                }
1200                cwriteln!(context.stream(), "{}", new_pwd);
1201                cwriteln!(context.stream());
1202
1203                if new {
1204                    new_pwd.create_dir_all()?;
1205                } else if !new_pwd.exists()? {
1206                    return Err(ScriptRunError::IO(std::io::Error::new(
1207                        std::io::ErrorKind::NotFound,
1208                        "directory does not exist",
1209                    )));
1210                }
1211                context.set_pwd(&new_pwd);
1212                Ok(Some(Box::new(move |context: &mut ScriptRunContext| {
1213                    if new {
1214                        cwriteln!(
1215                            context.stream(),
1216                            fg = Color::Yellow,
1217                            "removing {} && cd {}",
1218                            new_pwd,
1219                            current_pwd
1220                        );
1221                        cwriteln!(context.stream());
1222                    } else {
1223                        cwriteln!(context.stream(), fg = Color::Yellow, "cd {}", current_pwd);
1224                        cwriteln!(context.stream());
1225                    }
1226                    if new {
1227                        new_pwd.remove_dir_all()?;
1228                    }
1229                    context.set_pwd(current_pwd);
1230                    Ok::<_, ScriptRunError>(())
1231                })))
1232            }
1233            InternalCommand::ChangeDir(dir) => {
1234                let dir = context.expand(&dir)?;
1235
1236                cwriteln!(context.stream(), fg = Color::Yellow, "cd {dir}");
1237                cwriteln!(context.stream());
1238                let current_pwd = context.pwd();
1239                let new_pwd = current_pwd.join(dir);
1240                context.set_pwd(new_pwd);
1241                Ok(None)
1242            }
1243            InternalCommand::Set(name, value) => {
1244                let value = context.expand(&value)?;
1245
1246                context.set_env(&name, &value);
1247                let new_value = context.get_env(&name).unwrap_or_default();
1248                if new_value != value {
1249                    cwriteln!(
1250                        context.stream(),
1251                        fg = Color::Yellow,
1252                        "set {name} {value} (-> {new_value})"
1253                    );
1254                } else {
1255                    cwriteln!(context.stream(), fg = Color::Yellow, "set {name} {value}");
1256                }
1257                cwriteln!(context.stream());
1258
1259                Ok(None)
1260            }
1261        }
1262    }
1263}
1264
1265#[derive(Debug, Clone)]
1266pub enum IfCondition {
1267    True,
1268    False,
1269    EnvEq(bool, String, ShellBit),
1270}
1271
1272impl IfCondition {
1273    pub fn matches(&self, context: &ScriptRunContext) -> bool {
1274        match self {
1275            IfCondition::True => true,
1276            IfCondition::False => false,
1277            IfCondition::EnvEq(negated, name, expected) => {
1278                let value = context.get_env(name).unwrap_or_default();
1279                (expected == value) ^ negated
1280            }
1281        }
1282    }
1283
1284    pub fn expand(&self, context: &ScriptRunContext) -> Result<IfCondition, ScriptRunError> {
1285        match self {
1286            IfCondition::True => Ok(IfCondition::True),
1287            IfCondition::False => Ok(IfCondition::False),
1288            IfCondition::EnvEq(negated, name, expected) => {
1289                let value = context.expand(expected)?;
1290                Ok(IfCondition::EnvEq(
1291                    *negated,
1292                    name.clone(),
1293                    ShellBit::Literal(value),
1294                ))
1295            }
1296        }
1297    }
1298}
1299
1300impl Serialize for IfCondition {
1301    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1302    where
1303        S: serde::Serializer,
1304    {
1305        match self {
1306            IfCondition::True => "true".serialize(serializer),
1307            IfCondition::False => "false".serialize(serializer),
1308            IfCondition::EnvEq(negated, name, value) => {
1309                let mut ser = serializer.serialize_map(Some(3))?;
1310                ser.serialize_entry("op", if *negated { "!=" } else { "==" })?;
1311                ser.serialize_entry("env", name)?;
1312                ser.serialize_entry("value", value)?;
1313                ser.end()
1314            }
1315        }
1316    }
1317}
1318
1319#[derive(Debug)]
1320pub enum ForCondition {
1321    Env(String, Vec<ShellBit>),
1322}
1323
1324impl Serialize for ForCondition {
1325    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1326    where
1327        S: serde::Serializer,
1328    {
1329        match self {
1330            ForCondition::Env(name, values) => {
1331                let mut ser = serializer.serialize_map(Some(2))?;
1332                ser.serialize_entry("env", name)?;
1333                ser.serialize_entry("values", values)?;
1334                ser.end()
1335            }
1336        }
1337    }
1338}
1339
1340fn is_bool_false(b: &bool) -> bool {
1341    !b
1342}
1343
1344#[derive(Debug, Serialize)]
1345pub struct ScriptCommand {
1346    pub command: CommandLine,
1347    pub pattern: OutputPattern,
1348    #[serde(skip_serializing_if = "CommandExit::is_success")]
1349    pub exit: CommandExit,
1350    #[serde(skip_serializing_if = "is_bool_false")]
1351    pub expect_failure: bool,
1352    #[serde(skip_serializing_if = "Option::is_none")]
1353    pub set_var: Option<String>,
1354    #[serde(skip_serializing_if = "Option::is_none")]
1355    pub timeout: Option<Duration>,
1356}
1357
1358impl ScriptCommand {
1359    pub fn run(&self, context: &mut ScriptRunContext) -> Result<ScriptResult, ScriptRunError> {
1360        let command = &self.command;
1361        let args = &context.args;
1362        let start = Instant::now();
1363
1364        if let Some(delay) = args.delay_steps {
1365            std::thread::sleep(std::time::Duration::from_millis(delay));
1366        }
1367
1368        for line in command.command.split('\n') {
1369            cwriteln!(context.stream(), fg = Color::Green, "{}", line);
1370        }
1371        if args.simplified_output {
1372            cwriteln!(context.stream(), dimmed = true, "---");
1373        } else {
1374            cwriteln_rule!(context.stream(), fg = Color::Cyan, "{}", command.location);
1375        }
1376        let (output, status) = command.run(
1377            &mut context.stream(),
1378            context.args.show_line_numbers,
1379            context.args.runner.clone(),
1380            self.timeout.unwrap_or(context.timeout),
1381            &context.env_vars,
1382            &context.kill,
1383            &context.kill_sender,
1384        )?;
1385
1386        let exit_result = if !self.exit.matches(status) {
1387            ExitResult::Mismatch(status)
1388        } else {
1389            ExitResult::Matches(status)
1390        };
1391
1392        // Side-effects
1393        if let Some(set_var) = &self.set_var {
1394            context.set_env(set_var, output.to_string().trim());
1395        }
1396
1397        let match_context = OutputMatchContext::new(context);
1398        let pattern_result = match self.pattern.matches(match_context.clone(), output.clone()) {
1399            Ok(_) => {
1400                if self.expect_failure {
1401                    PatternResult::ExpectedFailure
1402                } else {
1403                    PatternResult::Matches
1404                }
1405            }
1406            Err(e) => {
1407                if self.expect_failure {
1408                    PatternResult::MatchesFailure
1409                } else {
1410                    let mut trace = String::new();
1411                    for line in match_context.traces() {
1412                        trace.push_str(&format!("{line}\n"));
1413                    }
1414                    PatternResult::Mismatch(e, trace)
1415                }
1416            }
1417        };
1418
1419        if output.is_empty() {
1420            cwriteln!(context.stream(), dimmed = true, "(no output)");
1421        }
1422
1423        if context.args.simplified_output {
1424            cwriteln!(context.stream(), dimmed = true, "---");
1425        } else {
1426            cwriteln_rule!(context.stream());
1427        }
1428
1429        Ok(ScriptResult {
1430            command: command.clone(),
1431            pattern: pattern_result,
1432            exit: exit_result,
1433            elapsed: start.elapsed(),
1434            output,
1435        })
1436    }
1437}
1438
1439#[derive(derive_more::Debug)]
1440pub struct ScriptResult {
1441    pub command: CommandLine,
1442    pub pattern: PatternResult,
1443    pub exit: ExitResult,
1444    pub elapsed: Duration,
1445    #[debug(skip)]
1446    pub output: Lines,
1447}
1448
1449impl ScriptResult {
1450    pub fn evaluate(&self, context: &mut ScriptRunContext) -> Result<(), ScriptRunError> {
1451        let args = &context.args;
1452        let (success, failure, warning, arrow) = if *crate::term::IS_UTF8 {
1453            ("✅", "❌", "⚠️", "→")
1454        } else {
1455            ("[*]", "[X]", "[!]", "->")
1456        };
1457
1458        if let ExitResult::Mismatch(status) = self.exit {
1459            if args.ignore_exit_codes {
1460                cwriteln!(
1461                    context.stream(),
1462                    fg = Color::Yellow,
1463                    "{warning} Ignored incorrect exit code: {status}"
1464                );
1465                cwriteln!(context.stream());
1466            } else {
1467                cwriteln!(
1468                    context.stream(),
1469                    fg = Color::Red,
1470                    "{failure} FAIL: {status}"
1471                );
1472                cwriteln!(
1473                    context.stream(),
1474                    dimmed = true,
1475                    " {arrow} {}",
1476                    self.command.command
1477                );
1478                cwriteln!(context.stream());
1479                return Err(ScriptRunError::Exit(status));
1480            }
1481        }
1482
1483        if let PatternResult::Mismatch(e, trace) = &self.pattern {
1484            if args.ignore_matches {
1485                cwriteln!(
1486                    context.stream(),
1487                    fg = Color::Yellow,
1488                    "{warning} Ignored error: {e} (ignoring mismatches)"
1489                );
1490                cwriteln!(context.stream());
1491            } else {
1492                cwriteln!(context.stream(), fg = Color::Red, "ERROR: {e}");
1493                cwriteln!(context.stream(), dimmed = true, "{trace}");
1494                cwriteln!(context.stream(), fg = Color::Red, "{failure} FAIL");
1495                cwriteln!(context.stream());
1496                return Err(ScriptRunError::Pattern(e.clone()));
1497            }
1498        }
1499
1500        if let PatternResult::ExpectedFailure = self.pattern {
1501            if args.ignore_matches {
1502                cwriteln!(
1503                    context.stream(),
1504                    fg = Color::Yellow,
1505                    "{warning} Should not have matched! (ignoring mismatches)"
1506                );
1507                cwriteln!(context.stream());
1508            } else {
1509                cwriteln!(
1510                    context.stream(),
1511                    fg = Color::Red,
1512                    "{failure} FAIL (output shouldn't match)"
1513                );
1514                cwriteln!(
1515                    context.stream(),
1516                    dimmed = true,
1517                    " {arrow} {}",
1518                    self.command.command
1519                );
1520                cwriteln!(context.stream());
1521                return Err(ScriptRunError::ExpectedFailure);
1522            }
1523        }
1524
1525        if let ExitResult::Matches(status) = self.exit {
1526            if status.success() {
1527                cwrite!(context.stream(), fg = Color::Green, "{success} OK");
1528                if !context.args.simplified_output {
1529                    cwriteln!(
1530                        context.stream(),
1531                        dimmed = true,
1532                        " ({:.2}s)",
1533                        self.elapsed.as_secs_f32()
1534                    );
1535                } else {
1536                    cwriteln!(context.stream());
1537                }
1538            } else {
1539                cwrite!(
1540                    context.stream(),
1541                    fg = Color::Green,
1542                    "{success} OK ({status})"
1543                );
1544                if !context.args.simplified_output {
1545                    cwriteln!(
1546                        context.stream(),
1547                        dimmed = true,
1548                        " ({:.2}s)",
1549                        self.elapsed.as_secs_f32()
1550                    );
1551                } else {
1552                    cwriteln!(context.stream());
1553                }
1554            }
1555            cwriteln!(context.stream());
1556        }
1557
1558        Ok(())
1559    }
1560}
1561
1562#[derive(Debug)]
1563pub enum PatternResult {
1564    Matches,
1565    MatchesFailure,
1566    ExpectedFailure,
1567    Mismatch(OutputPatternMatchFailure, String),
1568}
1569
1570#[derive(Debug)]
1571pub enum ExitResult {
1572    Matches(CommandResult),
1573    Mismatch(CommandResult),
1574    TimedOut,
1575}
1576
1577#[cfg(test)]
1578mod tests {
1579    use crate::parser::v0::parse_script;
1580
1581    use super::*;
1582    use std::error::Error;
1583
1584    #[test]
1585    fn test_script() -> Result<(), Box<dyn Error>> {
1586        let script = r#"
1587pattern VERSION \d+\.\d+\.\d+
1588
1589$ something --version || echo 1
1590? Something %{VERSION}
1591
1592$ something --help
1593? Usage: something [OPTIONS]
1594repeat {
1595    choice {
1596? %{DATA} %{GREEDYDATA}
1597? %{DATA}=%{DATA} %{GREEDYDATA}
1598    }
1599}
1600"#;
1601
1602        let script = parse_script(ScriptFile::new("test.cli"), script)?;
1603        assert_eq!(script.commands.len(), 2);
1604        eprintln!("{:?}", script);
1605        Ok(())
1606    }
1607
1608    #[test]
1609    fn test_bad_script() -> Result<(), Box<dyn Error>> {
1610        let script = r#"
1611$ (cmd; cmd)
1612$ cmd &
1613    "#;
1614
1615        assert!(matches!(
1616            parse_script(ScriptFile::new("test.cli"), script),
1617            Err(ScriptError {
1618                error: ScriptErrorType::BackgroundProcessNotAllowed,
1619                ..
1620            })
1621        ));
1622        Ok(())
1623    }
1624
1625    #[test]
1626    fn test_script_run_context_expand() {
1627        let mut context = ScriptRunContext::new(
1628            ScriptRunArgs::default(),
1629            Path::new("."),
1630            ScriptOutput::default(),
1631        );
1632        context.set_env("A", "1");
1633        context.set_env("B", "2");
1634        context.set_env("C", "3");
1635        assert_eq!(context.expand_str("$A").unwrap(), "1".to_string());
1636        assert_eq!(context.expand_str("$A $B ").unwrap(), "1 2 ".to_string());
1637        assert_eq!(
1638            context.expand_str("${A} ${B} ").unwrap(),
1639            "1 2 ".to_string()
1640        );
1641        assert_eq!(context.expand_str(r#"\$A"#).unwrap(), "$A".to_string());
1642        assert_eq!(context.expand_str(r#"\${A}"#).unwrap(), "${A}".to_string());
1643        assert_eq!(context.expand_str(r#"\\$A"#).unwrap(), r#"\1"#);
1644        assert_eq!(context.expand_str(r#"\\${A}"#).unwrap(), r#"\1"#);
1645        context.set_env("TEMP_DIR", "/tmp");
1646        assert_eq!(context.expand_str("$TEMP_DIR").unwrap(), "/tmp".to_string());
1647        assert_eq!(
1648            context.expand_str("${TEMP_DIR}").unwrap(),
1649            "/tmp".to_string()
1650        );
1651    }
1652}