mirror of
https://github.com/typst/typst
synced 2025-05-15 17:45:27 +08:00
Test autocomplete (#2912)
Co-authored-by: oliver <151407407+kwfn@users.noreply.github.com> Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
parent
cc1f974164
commit
c20b6ec6e1
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2665,6 +2665,7 @@ dependencies = [
|
|||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
"ttf-parser",
|
"ttf-parser",
|
||||||
"typst",
|
"typst",
|
||||||
|
"typst-ide",
|
||||||
"typst-pdf",
|
"typst-pdf",
|
||||||
"typst-render",
|
"typst-render",
|
||||||
"typst-svg",
|
"typst-svg",
|
||||||
|
@ -11,6 +11,7 @@ typst = { workspace = true }
|
|||||||
typst-pdf = { workspace = true }
|
typst-pdf = { workspace = true }
|
||||||
typst-render = { workspace = true }
|
typst-render = { workspace = true }
|
||||||
typst-svg = { workspace = true }
|
typst-svg = { workspace = true }
|
||||||
|
typst-ide = { workspace = true }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
comemo = { workspace = true }
|
comemo = { workspace = true }
|
||||||
ecow = { workspace = true }
|
ecow = { workspace = true }
|
||||||
|
333
tests/src/metadata.rs
Normal file
333
tests/src/metadata.rs
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,24 @@
|
|||||||
|
/*! This is Typst's test runner.
|
||||||
|
|
||||||
|
Tests are Typst files composed of a header part followed by subtests.
|
||||||
|
|
||||||
|
The header may contain:
|
||||||
|
- a small description `// tests that features X works well`
|
||||||
|
- metadata (see [metadata::TestConfiguration])
|
||||||
|
|
||||||
|
The subtests may use extra testing functions defined in [library], most
|
||||||
|
importantly, `test(x, y)` which will fail the test `if x != y`.
|
||||||
|
*/
|
||||||
|
|
||||||
#![allow(clippy::comparison_chain)]
|
#![allow(clippy::comparison_chain)]
|
||||||
|
mod metadata;
|
||||||
|
|
||||||
|
use self::metadata::*;
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fmt::{self, Display, Formatter, Write as _};
|
use std::fmt::Write as _;
|
||||||
use std::io::{self, IsTerminal, Write};
|
use std::io::{self, IsTerminal, Write as _};
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR};
|
use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR};
|
||||||
use std::sync::{OnceLock, RwLock};
|
use std::sync::{OnceLock, RwLock};
|
||||||
@ -11,23 +26,19 @@ use std::{env, fs};
|
|||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use comemo::{Prehashed, Track};
|
use comemo::{Prehashed, Track};
|
||||||
use ecow::EcoString;
|
|
||||||
use oxipng::{InFile, Options, OutFile};
|
use oxipng::{InFile, Options, OutFile};
|
||||||
use rayon::iter::{ParallelBridge, ParallelIterator};
|
use rayon::iter::{ParallelBridge, ParallelIterator};
|
||||||
use tiny_skia as sk;
|
use tiny_skia as sk;
|
||||||
use typst::diag::{bail, FileError, FileResult, Severity, StrResult};
|
use typst::diag::{bail, FileError, FileResult, Severity, SourceDiagnostic, StrResult};
|
||||||
use typst::eval::Tracer;
|
use typst::eval::Tracer;
|
||||||
use typst::foundations::{
|
use typst::foundations::{func, Bytes, Datetime, NoneValue, Repr, Smart, Value};
|
||||||
eco_format, func, Bytes, Datetime, NoneValue, Repr, Smart, Value,
|
|
||||||
};
|
|
||||||
use typst::introspection::Meta;
|
use typst::introspection::Meta;
|
||||||
use typst::layout::{Abs, Frame, FrameItem, Margin, PageElem, Transform};
|
use typst::layout::{Abs, Frame, FrameItem, Margin, PageElem, Transform};
|
||||||
use typst::model::Document;
|
use typst::model::Document;
|
||||||
use typst::syntax::{FileId, PackageVersion, Source, SyntaxNode, VirtualPath};
|
use typst::syntax::{FileId, Source, SyntaxNode, VirtualPath};
|
||||||
use typst::text::{Font, FontBook, TextElem, TextSize};
|
use typst::text::{Font, FontBook, TextElem, TextSize};
|
||||||
use typst::visualize::Color;
|
use typst::visualize::Color;
|
||||||
use typst::{Library, World, WorldExt};
|
use typst::{Library, World, WorldExt};
|
||||||
use unscanny::Scanner;
|
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
// These directories are all relative to the tests/ directory.
|
// These directories are all relative to the tests/ directory.
|
||||||
@ -39,33 +50,56 @@ const SVG_DIR: &str = "svg";
|
|||||||
const FONT_DIR: &str = "../assets/fonts";
|
const FONT_DIR: &str = "../assets/fonts";
|
||||||
const ASSET_DIR: &str = "../assets";
|
const ASSET_DIR: &str = "../assets";
|
||||||
|
|
||||||
|
/// Arguments that modify test behaviour.
|
||||||
|
///
|
||||||
|
/// Specify them like this when developing:
|
||||||
|
/// `cargo test --workspace --test tests -- --help`
|
||||||
#[derive(Debug, Clone, Parser)]
|
#[derive(Debug, Clone, Parser)]
|
||||||
#[clap(name = "typst-test", author)]
|
#[clap(name = "typst-test", author)]
|
||||||
struct Args {
|
struct Args {
|
||||||
|
/// All the tests that contains a filter string will be run (unless
|
||||||
|
/// `--exact` is specified, which is even stricter).
|
||||||
filter: Vec<String>,
|
filter: Vec<String>,
|
||||||
/// runs only the specified subtest
|
/// Runs only the specified subtest.
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
#[arg(allow_hyphen_values = true)]
|
#[arg(allow_hyphen_values = true)]
|
||||||
subtest: Option<isize>,
|
subtest: Option<isize>,
|
||||||
|
/// Runs only the test with the exact name specified in your command.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// `cargo test --workspace --test tests -- compiler/bytes.typ --exact`
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
exact: bool,
|
exact: bool,
|
||||||
|
/// Updates the reference images in `tests/ref`.
|
||||||
#[arg(long, default_value_t = env::var_os("UPDATE_EXPECT").is_some())]
|
#[arg(long, default_value_t = env::var_os("UPDATE_EXPECT").is_some())]
|
||||||
update: bool,
|
update: bool,
|
||||||
|
/// Exports the tests as PDF into `tests/pdf`.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pdf: bool,
|
pdf: bool,
|
||||||
|
/// Configuration of what to print.
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
print: PrintConfig,
|
print: PrintConfig,
|
||||||
|
/// Running `cargo test --workspace -- --nocapture` for the unit tests would
|
||||||
|
/// fail the test runner without argument.
|
||||||
|
// TODO: would it really still happen?
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
nocapture: bool, // simply ignores the argument
|
nocapture: bool,
|
||||||
|
/// Prevents the terminal from being cleared of test names and includes
|
||||||
|
/// non-essential test messages.
|
||||||
|
#[arg(short, long)]
|
||||||
|
verbose: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which things to print out for debugging.
|
/// Which things to print out for debugging.
|
||||||
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Parser)]
|
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Parser)]
|
||||||
struct PrintConfig {
|
struct PrintConfig {
|
||||||
|
/// Print the syntax tree.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
syntax: bool,
|
syntax: bool,
|
||||||
|
/// Print the content model.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
model: bool,
|
model: bool,
|
||||||
|
/// Print the layouted frames.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
frames: bool,
|
frames: bool,
|
||||||
}
|
}
|
||||||
@ -87,6 +121,7 @@ impl Args {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tests all test files and prints a summary.
|
||||||
fn main() {
|
fn main() {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
@ -359,6 +394,10 @@ fn read(path: &Path) -> FileResult<Vec<u8>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tests a test file and prints the result.
|
||||||
|
///
|
||||||
|
/// Also tests that the header of each test is written correctly.
|
||||||
|
/// See [parse_part_metadata] for more details.
|
||||||
fn test(
|
fn test(
|
||||||
world: &mut TestWorld,
|
world: &mut TestWorld,
|
||||||
src_path: &Path,
|
src_path: &Path,
|
||||||
@ -386,8 +425,7 @@ fn test(
|
|||||||
let mut updated = false;
|
let mut updated = false;
|
||||||
let mut frames = vec![];
|
let mut frames = vec![];
|
||||||
let mut line = 0;
|
let mut line = 0;
|
||||||
let mut compare_ref = None;
|
let mut header_configuration = None;
|
||||||
let mut validate_hints = None;
|
|
||||||
let mut compare_ever = false;
|
let mut compare_ever = false;
|
||||||
let mut rng = LinearShift::new();
|
let mut rng = LinearShift::new();
|
||||||
|
|
||||||
@ -414,9 +452,28 @@ fn test(
|
|||||||
.all(|s| s.starts_with("//") || s.chars().all(|c| c.is_whitespace()));
|
.all(|s| s.starts_with("//") || s.chars().all(|c| c.is_whitespace()));
|
||||||
|
|
||||||
if is_header {
|
if is_header {
|
||||||
for line in part.lines() {
|
let source = Source::detached(part.to_string());
|
||||||
compare_ref = get_flag_metadata(line, "Ref").or(compare_ref);
|
let metadata = parse_part_metadata(&source, true);
|
||||||
validate_hints = get_flag_metadata(line, "Hints").or(validate_hints);
|
match metadata {
|
||||||
|
Ok(metadata) => {
|
||||||
|
header_configuration = Some(metadata.config);
|
||||||
|
}
|
||||||
|
Err(invalid_data) => {
|
||||||
|
ok = false;
|
||||||
|
writeln!(
|
||||||
|
output,
|
||||||
|
" Test {}: invalid metadata in header, failing the test:",
|
||||||
|
name.display()
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
InvalidMetadata::write(
|
||||||
|
invalid_data,
|
||||||
|
&mut output,
|
||||||
|
&mut |annotation, output| {
|
||||||
|
print_annotation(output, &source, line, annotation)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let (part_ok, compare_here, part_frames) = test_part(
|
let (part_ok, compare_here, part_frames) = test_part(
|
||||||
@ -424,11 +481,11 @@ fn test(
|
|||||||
world,
|
world,
|
||||||
src_path,
|
src_path,
|
||||||
part.into(),
|
part.into(),
|
||||||
i,
|
|
||||||
compare_ref.unwrap_or(true),
|
|
||||||
validate_hints.unwrap_or(true),
|
|
||||||
line,
|
line,
|
||||||
|
i,
|
||||||
|
header_configuration.as_ref().unwrap_or(&Default::default()),
|
||||||
&mut rng,
|
&mut rng,
|
||||||
|
args.verbose,
|
||||||
);
|
);
|
||||||
|
|
||||||
ok &= part_ok;
|
ok &= part_ok;
|
||||||
@ -498,9 +555,9 @@ fn test(
|
|||||||
stdout.write_all(name.to_string_lossy().as_bytes()).unwrap();
|
stdout.write_all(name.to_string_lossy().as_bytes()).unwrap();
|
||||||
if ok {
|
if ok {
|
||||||
writeln!(stdout, " ✔").unwrap();
|
writeln!(stdout, " ✔").unwrap();
|
||||||
// Don't clear the line when the reference image was updated, to
|
// Don't clear the line when in verbose mode or when the reference image
|
||||||
// show in the output which test had its image updated.
|
// was updated, to show in the output which test had its image updated.
|
||||||
if !updated && stdout.is_terminal() {
|
if !updated && !args.verbose && stdout.is_terminal() {
|
||||||
// ANSI escape codes: cursor moves up and clears the line.
|
// ANSI escape codes: cursor moves up and clears the line.
|
||||||
write!(stdout, "\x1b[1A\x1b[2K").unwrap();
|
write!(stdout, "\x1b[1A\x1b[2K").unwrap();
|
||||||
}
|
}
|
||||||
@ -518,14 +575,6 @@ fn test(
|
|||||||
ok
|
ok
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_metadata<'a>(line: &'a str, key: &str) -> Option<&'a str> {
|
|
||||||
line.strip_prefix(eco_format!("// {key}: ").as_str())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_flag_metadata(line: &str, key: &str) -> Option<bool> {
|
|
||||||
get_metadata(line, key).map(|value| value == "true")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_image(png_path: &Path, ref_path: &Path) {
|
fn update_image(png_path: &Path, ref_path: &Path) {
|
||||||
oxipng::optimize(
|
oxipng::optimize(
|
||||||
&InFile::Path(png_path.to_owned()),
|
&InFile::Path(png_path.to_owned()),
|
||||||
@ -541,35 +590,19 @@ fn test_part(
|
|||||||
world: &mut TestWorld,
|
world: &mut TestWorld,
|
||||||
src_path: &Path,
|
src_path: &Path,
|
||||||
text: String,
|
text: String,
|
||||||
i: usize,
|
|
||||||
compare_ref: bool,
|
|
||||||
validate_hints: bool,
|
|
||||||
line: usize,
|
line: usize,
|
||||||
|
i: usize,
|
||||||
|
header_configuration: &TestConfig,
|
||||||
rng: &mut LinearShift,
|
rng: &mut LinearShift,
|
||||||
|
verbose: bool,
|
||||||
) -> (bool, bool, Vec<Frame>) {
|
) -> (bool, bool, Vec<Frame>) {
|
||||||
let mut ok = true;
|
|
||||||
|
|
||||||
let source = world.set(src_path, text);
|
let source = world.set(src_path, text);
|
||||||
if world.print.syntax {
|
if world.print.syntax {
|
||||||
writeln!(output, "Syntax Tree:\n{:#?}\n", source.root()).unwrap();
|
writeln!(output, "Syntax Tree:\n{:#?}\n", source.root()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let metadata = parse_part_metadata(&source);
|
|
||||||
let compare_ref = metadata.part_configuration.compare_ref.unwrap_or(compare_ref);
|
|
||||||
let validate_hints =
|
|
||||||
metadata.part_configuration.validate_hints.unwrap_or(validate_hints);
|
|
||||||
|
|
||||||
ok &= test_spans(output, source.root());
|
|
||||||
ok &= test_reparse(output, source.text(), i, rng);
|
|
||||||
|
|
||||||
if world.print.model {
|
if world.print.model {
|
||||||
let world = (world as &dyn World).track();
|
print_model(world, &source, output);
|
||||||
let route = typst::engine::Route::default();
|
|
||||||
let mut tracer = typst::eval::Tracer::new();
|
|
||||||
|
|
||||||
let module =
|
|
||||||
typst::eval::eval(world, route.track(), tracer.track_mut(), &source).unwrap();
|
|
||||||
writeln!(output, "Model:\n{:#?}\n", module.content()).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut tracer = Tracer::new();
|
let mut tracer = Tracer::new();
|
||||||
@ -582,11 +615,186 @@ fn test_part(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let metadata = parse_part_metadata(&source, false);
|
||||||
|
match metadata {
|
||||||
|
Ok(metadata) => {
|
||||||
|
let mut ok = true;
|
||||||
|
let compare_ref = metadata
|
||||||
|
.config
|
||||||
|
.compare_ref
|
||||||
|
.unwrap_or(header_configuration.compare_ref.unwrap_or(true));
|
||||||
|
let validate_hints = metadata
|
||||||
|
.config
|
||||||
|
.validate_hints
|
||||||
|
.unwrap_or(header_configuration.validate_hints.unwrap_or(true));
|
||||||
|
let validate_autocomplete = metadata
|
||||||
|
.config
|
||||||
|
.validate_autocomplete
|
||||||
|
.unwrap_or(header_configuration.validate_autocomplete.unwrap_or(false));
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
writeln!(output, "Subtest {i} runs with compare_ref={compare_ref}; validate_hints={validate_hints}; validate_autocomplete={validate_autocomplete};").unwrap();
|
||||||
|
}
|
||||||
|
ok &= test_spans(output, source.root());
|
||||||
|
ok &= test_reparse(output, source.text(), i, rng);
|
||||||
|
|
||||||
// Don't retain frames if we don't want to compare with reference images.
|
// Don't retain frames if we don't want to compare with reference images.
|
||||||
if !compare_ref {
|
if !compare_ref {
|
||||||
frames.clear();
|
frames.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we never check autocomplete and error at the same time
|
||||||
|
|
||||||
|
let diagnostic_annotations = metadata
|
||||||
|
.annotations
|
||||||
|
.iter()
|
||||||
|
.filter(|a| {
|
||||||
|
!matches!(
|
||||||
|
a.kind,
|
||||||
|
AnnotationKind::AutocompleteContains
|
||||||
|
| AnnotationKind::AutocompleteExcludes
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
if validate_autocomplete {
|
||||||
|
// warns and ignores diagnostics
|
||||||
|
if !diagnostic_annotations.is_empty() {
|
||||||
|
writeln!(
|
||||||
|
output,
|
||||||
|
" Subtest {i} contains diagnostics but is in autocomplete mode."
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
for annotation in diagnostic_annotations {
|
||||||
|
write!(output, " Ignored | ").unwrap();
|
||||||
|
print_annotation(output, &source, line, &annotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test_autocomplete(
|
||||||
|
output,
|
||||||
|
world,
|
||||||
|
&source,
|
||||||
|
line,
|
||||||
|
i,
|
||||||
|
&mut ok,
|
||||||
|
metadata.annotations.iter(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
test_diagnostics(
|
||||||
|
output,
|
||||||
|
world,
|
||||||
|
&source,
|
||||||
|
line,
|
||||||
|
i,
|
||||||
|
&mut ok,
|
||||||
|
validate_hints,
|
||||||
|
diagnostics.iter(),
|
||||||
|
&diagnostic_annotations,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
(ok, compare_ref, frames)
|
||||||
|
}
|
||||||
|
Err(invalid_data) => {
|
||||||
|
writeln!(output, " Subtest {i} has invalid metadata, failing the test:")
|
||||||
|
.unwrap();
|
||||||
|
InvalidMetadata::write(
|
||||||
|
invalid_data,
|
||||||
|
output,
|
||||||
|
&mut |annotation: &Annotation, output: &mut String| {
|
||||||
|
print_annotation(output, &source, line, annotation)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
(false, false, frames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn test_autocomplete<'a>(
|
||||||
|
output: &mut String,
|
||||||
|
world: &mut TestWorld,
|
||||||
|
source: &Source,
|
||||||
|
line: usize,
|
||||||
|
i: usize,
|
||||||
|
ok: &mut bool,
|
||||||
|
annotations: impl Iterator<Item = &'a Annotation>,
|
||||||
|
) {
|
||||||
|
for annotation in annotations.filter(|a| {
|
||||||
|
matches!(
|
||||||
|
a.kind,
|
||||||
|
AnnotationKind::AutocompleteContains | AnnotationKind::AutocompleteExcludes
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
// Ok cause we checked in parsing that range was Some for this annotation
|
||||||
|
let cursor = annotation.range.as_ref().unwrap().start;
|
||||||
|
|
||||||
|
// todo, use document if is_some to test labels autocomplete
|
||||||
|
let completions = typst_ide::autocomplete(world, None, source, cursor, true)
|
||||||
|
.map(|(_, c)| c)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| c.label.to_string())
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
let completions =
|
||||||
|
completions.iter().map(|s| s.as_str()).collect::<HashSet<&str>>();
|
||||||
|
|
||||||
|
let must_contain_or_exclude = parse_string_list(&annotation.text);
|
||||||
|
let missing =
|
||||||
|
must_contain_or_exclude.difference(&completions).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if !missing.is_empty()
|
||||||
|
&& matches!(annotation.kind, AnnotationKind::AutocompleteContains)
|
||||||
|
{
|
||||||
|
writeln!(output, " Subtest {i} does not match expected completions.")
|
||||||
|
.unwrap();
|
||||||
|
write!(output, " for annotation | ").unwrap();
|
||||||
|
print_annotation(output, source, line, annotation);
|
||||||
|
|
||||||
|
write!(output, " Not contained | ").unwrap();
|
||||||
|
for item in missing {
|
||||||
|
write!(output, "{item:?}, ").unwrap()
|
||||||
|
}
|
||||||
|
writeln!(output).unwrap();
|
||||||
|
*ok = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let undesired =
|
||||||
|
must_contain_or_exclude.intersection(&completions).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if !undesired.is_empty()
|
||||||
|
&& matches!(annotation.kind, AnnotationKind::AutocompleteExcludes)
|
||||||
|
{
|
||||||
|
writeln!(output, " Subtest {i} does not match expected completions.")
|
||||||
|
.unwrap();
|
||||||
|
write!(output, " for annotation | ").unwrap();
|
||||||
|
print_annotation(output, source, line, annotation);
|
||||||
|
|
||||||
|
write!(output, " Not excluded| ").unwrap();
|
||||||
|
for item in undesired {
|
||||||
|
write!(output, "{item:?}, ").unwrap()
|
||||||
|
}
|
||||||
|
writeln!(output).unwrap();
|
||||||
|
*ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn test_diagnostics<'a>(
|
||||||
|
output: &mut String,
|
||||||
|
world: &mut TestWorld,
|
||||||
|
source: &Source,
|
||||||
|
line: usize,
|
||||||
|
i: usize,
|
||||||
|
ok: &mut bool,
|
||||||
|
validate_hints: bool,
|
||||||
|
diagnostics: impl Iterator<Item = &'a SourceDiagnostic>,
|
||||||
|
diagnostic_annotations: &HashSet<Annotation>,
|
||||||
|
) {
|
||||||
// Map diagnostics to range and message format, discard traces and errors from
|
// Map diagnostics to range and message format, discard traces and errors from
|
||||||
// other files, collect hints.
|
// other files, collect hints.
|
||||||
//
|
//
|
||||||
@ -594,7 +802,7 @@ fn test_part(
|
|||||||
// verify if a hint belongs to a diagnostic or not. That should be irrelevant
|
// verify if a hint belongs to a diagnostic or not. That should be irrelevant
|
||||||
// however, as the line of the hint is still verified.
|
// however, as the line of the hint is still verified.
|
||||||
let mut actual_diagnostics = HashSet::new();
|
let mut actual_diagnostics = HashSet::new();
|
||||||
for diagnostic in &diagnostics {
|
for diagnostic in diagnostics {
|
||||||
// Ignore diagnostics from other files.
|
// Ignore diagnostics from other files.
|
||||||
if diagnostic.span.id().map_or(false, |id| id != source.id()) {
|
if diagnostic.span.id().map_or(false, |id| id != source.id()) {
|
||||||
continue;
|
continue;
|
||||||
@ -606,14 +814,14 @@ fn test_part(
|
|||||||
Severity::Warning => AnnotationKind::Warning,
|
Severity::Warning => AnnotationKind::Warning,
|
||||||
},
|
},
|
||||||
range: world.range(diagnostic.span),
|
range: world.range(diagnostic.span),
|
||||||
message: diagnostic.message.replace("\\", "/"),
|
text: diagnostic.message.replace("\\", "/"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if validate_hints {
|
if validate_hints {
|
||||||
for hint in &diagnostic.hints {
|
for hint in &diagnostic.hints {
|
||||||
actual_diagnostics.insert(Annotation {
|
actual_diagnostics.insert(Annotation {
|
||||||
kind: AnnotationKind::Hint,
|
kind: AnnotationKind::Hint,
|
||||||
message: hint.clone(),
|
text: hint.clone(),
|
||||||
range: annotation.range.clone(),
|
range: annotation.range.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -624,10 +832,9 @@ fn test_part(
|
|||||||
|
|
||||||
// Basically symmetric_difference, but we need to know where an item is coming from.
|
// Basically symmetric_difference, but we need to know where an item is coming from.
|
||||||
let mut unexpected_outputs = actual_diagnostics
|
let mut unexpected_outputs = actual_diagnostics
|
||||||
.difference(&metadata.annotations)
|
.difference(diagnostic_annotations)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut missing_outputs = metadata
|
let mut missing_outputs = diagnostic_annotations
|
||||||
.annotations
|
|
||||||
.difference(&actual_diagnostics)
|
.difference(&actual_diagnostics)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
@ -638,20 +845,28 @@ fn test_part(
|
|||||||
// Is this reasonable or subject to change?
|
// Is this reasonable or subject to change?
|
||||||
if !(unexpected_outputs.is_empty() && missing_outputs.is_empty()) {
|
if !(unexpected_outputs.is_empty() && missing_outputs.is_empty()) {
|
||||||
writeln!(output, " Subtest {i} does not match expected errors.").unwrap();
|
writeln!(output, " Subtest {i} does not match expected errors.").unwrap();
|
||||||
ok = false;
|
*ok = false;
|
||||||
|
|
||||||
for unexpected in unexpected_outputs {
|
for unexpected in unexpected_outputs {
|
||||||
write!(output, " Not annotated | ").unwrap();
|
write!(output, " Not annotated | ").unwrap();
|
||||||
print_annotation(output, &source, line, unexpected)
|
print_annotation(output, source, line, unexpected)
|
||||||
}
|
}
|
||||||
|
|
||||||
for missing in missing_outputs {
|
for missing in missing_outputs {
|
||||||
write!(output, " Not emitted | ").unwrap();
|
write!(output, " Not emitted | ").unwrap();
|
||||||
print_annotation(output, &source, line, missing)
|
print_annotation(output, source, line, missing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(ok, compare_ref, frames)
|
fn print_model(world: &mut TestWorld, source: &Source, output: &mut String) {
|
||||||
|
let world = (world as &dyn World).track();
|
||||||
|
let route = typst::engine::Route::default();
|
||||||
|
let mut tracer = typst::eval::Tracer::new();
|
||||||
|
|
||||||
|
let module =
|
||||||
|
typst::eval::eval(world, route.track(), tracer.track_mut(), source).unwrap();
|
||||||
|
writeln!(output, "Model:\n{:#?}\n", module.content()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_annotation(
|
fn print_annotation(
|
||||||
@ -660,7 +875,7 @@ fn print_annotation(
|
|||||||
line: usize,
|
line: usize,
|
||||||
annotation: &Annotation,
|
annotation: &Annotation,
|
||||||
) {
|
) {
|
||||||
let Annotation { range, message, kind } = annotation;
|
let Annotation { range, text, kind } = annotation;
|
||||||
write!(output, "{kind}: ").unwrap();
|
write!(output, "{kind}: ").unwrap();
|
||||||
if let Some(range) = range {
|
if let Some(range) = range {
|
||||||
let start_line = 1 + line + source.byte_to_line(range.start).unwrap();
|
let start_line = 1 + line + source.byte_to_line(range.start).unwrap();
|
||||||
@ -669,107 +884,7 @@ fn print_annotation(
|
|||||||
let end_col = 1 + source.byte_to_column(range.end).unwrap();
|
let end_col = 1 + source.byte_to_column(range.end).unwrap();
|
||||||
write!(output, "{start_line}:{start_col}-{end_line}:{end_col}: ").unwrap();
|
write!(output, "{start_line}:{start_col}-{end_line}:{end_col}: ").unwrap();
|
||||||
}
|
}
|
||||||
writeln!(output, "{message}").unwrap();
|
writeln!(output, "{text}").unwrap();
|
||||||
}
|
|
||||||
|
|
||||||
struct TestConfiguration {
|
|
||||||
compare_ref: Option<bool>,
|
|
||||||
validate_hints: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TestPartMetadata {
|
|
||||||
part_configuration: TestConfiguration,
|
|
||||||
annotations: HashSet<Annotation>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
|
||||||
struct Annotation {
|
|
||||||
range: Option<Range<usize>>,
|
|
||||||
message: EcoString,
|
|
||||||
kind: AnnotationKind,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
||||||
enum AnnotationKind {
|
|
||||||
Error,
|
|
||||||
Warning,
|
|
||||||
Hint,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AnnotationKind {
|
|
||||||
fn iter() -> impl Iterator<Item = Self> {
|
|
||||||
[AnnotationKind::Error, AnnotationKind::Warning, AnnotationKind::Hint].into_iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
AnnotationKind::Error => "Error",
|
|
||||||
AnnotationKind::Warning => "Warning",
|
|
||||||
AnnotationKind::Hint => "Hint",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for AnnotationKind {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
||||||
f.pad(self.as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_part_metadata(source: &Source) -> TestPartMetadata {
|
|
||||||
let mut compare_ref = None;
|
|
||||||
let mut validate_hints = None;
|
|
||||||
let mut annotations = HashSet::default();
|
|
||||||
|
|
||||||
let lines: Vec<_> = source.text().lines().map(str::trim).collect();
|
|
||||||
for (i, line) in lines.iter().enumerate() {
|
|
||||||
compare_ref = get_flag_metadata(line, "Ref").or(compare_ref);
|
|
||||||
validate_hints = get_flag_metadata(line, "Hints").or(validate_hints);
|
|
||||||
|
|
||||||
fn 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
let comments_until_code =
|
|
||||||
lines[i..].iter().take_while(|line| line.starts_with("//")).count();
|
|
||||||
|
|
||||||
let pos = |s: &mut Scanner| -> Option<usize> {
|
|
||||||
let first = num(s)? - 1;
|
|
||||||
let (delta, column) =
|
|
||||||
if s.eat_if(':') { (first, num(s)? - 1) } else { (0, first) };
|
|
||||||
let line = (i + comments_until_code).checked_add_signed(delta)?;
|
|
||||||
source.line_column_to_byte(line, usize::try_from(column).ok()?)
|
|
||||||
};
|
|
||||||
|
|
||||||
let range = |s: &mut Scanner| -> Option<Range<usize>> {
|
|
||||||
let start = pos(s)?;
|
|
||||||
let end = if s.eat_if('-') { pos(s)? } else { start };
|
|
||||||
Some(start..end)
|
|
||||||
};
|
|
||||||
|
|
||||||
for kind in AnnotationKind::iter() {
|
|
||||||
let Some(expectation) = get_metadata(line, kind.as_str()) else { continue };
|
|
||||||
let mut s = Scanner::new(expectation);
|
|
||||||
let range = range(&mut s);
|
|
||||||
let rest = if range.is_some() { s.after() } else { s.string() };
|
|
||||||
let message = rest
|
|
||||||
.trim()
|
|
||||||
.replace("VERSION", &PackageVersion::compiler().to_string())
|
|
||||||
.into();
|
|
||||||
annotations.insert(Annotation { kind, range, message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TestPartMetadata {
|
|
||||||
part_configuration: TestConfiguration { compare_ref, validate_hints },
|
|
||||||
annotations,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pseudorandomly edit the source file and test whether a reparse produces the
|
/// Pseudorandomly edit the source file and test whether a reparse produces the
|
||||||
|
13
tests/typ/autocomplete/showcase.typ
Normal file
13
tests/typ/autocomplete/showcase.typ
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// Autocomplete: true
|
||||||
|
// Ref: false
|
||||||
|
|
||||||
|
---
|
||||||
|
// Autocomplete contains: -1 "int", "if conditional"
|
||||||
|
// Autocomplete excludes: -1 "foo"
|
||||||
|
#i
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
// Autocomplete contains: -1 "insert", "remove", "len", "all"
|
||||||
|
// Autocomplete excludes: -1 "foobar", "foo",
|
||||||
|
#().
|
Loading…
x
Reference in New Issue
Block a user