litcheck_lit/formats/sh/script/
mod.rs1pub 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 pub commands: Vec<ScriptCommand>,
105 pub xfails: Vec<Span<BooleanExpr>>,
110 pub requires: Vec<Span<BooleanExpr>>,
114 pub unsupported: Vec<Span<BooleanExpr>>,
118 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}