summaryrefslogtreecommitdiff
path: root/tests/src/metadata.rs
diff options
context:
space:
mode:
authorastrale-sharp <ash4567@outlook.fr>2024-01-09 10:05:57 +0100
committerGitHub <noreply@github.com>2024-01-09 09:05:57 +0000
commitc20b6ec6e157b753e90aa60ccd5b50f6f31cb7f6 (patch)
tree350ca5d05c68deafbdcaa0ad6cc1f649b8ee7a51 /tests/src/metadata.rs
parentcc1f974164ab5d42c3eb1b8fcd7a9227fbfd1d0e (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.rs333
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")
+ }
+}