1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
pub mod directives;
mod parser;
#[cfg(test)]
mod tests;

use std::borrow::Cow;

use litcheck::diagnostics::{
    DiagResult, Diagnostic, FileName, NamedSourceFile, SourceSpan, Span, Spanned,
};

use self::directives::*;
use self::parser::TestScriptParser;

use crate::config::{BooleanExpr, InvalidBooleanExprError, ScopedSubstitutionSet};

#[derive(Diagnostic, Debug, thiserror::Error)]
#[error("parsing test script at '{file}' failed")]
#[diagnostic()]
pub struct InvalidTestScriptError {
    file: FileName,
    #[related]
    errors: Vec<TestScriptError>,
}
impl InvalidTestScriptError {
    pub fn new(file: FileName, errors: Vec<TestScriptError>) -> Self {
        Self { file, errors }
    }
}

#[derive(Diagnostic, Debug, thiserror::Error)]
pub enum TestScriptError {
    #[error("unable to read file")]
    #[diagnostic()]
    Io(#[from] std::io::Error),
    #[error("invalid line substitution syntax")]
    #[diagnostic()]
    InvalidLineSubstitution {
        #[label("{error}")]
        span: SourceSpan,
        #[source]
        error: core::num::ParseIntError,
    },
    #[error(transparent)]
    #[diagnostic(transparent)]
    InvalidBooleanExpr(
        #[from]
        #[diagnostic_source]
        InvalidBooleanExprError,
    ),
    #[error(transparent)]
    #[diagnostic(transparent)]
    InvalidSubstitution(
        #[from]
        #[diagnostic_source]
        InvalidSubstitutionError,
    ),
    #[error("unexpected directive keyword")]
    #[diagnostic()]
    InvalidDirectiveContinuation {
        #[label("expected {expected} here")]
        span: SourceSpan,
        #[label("because this line ended with a continuation")]
        prev_line: SourceSpan,
        expected: DirectiveKind,
    },
    #[error("missing directive keyword")]
    #[diagnostic()]
    MissingDirectiveContinuation {
        #[label("expected to find the {expected} keyword on this line")]
        span: SourceSpan,
        #[label("because this line ended with a continuation")]
        prev_line: SourceSpan,
        expected: DirectiveKind,
    },
    #[error("test has no 'RUN:' directive")]
    #[diagnostic(help("lit requires at least one command to run per test"))]
    MissingRunDirective,
    #[error("'{kind}' directive conflict")]
    #[diagnostic()]
    DirectiveConflict {
        #[label("this directive conflicts with a previous occurrence")]
        span: SourceSpan,
        #[label("previously occurred here")]
        prev_span: SourceSpan,
        kind: DirectiveKind,
    },
    #[error("unable to parse value of '{kind}' directive")]
    #[diagnostic()]
    InvalidIntegerDirective {
        #[label("{error}")]
        span: SourceSpan,
        kind: DirectiveKind,
        #[source]
        error: core::num::ParseIntError,
    },
}

#[derive(Debug, Default)]
pub struct TestScript {
    /// A list of commands to be evaluated in-order during test execution.
    ///
    /// Commands are either [Run] or [Substitution] directives.
    pub commands: Vec<ScriptCommand>,
    /// A list of conditions under which this test is expected to fail.
    /// Each condition is a boolean expression of features, or '*'.
    /// These can optionally be provided by test format handlers,
    /// and will be honored when the test result is supplied.
    pub xfails: Vec<Span<BooleanExpr>>,
    /// A list of conditions that must be satisfied before running the test.
    /// Each condition is a boolean expression of features. All of them
    /// must be True for the test to run.
    pub requires: Vec<Span<BooleanExpr>>,
    /// A list of conditions that prevent execution of the test.
    /// Each condition is a boolean expression of features. All of them
    /// must be False for the test to run.
    pub unsupported: Vec<Span<BooleanExpr>>,
    /// An optional number of retries allowed before the test finally succeeds.
    /// The test is run at most once plus the number of retries specified here.
    pub allowed_retries: Option<Span<usize>>,
}
impl TestScript {
    #[cfg(test)]
    pub fn parse_str(input: &'static str) -> DiagResult<Self> {
        let source = litcheck::diagnostics::Source::new("-", input);
        Self::parse_source(&source).map_err(|err| err.with_source_code(source))
    }

    pub fn parse_source<S: NamedSourceFile + ?Sized>(source: &S) -> DiagResult<Self> {
        let parser = TestScriptParser::new(source);
        parser.parse().map_err(litcheck::diagnostics::Report::new)
    }

    pub fn apply_substitutions(
        &mut self,
        substitutions: &mut ScopedSubstitutionSet<'_>,
    ) -> DiagResult<()> {
        for script_command in self.commands.iter_mut() {
            match script_command {
                ScriptCommand::Run(ref mut run) => {
                    let span = run.span();
                    let command = substitutions.apply(Span::new(span, run.command.as_str()))?;
                    if let Cow::Owned(command) = command {
                        run.command = command;
                    }
                }
                ScriptCommand::Substitution(ref subst) => {
                    subst.apply(substitutions)?;
                }
            }
        }

        Ok(())
    }
}