Refine test infrastructure

- Tests diagnostics
- More and better separated image tests
This commit is contained in:
Laurenz 2020-12-10 22:44:35 +01:00
parent fdc1b378a3
commit 1cbd5f3051
25 changed files with 240 additions and 126 deletions

3
.gitignore vendored
View File

@ -3,6 +3,5 @@
**/*.rs.bk
Cargo.lock
bench/target
tests/png
tests/pdf
tests/out
_things

View File

@ -8,26 +8,22 @@ edition = "2018"
members = ["bench"]
[features]
default = ["fs", "anyhow"]
default = ["cli", "fs"]
cli = ["fs", "anyhow"]
fs = ["fontdock/fs"]
[dependencies]
fontdock = { path = "../fontdock", default-features = false }
pdf-writer = { path = "../pdf-writer" }
deflate = { version = "0.8.6" }
fontdock = { path = "../fontdock", default-features = false }
image = { version = "0.23", default-features = false, features = ["jpeg", "png"] }
itoa = "0.4"
pdf-writer = { path = "../pdf-writer" }
ttf-parser = "0.8.2"
unicode-xid = "0.2"
# feature = "serde"
anyhow = { version = "1", optional = true }
serde = { version = "1", features = ["derive"], optional = true }
# for the CLI
anyhow = { version = "1", optional = true }
[dev-dependencies]
memmap = "0.7"
tiny-skia = "0.2"
[profile.dev]
@ -38,7 +34,7 @@ lto = true
[[bin]]
name = "typst"
required-features = ["fs", "anyhow"]
required-features = ["cli"]
[[test]]
name = "typeset"

View File

@ -13,7 +13,7 @@ use typst::parse::parse;
use typst::typeset;
const FONT_DIR: &str = "../fonts";
const COMA: &str = include_str!("../../tests/typ/coma.typ");
const COMA: &str = include_str!("../../tests/typ/example-coma.typ");
fn benchmarks(c: &mut Criterion) {
macro_rules! bench {

View File

@ -17,6 +17,11 @@ impl Point {
pub fn new(x: Length, y: Length) -> Self {
Self { x, y }
}
/// Create an instance with two equal components.
pub fn uniform(value: Length) -> Self {
Self { x: value, y: value }
}
}
impl Get<SpecAxis> for Point {

View File

@ -21,6 +21,11 @@ impl Size {
Self { width, height }
}
/// Create an instance with two equal components.
pub fn uniform(value: Length) -> Self {
Self { width: value, height: value }
}
/// Whether the other size fits into this one (smaller width and height).
pub fn fits(self, other: Self) -> bool {
self.width >= other.width && self.height >= other.height

View File

@ -59,8 +59,8 @@ fn main() -> anyhow::Result<()> {
let map = LineMap::new(&src);
for diag in diags {
let span = diag.span;
let start = map.location(span.start);
let end = map.location(span.end);
let start = map.location(span.start).unwrap();
let end = map.location(span.end).unwrap();
println!(
"{}: {}:{}-{}: {}",
diag.v.level,

View File

@ -1,7 +1,7 @@
//! Conversion of byte positions to line/column locations.
use super::Scanner;
use crate::syntax::{Location, Pos};
use crate::syntax::{Location, Offset, Pos};
/// Enables conversion of byte position to locations.
pub struct LineMap<'s> {
@ -25,23 +25,48 @@ impl<'s> LineMap<'s> {
}
/// Convert a byte position to a location.
///
/// # Panics
/// This panics if the position is out of bounds.
pub fn location(&self, pos: Pos) -> Location {
pub fn location(&self, pos: Pos) -> Option<Location> {
// Find the line which contains the position.
let line_index = match self.line_starts.binary_search(&pos) {
Ok(i) => i,
Err(i) => i - 1,
};
let line_start = self.line_starts[line_index];
let head = &self.src[line_start.to_usize() .. pos.to_usize()];
let start = self.line_starts.get(line_index)?;
let head = self.src.get(start.to_usize() .. pos.to_usize())?;
let column_index = head.chars().count();
Location {
Some(Location {
line: 1 + line_index as u32,
column: 1 + column_index as u32,
})
}
/// Convert a location to a byte position.
pub fn pos(&self, location: Location) -> Option<Pos> {
// Determine the boundaries of the line.
let line_idx = location.line.checked_sub(1)? as usize;
let line_start = self.line_starts.get(line_idx)?;
let line_end = self
.line_starts
.get(location.line as usize)
.map_or(self.src.len(), |pos| pos.to_usize());
let line = self.src.get(line_start.to_usize() .. line_end)?;
// Find the index in the line. For the first column, the index is always zero. For
// other columns, we have to look at which byte the char directly before the
// column in question ends. We can't do `nth(column_idx)` directly since the
// column may be behind the last char.
let column_idx = location.column.checked_sub(1)? as usize;
let line_offset = if let Some(prev_idx) = column_idx.checked_sub(1) {
let (idx, prev) = line.char_indices().nth(prev_idx)?;
idx + prev.len_utf8()
} else {
0
};
Some(line_start.offset(Pos(line_offset as u32)))
}
}
@ -71,18 +96,26 @@ mod tests {
#[test]
fn test_line_map_location() {
let map = LineMap::new(TEST);
assert_eq!(map.location(Pos(0)), Location::new(1, 1));
assert_eq!(map.location(Pos(2)), Location::new(1, 2));
assert_eq!(map.location(Pos(6)), Location::new(1, 6));
assert_eq!(map.location(Pos(7)), Location::new(2, 1));
assert_eq!(map.location(Pos(8)), Location::new(2, 2));
assert_eq!(map.location(Pos(12)), Location::new(2, 3));
assert_eq!(map.location(Pos(21)), Location::new(4, 4));
assert_eq!(map.location(Pos(0)), Some(Location::new(1, 1)));
assert_eq!(map.location(Pos(2)), Some(Location::new(1, 2)));
assert_eq!(map.location(Pos(6)), Some(Location::new(1, 6)));
assert_eq!(map.location(Pos(7)), Some(Location::new(2, 1)));
assert_eq!(map.location(Pos(8)), Some(Location::new(2, 2)));
assert_eq!(map.location(Pos(12)), Some(Location::new(2, 3)));
assert_eq!(map.location(Pos(21)), Some(Location::new(4, 4)));
assert_eq!(map.location(Pos(22)), None);
}
#[test]
#[should_panic]
fn test_line_map_panics_out_of_bounds() {
LineMap::new(TEST).location(Pos(22));
fn test_line_map_pos() {
fn assert_round_trip(map: &LineMap, pos: Pos) {
assert_eq!(map.location(pos).and_then(|loc| map.pos(loc)), Some(pos));
}
let map = LineMap::new(TEST);
assert_round_trip(&map, Pos(0));
assert_round_trip(&map, Pos(7));
assert_round_trip(&map, Pos(12));
assert_round_trip(&map, Pos(21));
}
}

View File

@ -56,24 +56,31 @@ use Unit::*;
fn Id(ident: &str) -> Expr {
Expr::Lit(Lit::Ident(Ident(ident.to_string())))
}
fn Bool(b: bool) -> Expr {
Expr::Lit(Lit::Bool(b))
}
fn Int(int: i64) -> Expr {
Expr::Lit(Lit::Int(int))
}
fn Float(float: f64) -> Expr {
Expr::Lit(Lit::Float(float))
}
fn Percent(percent: f64) -> Expr {
Expr::Lit(Lit::Percent(percent))
}
fn Length(val: f64, unit: Unit) -> Expr {
Expr::Lit(Lit::Length(val, unit))
}
fn Color(color: RgbaColor) -> Expr {
Expr::Lit(Lit::Color(color))
}
fn Str(string: &str) -> Expr {
Expr::Lit(Lit::Str(string.to_string()))
}
@ -98,6 +105,7 @@ fn Unary(op: impl Into<Spanned<UnOp>>, expr: impl Into<Spanned<Expr>>) -> Expr {
expr: expr.into().map(Box::new),
})
}
fn Binary(
op: impl Into<Spanned<BinOp>>,
lhs: impl Into<Spanned<Expr>>,

View File

@ -1,8 +1,7 @@
# Tests
- `typ`: Input files
- `pdf`: PDF files produced by tests
- `png`: PNG files produced by tests
- `cmp`: Reference images which the PNGs are compared to byte-wise to determine
whether the test passed or failed
- `ref`: Reference images which the output is compared with to determine
whether a test passed or failed
- `res`: Resource files used by tests
- `out`: PNG and PDF files produced by tests

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

BIN
tests/ref/empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

BIN
tests/ref/example-coma.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
tests/ref/image-error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

BIN
tests/ref/image-fit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

BIN
tests/ref/image-jpeg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
tests/ref/image-png.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

0
tests/typ/empty.typ Normal file
View File

View File

@ -1,3 +1,5 @@
// Small integration test of syntax, page setup, box layout and alignment.
[page: width=450pt, height=300pt, margins=1cm]
[box][

View File

@ -0,0 +1,8 @@
// error: 5:9-5:30 failed to load image
// error: 8:9-8:30 failed to load image
// File does not exist.
[image: "path/does/not/exist"]
// File exists, but is no image.
[image: "typ/image-error.typ"]

21
tests/typ/image-fit.typ Normal file
View File

@ -0,0 +1,21 @@
// Fit to width of page.
[image: "res/rhino.png"]
// Fit to height of page.
[page: width=270pt][
[image: "res/rhino.png"]
]
// Set width explicitly.
[image: "res/rhino.png", width=50pt]
// Set height explicitly.
[image: "res/rhino.png", height=50pt]
// Set width and height explicitly and force stretching.
[image: "res/rhino.png", width=25pt, height=50pt]
// Make sure the bounding-box of the image is correct.
[align: bottom, right][
[image: "res/tiger.jpg"]
]

2
tests/typ/image-jpeg.typ Normal file
View File

@ -0,0 +1,2 @@
// Load an RGB JPEG image.
[image: "res/tiger.jpg"]

2
tests/typ/image-png.typ Normal file
View File

@ -0,0 +1,2 @@
// Load an RGBA PNG image.
[image: "res/rhino.png"]

View File

@ -1,15 +0,0 @@
[page: width=5cm, height=5cm, margins=0.25cm]
[image: "res/tiger.jpg"]
[pagebreak]
# Tiger
[image: "res/tiger.jpg", width=2cm]
[image: "res/rhino.png", width=1cm]
[image: "res/rhino.png", height=2cm]
[pagebreak]
[align: center, bottom]
[image: "res/tiger.jpg", width=2cm, height=3.5cm]

View File

@ -1,35 +1,35 @@
use std::cell::RefCell;
use std::env;
use std::ffi::OsStr;
use std::fs::{self, File};
use std::fs;
use std::path::Path;
use std::rc::Rc;
use fontdock::fs::{FsIndex, FsSource};
use image::{GenericImageView, Rgba};
use memmap::Mmap;
use tiny_skia::{
Canvas, Color, ColorU8, FillRule, FilterQuality, Paint, PathBuilder, Pattern, Pixmap,
Rect, SpreadMode, Transform,
};
use ttf_parser::OutlineBuilder;
use typst::diag::{Feedback, Pass};
use typst::diag::{Diag, Feedback, Level, Pass};
use typst::env::{Env, ImageResource, ResourceLoader, SharedEnv};
use typst::eval::State;
use typst::export::pdf;
use typst::font::FontLoader;
use typst::geom::{Length, Point};
use typst::geom::{Length, Point, Sides, Size};
use typst::layout::{BoxLayout, ImageElement, LayoutElement};
use typst::parse::LineMap;
use typst::parse::{LineMap, Scanner};
use typst::shaping::Shaped;
use typst::syntax::{Location, Pos, SpanVec, SpanWith, Spanned};
use typst::typeset;
const FONT_DIR: &str = "../fonts";
const TYP_DIR: &str = "typ";
const PDF_DIR: &str = "pdf";
const PNG_DIR: &str = "png";
const CMP_DIR: &str = "cmp";
const REF_DIR: &str = "ref";
const PNG_DIR: &str = "out/png";
const PDF_DIR: &str = "out/pdf";
const FONT_DIR: &str = "../fonts";
fn main() {
env::set_current_dir(env::current_dir().unwrap().join("tests")).unwrap();
@ -44,12 +44,8 @@ fn main() {
}
let name = src_path.file_stem().unwrap().to_string_lossy().to_string();
let pdf_path = Path::new(PDF_DIR).join(&name).with_extension("pdf");
let png_path = Path::new(PNG_DIR).join(&name).with_extension("png");
let ref_path = Path::new(CMP_DIR).join(&name).with_extension("png");
if filter.matches(&name) {
filtered.push((name, src_path, pdf_path, png_path, ref_path));
filtered.push((name, src_path));
}
}
@ -62,8 +58,8 @@ fn main() {
println!("Running {} tests", len);
}
fs::create_dir_all(PDF_DIR).unwrap();
fs::create_dir_all(PNG_DIR).unwrap();
fs::create_dir_all(PDF_DIR).unwrap();
let mut index = FsIndex::new();
index.search_dir(FONT_DIR);
@ -76,29 +72,12 @@ fn main() {
let mut ok = true;
for (name, src_path, pdf_path, png_path, ref_path) in filtered {
print!("Testing {}.", name);
test(&src_path, &pdf_path, &png_path, &env);
for (name, src_path) in filtered {
let png_path = Path::new(PNG_DIR).join(&name).with_extension("png");
let pdf_path = Path::new(PDF_DIR).join(&name).with_extension("pdf");
let ref_path = Path::new(REF_DIR).join(&name).with_extension("png");
let png_file = File::open(&png_path).unwrap();
let ref_file = match File::open(&ref_path) {
Ok(file) => file,
Err(_) => {
println!(" Failed to open reference image. ❌");
ok = false;
continue;
}
};
let a = unsafe { Mmap::map(&png_file).unwrap() };
let b = unsafe { Mmap::map(&ref_file).unwrap() };
if *a != *b {
println!(" Does not match reference image. ❌");
ok = false;
} else {
println!(" Okay. ✔");
}
ok &= test(&name, &src_path, &pdf_path, &png_path, &ref_path, &env);
}
if !ok {
@ -106,41 +85,6 @@ fn main() {
}
}
fn test(src_path: &Path, pdf_path: &Path, png_path: &Path, env: &SharedEnv) {
let src = fs::read_to_string(src_path).unwrap();
let state = State::default();
let Pass {
output: layouts,
feedback: Feedback { mut diags, .. },
} = typeset(&src, Rc::clone(env), state);
if !diags.is_empty() {
diags.sort();
let map = LineMap::new(&src);
for diag in diags {
let span = diag.span;
let start = map.location(span.start);
let end = map.location(span.end);
println!(
" {}: {}:{}-{}: {}",
diag.v.level,
src_path.display(),
start,
end,
diag.v.message,
);
}
}
let env = env.borrow();
let canvas = draw(&layouts, &env, 2.0);
canvas.pixmap.save_png(png_path).unwrap();
let pdf_data = pdf::export(&layouts, &env);
fs::write(pdf_path, pdf_data).unwrap();
}
struct TestFilter {
filter: Vec<String>,
perfect: bool,
@ -171,6 +115,111 @@ impl TestFilter {
}
}
fn test(
name: &str,
src_path: &Path,
pdf_path: &Path,
png_path: &Path,
ref_path: &Path,
env: &SharedEnv,
) -> bool {
println!("Testing {}.", name);
let src = fs::read_to_string(src_path).unwrap();
let map = LineMap::new(&src);
let ref_diags = parse_diags(&src, &map);
let mut state = State::default();
state.page.size = Size::uniform(Length::pt(120.0));
state.page.margins = Sides::uniform(Some(Length::pt(10.0).into()));
let Pass {
output: layouts,
feedback: Feedback { mut diags, .. },
} = typeset(&src, Rc::clone(env), state);
diags.sort();
let env = env.borrow();
let canvas = draw(&layouts, &env, 2.0);
canvas.pixmap.save_png(png_path).unwrap();
let pdf_data = pdf::export(&layouts, &env);
fs::write(pdf_path, pdf_data).unwrap();
let mut ok = true;
if diags != ref_diags {
println!(" Does not match expected diagnostics. ❌");
ok = false;
for diag in &diags {
if ref_diags.binary_search(diag).is_err() {
print!(" Unexpected | ");
print_diag(diag, &map);
}
}
for diag in &ref_diags {
if diags.binary_search(diag).is_err() {
print!(" Missing | ");
print_diag(diag, &map);
}
}
}
if let Ok(ref_pixmap) = Pixmap::load_png(&ref_path) {
if canvas.pixmap != ref_pixmap {
println!(" Does not match reference image. ❌");
ok = false;
}
} else {
println!(" Failed to open reference image. ❌");
ok = false;
}
if ok {
println!("\x1b[1ATesting {}. ✔", name);
}
ok
}
fn parse_diags(src: &str, map: &LineMap) -> SpanVec<Diag> {
let mut diags = vec![];
for line in src.lines() {
let (level, rest) = if let Some(rest) = line.strip_prefix("// error: ") {
(Level::Error, rest)
} else if let Some(rest) = line.strip_prefix("// warning: ") {
(Level::Warning, rest)
} else {
continue;
};
fn pos(s: &mut Scanner, map: &LineMap) -> Pos {
let (line, _, column) = (num(s), s.eat_assert(':'), num(s));
map.pos(Location { line, column }).unwrap()
}
fn num(s: &mut Scanner) -> u32 {
s.eat_while(|c| c.is_numeric()).parse().unwrap()
}
let mut s = Scanner::new(rest);
let (start, _, end) = (pos(&mut s, map), s.eat_assert('-'), pos(&mut s, map));
diags.push(Diag::new(level, s.rest().trim()).span_with(start .. end));
}
diags.sort();
diags
}
fn print_diag(diag: &Spanned<Diag>, map: &LineMap) {
let start = map.location(diag.span.start).unwrap();
let end = map.location(diag.span.end).unwrap();
println!("{}: {}-{}: {}", diag.v.level, start, end, diag.v.message,);
}
fn draw(layouts: &[BoxLayout], env: &Env, pixel_per_pt: f32) -> Canvas {
let pad = Length::pt(5.0);