diff options
Diffstat (limited to 'tests/src/metadata.rs')
| -rw-r--r-- | tests/src/metadata.rs | 334 |
1 files changed, 0 insertions, 334 deletions
diff --git a/tests/src/metadata.rs b/tests/src/metadata.rs deleted file mode 100644 index 53cbbdff..00000000 --- a/tests/src/metadata.rs +++ /dev/null @@ -1,334 +0,0 @@ -use std::collections::HashSet; -use std::fmt::{self, Display, Formatter}; -use std::ops::Range; -use std::str::FromStr; - -use ecow::EcoString; -use typst::syntax::package::PackageVersion; -use typst::syntax::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") - } -} |
