Skip to content

Commit

Permalink
Added export target to test suite
Browse files Browse the repository at this point in the history
  • Loading branch information
tingerrr committed Jun 13, 2024
1 parent ffedd95 commit 442a179
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 61 deletions.
12 changes: 11 additions & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 62,14 @@ making changes.
testit --pdf
```

The same applies to `--svg` and `--extended`, see target tests below.

## Writing tests
The syntax for an individual test is `--- {name} ---` followed by some Typst
code that should be tested. The name must be globally unique in the test suite,
so that tests can be easily migrated across files.

There are, broadly speaking, three kinds of tests:
There are, broadly speaking, four kinds of tests:

- Tests that just ensure that the code runs successfully: Those typically make
use of `test` or `assert.eq` (both are very similar, `test` is just shorter)
Expand All @@ -80,6 82,14 @@ There are, broadly speaking, three kinds of tests:
below. If the code span is in a line further below, you can write ranges
like `3:2-3:7` to indicate the 2-7 column in the 3rd non-comment line.

- Tests for specific export targets: Those have an annotation like
`// Target: pdf` that specifies the export target and are only run for that
target. A target annotation has no range and must have a target, i.e one of
"pdf", "svg", "raster". Only one or no target annotation is valid per test.
A test without a target annotation is run for "raster" by default and for all
targets if `--extended` is used. This means that tests for "pdf" are _not_ run
unlesss `--pdf` or `--extended` are passed.

- Tests that ensure certain visual output is produced: Those render the result
of the test with the `typst-render` crate and compare against a reference
image stored in the repository. The test runner automatically detects whether
Expand Down
20 changes: 12 additions & 8 deletions tests/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 2,7 @@ use std::path::PathBuf;

use clap::{Parser, Subcommand};
use regex::Regex;
use typst::ExportTarget;

/// Typst's test runner.
#[derive(Debug, Clone, Parser)]
Expand Down Expand Up @@ -55,14 56,17 @@ pub struct CliArguments {
}

impl CliArguments {
/// Whether to run PDF export.
pub fn pdf(&self) -> bool {
self.pdf || self.extended
}

/// Whether to run SVG export.
pub fn svg(&self) -> bool {
self.svg || self.extended
/// The target to test for, or `None` if all are tested.
pub fn target(&self) -> Option<ExportTarget> {
if self.extended {
None
} else if self.pdf {
Some(ExportTarget::Pdf)
} else if self.svg {
Some(ExportTarget::Svg)
} else {
Some(ExportTarget::Raster)
}
}
}

Expand Down
54 changes: 53 additions & 1 deletion tests/src/collect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 7,11 @@ use std::str::FromStr;
use ecow::{eco_format, EcoString};
use typst::syntax::package::PackageVersion;
use typst::syntax::{is_id_continue, is_ident, is_newline, FileId, Source, VirtualPath};
use typst::ExportTarget;
use unscanny::Scanner;

use crate::ARGS;

/// Collects all tests from all files.
///
/// Returns:
Expand All @@ -25,6 28,7 @@ pub struct Test {
pub source: Source,
pub notes: Vec<Note>,
pub large: bool,
pub target: Option<ExportTarget>,
}

impl Display for Test {
Expand Down Expand Up @@ -75,6 79,7 @@ pub enum NoteKind {
Error,
Warning,
Hint,
Target,
}

impl FromStr for NoteKind {
Expand All @@ -85,6 90,7 @@ impl FromStr for NoteKind {
"Error" => Self::Error,
"Warning" => Self::Warning,
"Hint" => Self::Hint,
"Target" => Self::Target,
_ => return Err(()),
})
}
Expand All @@ -96,6 102,7 @@ impl Display for NoteKind {
Self::Error => "Error",
Self::Warning => "Warning",
Self::Hint => "Hint",
Self::Target => "Target",
})
}
}
Expand Down Expand Up @@ -280,7 287,35 @@ impl<'a> Parser<'a> {
}
}

self.collector.tests.push(Test { pos, name, source, notes, large });
if notes.iter().filter(|note| note.kind == NoteKind::Target).count() > 1 {
self.error("only one export target is allowed");
}

