litcheck_lit/formats/sh/script/
mod.rs

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