Skip to main content

litcheck_lit/formats/sh/script/
mod.rs

1#![expect(unused_assignments)]
2
3pub mod directives;
4mod parser;
5#[cfg(test)]
6mod tests;
7
8use std::{borrow::Cow, sync::Arc};
9
10use litcheck::diagnostics::{
11    DiagResult, Diagnostic, FileName, SourceFile, SourceSpan, Span, Spanned,
12};
13
14use self::directives::*;
15use self::parser::TestScriptParser;
16
17use crate::config::{BooleanExpr, InvalidBooleanExprError, ScopedSubstitutionSet};
18
19#[derive(Diagnostic, Debug, thiserror::Error)]
20#[error("parsing test script at '{file}' failed")]
21#[diagnostic()]
22pub struct InvalidTestScriptError {
23    file: FileName,
24    #[related]
25    errors: Vec<TestScriptError>,
26}
27impl InvalidTestScriptError {
28    pub fn new(file: FileName, errors: Vec<TestScriptError>) -> Self {
29        Self { file, errors }
30    }
31}
32
33#[derive(Diagnostic, Debug, thiserror::Error)]
34pub enum TestScriptError {
35    #[error("unable to read file")]
36    #[diagnostic()]
37    Io(#[from] std::io::Error),
38    #[error("invalid line substitution syntax")]
39    #[diagnostic()]
40    InvalidLineSubstitution {
41        #[label("{error}")]
42        span: SourceSpan,
43        #[source]
44        error: core::num::ParseIntError,
45    },
46    #[error(transparent)]
47    #[diagnostic(transparent)]
48    InvalidBooleanExpr(
49        #[from]
50        #[diagnostic_source]
51        InvalidBooleanExprError,
52    ),
53    #[error(transparent)]
54    #[diagnostic(transparent)]
55    InvalidSubstitution(
56        #[from]
57        #[diagnostic_source]
58        InvalidSubstitutionError,
59    ),
60    #[error("unexpected directive keyword")]
61    #[diagnostic()]
62    InvalidDirectiveContinuation {
63        #[label("expected {expected} here")]
64        span: SourceSpan,
65        #[label("because this line ended with a continuation")]
66        prev_line: SourceSpan,
67        expected: DirectiveKind,
68    },
69    #[error("missing directive keyword")]
70    #[diagnostic()]
71    MissingDirectiveContinuation {
72        #[label("expected to find the {expected} keyword on this line")]
73        span: SourceSpan,
74        #[label("because this line ended with a continuation")]
75        prev_line: SourceSpan,
76        expected: DirectiveKind,
77    },
78    #[error("test has no 'RUN:' directive")]
79    #[diagnostic(help("lit requires at least one command to run per test"))]
80    MissingRunDirective,
81    #[error("'{kind}' directive conflict")]
82    #[diagnostic()]
83    DirectiveConflict {
84        #[label("this directive conflicts with a previous occurrence")]
85        span: SourceSpan,
86        #[label("previously occurred here")]
87        prev_span: SourceSpan,
88        kind: DirectiveKind,
89    },
90    #[error("unable to parse value of '{kind}' directive")]
91    #[diagnostic()]
92    InvalidIntegerDirective {
93        #[label("{error}")]
94        span: SourceSpan,
95        kind: DirectiveKind,
96        #[source]
97        error: core::num::ParseIntError,
98    },
99}
100
101#[derive(Debug, Default)]
102pub struct TestScript {
103    /// A list of commands to be evaluated in-order during test execution.
104    ///
105    /// Commands are either [Run] or [Substitution] directives.
106    pub commands: Vec<ScriptCommand>,
107    /// A list of conditions under which this test is expected to fail.
108    /// Each condition is a boolean expression of features, or '*'.
109    /// These can optionally be provided by test format handlers,
110    /// and will be honored when the test result is supplied.
111    pub xfails: Vec<Span<BooleanExpr>>,
112    /// A list of conditions that must be satisfied before running the test.
113    /// Each condition is a boolean expression of features. All of them
114    /// must be True for the test to run.
115    pub requires: Vec<Span<BooleanExpr>>,
116    /// A list of conditions that prevent execution of the test.
117    /// Each condition is a boolean expression of features. All of them
118    /// must be False for the test to run.
119    pub unsupported: Vec<Span<BooleanExpr>>,
120    /// An optional number of retries allowed before the test finally succeeds.
121    /// The test is run at most once plus the number of retries specified here.
122    pub allowed_retries: Option<Span<usize>>,
123}
124impl TestScript {
125    pub fn parse(source: Arc<SourceFile>) -> DiagResult<Self> {
126        let parser = TestScriptParser::new(&source);
127        parser
128            .parse()
129            .map_err(|err| litcheck::diagnostics::Report::from(err).with_source_code(source))
130    }
131
132    pub fn apply_substitutions(
133        &mut self,
134        substitutions: &mut ScopedSubstitutionSet<'_>,
135    ) -> DiagResult<()> {
136        for script_command in self.commands.iter_mut() {
137            match script_command {
138                ScriptCommand::Run(ref mut run) => {
139                    let span = run.span();
140                    let command = substitutions.apply(Span::new(span, run.command.as_str()))?;
141                    if let Cow::Owned(command) = command {
142                        run.command = command;
143                    }
144                }
145                ScriptCommand::Substitution(ref subst) => {
146                    subst.apply(substitutions)?;
147                }
148            }
149        }
150
151        Ok(())
152    }
153}