let target =
notes
.iter()
.position(|note| note.kind == NoteKind::Target)
.map(|idx| match notes.remove(idx).message.as_str() {
"pdf" => ExportTarget::Pdf,
"svg" => ExportTarget::Svg,
"raster" => ExportTarget::Raster,
_ => unreachable!(),
});

let targeted = match (ARGS.target(), target) {
(None, Some(_)) | (Some(_), None) => true,
(x, y) if x == y => true,
_ => false,
};

if !targeted {
self.collector.skipped = 1;
continue;
}

self.collector
.tests
.push(Test { pos, name, source, notes, large, target });
}
}

Expand All @@ -302,15 337,22 @@ impl<'a> Parser<'a> {
/// Parses an annotation in a test.
fn parse_note(&mut self, source: &Source) -> Option<Note> {
let head = self.s.eat_while(is_id_continue);

if !self.s.eat_if(':') {
return None;
}

let kind: NoteKind = head.parse().ok()?;

self.s.eat_if(' ');

let mut range = None;
if self.s.at('-') || self.s.at(char::is_numeric) {
if kind == NoteKind::Target {
self.error("range not allowed for target");
return None;
}

range = self.parse_range(source);
if range.is_none() {
self.error("range is malformed");
Expand All @@ -324,6 366,16 @@ impl<'a> Parser<'a> {
.trim()
.replace("VERSION", &eco_format!("{}", PackageVersion::compiler()));

if kind == NoteKind::Target {
match message.as_str() {
"pdf" | "svg" | "raster" => {}
_ => {
self.error("export target must be one of 'pdf', 'svg','raster'");
return None;
}
}
}

Some(Note {
pos: FilePos::new(self.path, self.line),
kind,
Expand Down
47 changes: 28 additions & 19 deletions tests/src/logger.rs
Original file line number Diff line number Diff line change
@@ -1,6 1,8 @@
use std::io::{self, IsTerminal, StderrLock, Write};
use std::time::{Duration, Instant};

use typst::ExportTarget;

use crate::collect::Test;
use crate::run::TestResult;

Expand Down Expand Up @@ -41,11 43,15 @@ impl<'a> Logger<'a> {
}

/// Register a finished test.
pub fn end(&mut self, test: &'a Test, result: std::thread::Result<TestResult>) {
pub fn end(
&mut self,
test: &'a Test,
results: std::thread::Result<Vec<(ExportTarget, TestResult)>>,
) {
self.active.retain(|t| t.name != test.name);

let result = match result {
Ok(result) => result,
let results = match results {
Ok(results) => results,
Err(_) => {
self.failed = 1;
self.temp_lines = 0;
Expand All @@ -58,30 64,33 @@ impl<'a> Logger<'a> {
}
};

if result.is_ok() {
if results.iter().all(|(_, result)| result.is_ok()) {
self.passed = 1;
} else {
self.failed = 1;
}

self.mismatched_image |= result.mismatched_image;
self.last_change = Instant::now();
for (target, result) in results {
self.mismatched_image |= result.mismatched_image;
self.last_change = Instant::now();

self.print(move |out| {
if !result.errors.is_empty() {
writeln!(out, "❌ {test}")?;
for line in result.errors.lines() {
self.print(move |out| {
if !result.errors.is_empty() {
writeln!(out, "❌ {test} ({target})",)?;

for line in result.errors.lines() {
writeln!(out, " {line}")?;
}
} else if crate::ARGS.verbose || !result.infos.is_empty() {
writeln!(out, "✅ {test} ({target})",)?;
}
for line in result.infos.lines() {
writeln!(out, " {line}")?;
}
} else if crate::ARGS.verbose || !result.infos.is_empty() {
writeln!(out, "✅ {test}")?;
}
for line in result.infos.lines() {
writeln!(out, " {line}")?;
}
Ok(())
})
.unwrap();
Ok(())
})
.unwrap();
}
}

/// Prints a summary and returns whether the test suite passed.
Expand Down
54 changes: 34 additions & 20 deletions tests/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 9,24 @@ use typst::foundations::Smart;
use typst::layout::{Abs, Frame, FrameItem, Page, Transform};
use typst::model::Document;
use typst::visualize::Color;
use typst::WorldExt;
use typst::{ExportTarget, WorldExt};

use crate::collect::{FileSize, NoteKind, Test};
use crate::world::TestWorld;
use crate::ARGS;

/// Runs a single test.
///
/// Returns whether the test passed.
pub fn run(test: &Test) -> TestResult {
Runner::new(test).run()
/// Returns whether the test passed for the given export target.
pub fn run(test: &Test) -> Vec<(ExportTarget, TestResult)> {
if let Some(target) = ARGS.target().or(test.target) {
vec![(target, Runner::new(test, target).run())]
} else {
[ExportTarget::Raster, ExportTarget::Pdf, ExportTarget::Svg]
.map(|target| (target, Runner::new(test, target).run()))
.into_iter()
.collect()
}
}

/// The result of running a single test.
Expand Down Expand Up @@ -51,6 59,7 @@ macro_rules! log {
/// Runs a single test.
pub struct Runner<'a> {
test: &'a Test,
target: ExportTarget,
world: TestWorld,
seen: Vec<bool>,
result: TestResult,
Expand All @@ -59,10 68,11 @@ pub struct Runner<'a> {

impl<'a> Runner<'a> {
/// Create a new test runner.
fn new(test: &'a Test) -> Self {
fn new(test: &'a Test, target: ExportTarget) -> Self {
Self {
test,
world: TestWorld::new(test.source.clone()),
target,
world: TestWorld::new(test.source.clone(), target),
seen: vec![false; test.notes.len()],
result: TestResult {
errors: String::new(),
Expand Down Expand Up @@ -155,6 165,24 @@ impl<'a> Runner<'a> {
return;
}

// Write PDF if requested.
if self.target == ExportTarget::Pdf {
let pdf_path = format!("{}/pdf/{}.pdf", crate::STORE_PATH, self.test.name);
let pdf = typst_pdf::pdf(document, Smart::Auto, None, None);
std::fs::write(pdf_path, pdf).unwrap();
}

// Write SVG if requested.
if self.target == ExportTarget::Svg {
let svg_path = format!("{}/svg/{}.svg", crate::STORE_PATH, self.test.name);
let svg = typst_svg::svg_merged(document, Abs::pt(5.0));
std::fs::write(svg_path, svg).unwrap();
}

if self.target != ExportTarget::Raster {
return;
}

// Render the live version.
let pixmap = render(document, 1.0);

Expand All @@ -170,20 198,6 @@ impl<'a> Runner<'a> {
let data = pixmap_live.encode_png().unwrap();
std::fs::write(&live_path, data).unwrap();

// Write PDF if requested.
if crate::ARGS.pdf() {
let pdf_path = format!("{}/pdf/{}.pdf", crate::STORE_PATH, self.test.name);
let pdf = typst_pdf::pdf(document, Smart::Auto, None, None);
std::fs::write(pdf_path, pdf).unwrap();
}

// Write SVG if requested.
if crate::ARGS.svg() {
let svg_path = format!("{}/svg/{}.svg", crate::STORE_PATH, self.test.name);
let svg = typst_svg::svg_merged(document, Abs::pt(5.0));
std::fs::write(svg_path, svg).unwrap();
}

// Compare against reference image if available.
let equal = has_ref && {
let ref_data = std::fs::read(&ref_path).unwrap();
Expand Down
10 changes: 7 additions & 3 deletions tests/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 75,11 @@ fn test() {
let selected = tests.len();
if ARGS.list {
for test in tests.iter() {
println!("{test}");
print!("{test}");
if let Some(target) = test.target {
print!(" ({target} only)");
}
println!();
}
eprintln!("{selected} selected, {skipped} skipped");
return;
Expand Down Expand Up @@ -104,8 108,8 @@ fn test() {
// to `typst::utils::Deferred` yielding.
tests.iter().par_bridge().for_each(|test| {
logger.lock().start(test);
let result = std::panic::catch_unwind(|| run::run(test));
logger.lock().end(test, result);
let results = std::panic::catch_unwind(|| run::run(test));
logger.lock().end(test, results);
});

sender.send(()).unwrap();
Expand Down
Loading

0 comments on commit 442a179

Please sign in to comment.