diff options
| author | astrale-sharp <ash4567@outlook.fr> | 2024-01-09 10:05:57 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-01-09 09:05:57 +0000 |
| commit | c20b6ec6e157b753e90aa60ccd5b50f6f31cb7f6 (patch) | |
| tree | 350ca5d05c68deafbdcaa0ad6cc1f649b8ee7a51 /tests/src/metadata.rs | |
| parent | cc1f974164ab5d42c3eb1b8fcd7a9227fbfd1d0e (diff) | |
Test autocomplete (#2912)
Co-authored-by: oliver <151407407+kwfn@users.noreply.github.com>
Co-authored-by: Laurenz <laurmaedje@gmail.com>
Diffstat (limited to 'tests/src/metadata.rs')
| -rw-r--r-- | tests/src/metadata.rs | 333 |
1 files changed, 333 insertions, 0 deletions
diff --git a/tests/src/metadata.rs b/tests/src/metadata.rs new file mode 100644 index 00000000..72a627f0 --- /dev/null +++ b/tests/src/metadata.rs @@ -0,0 +1,333 @@ +use std::collections::HashSet; +use std::fmt::{self, Display, Formatter}; +use std::ops::Range; +use std::str::FromStr; + +use ecow::EcoString; +use typst::syntax::{PackageVersion, Source}; +use unscanny::Scanner; + +/// Each test and subset may contain metadata. +#[derive(Debug)] +pub struct TestMetadata { + /// Configures how the test is run. + pub config: TestConfig, + /// Declares properties that must hold for a test. + /// + /// For instance, `// Warning: 1-3 no text within underscores` + /// will fail the test if the warning isn't generated by your test. + pub annotations: HashSet<Annotation>, +} + +/// Configuration of a test or subtest. +#[derive(Debug, Default)] +pub struct TestConfig { + /// Reference images will be generated and compared. + /// + /// Defaults to `true`, can be disabled with `Ref: false`. + pub compare_ref: Option<bool>, + /// Hint annotations will be compared to compiler hints. + /// + /// Defaults to `true`, can be disabled with `Hints: false`. + pub validate_hints: Option<bool>, + /// Autocompletion annotations will be validated against autocompletions. + /// Mutually exclusive with error and hint annotations. + /// + /// Defaults to `false`, can be enabled with `Autocomplete: true`. + pub validate_autocomplete: Option<bool>, +} + +/// Parsing error when the metadata is invalid. +pub(crate) enum InvalidMetadata { + /// An invalid annotation and it's error message. + InvalidAnnotation(Annotation, String), + /// Setting metadata can only be done with `true` or `false` as a value. + InvalidSet(String), +} + +impl InvalidMetadata { + pub(crate) fn write( + invalid_data: Vec<InvalidMetadata>, + output: &mut String, + print_annotation: &mut impl FnMut(&Annotation, &mut String), + ) { + use std::fmt::Write; + for data in invalid_data.into_iter() { + let (annotation, error) = match data { + InvalidMetadata::InvalidAnnotation(a, e) => (Some(a), e), + InvalidMetadata::InvalidSet(e) => (None, e), + }; + write!(output, "{error}",).unwrap(); + if let Some(annotation) = annotation { + print_annotation(&annotation, output) + } else { + writeln!(output).unwrap(); + } + } + } +} + +/// Annotation of the form `// KIND: RANGE TEXT`. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Annotation { + /// Which kind of annotation this is. + pub kind: AnnotationKind, + /// May be written as: + /// - `{line}:{col}-{line}:{col}`, e.g. `0:4-0:6`. + /// - `{col}-{col}`, e.g. `4-6`: + /// The line is assumed to be the line after the annotation. + /// - `-1`: Produces a range of length zero at the end of the next line. + /// Mostly useful for autocompletion tests which require an index. + pub range: Option<Range<usize>>, + /// The raw text after the annotation. + pub text: EcoString, +} + +/// The different kinds of in-test annotations. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum AnnotationKind { + Error, + Warning, + Hint, + AutocompleteContains, + AutocompleteExcludes, +} + +impl AnnotationKind { + /// Returns the user-facing string for this annotation. + pub fn as_str(self) -> &'static str { + match self { + AnnotationKind::Error => "Error", + AnnotationKind::Warning => "Warning", + AnnotationKind::Hint => "Hint", + AnnotationKind::AutocompleteContains => "Autocomplete contains", + AnnotationKind::AutocompleteExcludes => "Autocomplete excludes", + } + } +} + +impl FromStr for AnnotationKind { + type Err = &'static str; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(match s { + "Error" => AnnotationKind::Error, + "Warning" => AnnotationKind::Warning, + "Hint" => AnnotationKind::Hint, + "Autocomplete contains" => AnnotationKind::AutocompleteContains, + "Autocomplete excludes" => AnnotationKind::AutocompleteExcludes, + _ => return Err("invalid annotatino"), + }) + } +} + +impl Display for AnnotationKind { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(self.as_str()) + } +} + +/// Parse metadata for a test. +pub fn parse_part_metadata( + source: &Source, + is_header: bool, +) -> Result<TestMetadata, Vec<InvalidMetadata>> { + let mut config = TestConfig::default(); + let mut annotations = HashSet::default(); + let mut invalid_data = vec![]; + + let lines = source_to_lines(source); + + for (i, line) in lines.iter().enumerate() { + if let Some((key, value)) = parse_metadata_line(line) { + let key = key.trim(); + match key { + "Ref" => validate_set_annotation( + value, + &mut config.compare_ref, + &mut invalid_data, + ), + "Hints" => validate_set_annotation( + value, + &mut config.validate_hints, + &mut invalid_data, + ), + "Autocomplete" => validate_set_annotation( + value, + &mut config.validate_autocomplete, + &mut invalid_data, + ), + annotation_key => { + let Ok(kind) = AnnotationKind::from_str(annotation_key) else { + continue; + }; + let mut s = Scanner::new(value); + let range = parse_range(&mut s, i, source); + let rest = if range.is_some() { s.after() } else { s.string() }; + let message = rest + .trim() + .replace("VERSION", &PackageVersion::compiler().to_string()) + .into(); + + let annotation = + Annotation { kind, range: range.clone(), text: message }; + + if is_header { + invalid_data.push(InvalidMetadata::InvalidAnnotation( + annotation, + format!( + "Error: header may not contain annotations of type {kind}" + ), + )); + continue; + } + + if matches!( + kind, + AnnotationKind::AutocompleteContains + | AnnotationKind::AutocompleteExcludes + ) { + if let Some(range) = range { + if range.start != range.end { + invalid_data.push(InvalidMetadata::InvalidAnnotation( + annotation, + "Error: found range in Autocomplete annotation where range.start != range.end, range.end would be ignored." + .to_string() + )); + continue; + } + } else { + invalid_data.push(InvalidMetadata::InvalidAnnotation( + annotation, + "Error: autocomplete annotation but no range specified" + .to_string(), + )); + continue; + } + } + annotations.insert(annotation); + } + } + } + } + if invalid_data.is_empty() { + Ok(TestMetadata { config, annotations }) + } else { + Err(invalid_data) + } +} + +/// Extract key and value for a metadata line of the form: `// KEY: VALUE`. +fn parse_metadata_line(line: &str) -> Option<(&str, &str)> { + let mut s = Scanner::new(line); + if !s.eat_if("// ") { + return None; + } + + let key = s.eat_until(':').trim(); + if !s.eat_if(':') { + return None; + } + + let value = s.eat_until('\n').trim(); + Some((key, value)) +} + +/// Parse a quoted string. +fn parse_string<'a>(s: &mut Scanner<'a>) -> Option<&'a str> { + if !s.eat_if('"') { + return None; + } + let sub = s.eat_until('"'); + if !s.eat_if('"') { + return None; + } + + Some(sub) +} + +/// Parse a number. +fn parse_num(s: &mut Scanner) -> Option<isize> { + let mut first = true; + let n = &s.eat_while(|c: char| { + let valid = first && c == '-' || c.is_numeric(); + first = false; + valid + }); + n.parse().ok() +} + +/// Parse a comma-separated list of strings. +pub fn parse_string_list(text: &str) -> HashSet<&str> { + let mut s = Scanner::new(text); + let mut result = HashSet::new(); + while let Some(sub) = parse_string(&mut s) { + result.insert(sub); + s.eat_whitespace(); + if !s.eat_if(',') { + break; + } + s.eat_whitespace(); + } + result +} + +/// Parse a position. +fn parse_pos(s: &mut Scanner, i: usize, source: &Source) -> Option<usize> { + let first = parse_num(s)? - 1; + let (delta, column) = + if s.eat_if(':') { (first, parse_num(s)? - 1) } else { (0, first) }; + let line = (i + comments_until_code(source, i)).checked_add_signed(delta)?; + source.line_column_to_byte(line, usize::try_from(column).ok()?) +} + +/// Parse a range. +fn parse_range(s: &mut Scanner, i: usize, source: &Source) -> Option<Range<usize>> { + let lines = source_to_lines(source); + s.eat_whitespace(); + if s.eat_if("-1") { + let mut add = 1; + while let Some(line) = lines.get(i + add) { + if !line.starts_with("//") { + break; + } + add += 1; + } + let next_line = lines.get(i + add)?; + let col = next_line.chars().count(); + + let index = source.line_column_to_byte(i + add, col)?; + s.eat_whitespace(); + return Some(index..index); + } + let start = parse_pos(s, i, source)?; + let end = if s.eat_if('-') { parse_pos(s, i, source)? } else { start }; + s.eat_whitespace(); + Some(start..end) +} + +/// Returns the number of lines of comment from line i to next line of code. +fn comments_until_code(source: &Source, i: usize) -> usize { + source_to_lines(source)[i..] + .iter() + .take_while(|line| line.starts_with("//")) + .count() +} + +fn source_to_lines(source: &Source) -> Vec<&str> { + source.text().lines().map(str::trim).collect() +} + +fn validate_set_annotation( + value: &str, + flag: &mut Option<bool>, + invalid_data: &mut Vec<InvalidMetadata>, +) { + let value = value.trim(); + if value != "false" && value != "true" { + invalid_data.push( + InvalidMetadata::InvalidSet(format!("Error: trying to set Ref, Hints, or Autocomplete with value {value:?} != true, != false."))) + } else { + *flag = Some(value == "true") + } +} |
