Refine test infrastructure ✅
- Tests diagnostics - More and better separated image tests
3
.gitignore
vendored
@ -3,6 +3,5 @@
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
bench/target
|
||||
tests/png
|
||||
tests/pdf
|
||||
tests/out
|
||||
_things
|
||||
|
16
Cargo.toml
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -59,10 +59,10 @@ 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,
|
||||
src_path.display(),
|
||||
start,
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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>>,
|
||||
|
@ -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
|
||||
|
Before Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 275 KiB |
BIN
tests/ref/empty.png
Normal file
After Width: | Height: | Size: 120 B |
BIN
tests/ref/example-coma.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
tests/ref/image-error.png
Normal file
After Width: | Height: | Size: 120 B |
BIN
tests/ref/image-fit.png
Normal file
After Width: | Height: | Size: 186 KiB |
BIN
tests/ref/image-jpeg.png
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
tests/ref/image-png.png
Normal file
After Width: | Height: | Size: 37 KiB |
0
tests/typ/empty.typ
Normal file
@ -1,3 +1,5 @@
|
||||
// Small integration test of syntax, page setup, box layout and alignment.
|
||||
|
||||
[page: width=450pt, height=300pt, margins=1cm]
|
||||
|
||||
[box][
|
8
tests/typ/image-error.typ
Normal 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
@ -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
@ -0,0 +1,2 @@
|
||||
// Load an RGB JPEG image.
|
||||
[image: "res/tiger.jpg"]
|
2
tests/typ/image-png.typ
Normal file
@ -0,0 +1,2 @@
|
||||
// Load an RGBA PNG image.
|
||||
[image: "res/rhino.png"]
|
@ -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]
|
193
tests/typeset.rs
@ -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);
|
||||
|
||||
|