Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_austenite/tests/wasm_api.rs

11.3 KiB, 5 runs

created by r1870400018:59839, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! The native half of the wasm compile surface's reporting: page counts, positioned diagnostics, strict
2//! refusals, font families and engine identity, driven through the same `compile` functions the wasm
3//! `DaimondTypst` calls with the source map installed as it installs one. External facts check each: the
4//! PDF's page count from `pdfinfo`, the embedded family names from `fc-scan`, the commit from `git`.
5
6use oxedyne_fe2o3_austenite::compile::{
7 self,
8 Diagnostic,
9 Report,
10};
11use oxedyne_fe2o3_austenite::fonts;
12use oxedyne_fe2o3_austenite::vfs;
13
14use oxedyne_fe2o3_core::prelude::*;
15
16use std::collections::HashMap;
17use std::path::{
18 Path,
19 PathBuf,
20};
21use std::process::Command;
22use std::sync::{
23 Arc,
24 Mutex,
25 MutexGuard,
26};
27
28// The source map is a process global, so the tests that install one take turns.
29static VFS_TURN: Mutex<()> = Mutex::new(());
30
31/// Takes this test's turn at the source map; a turn a failed test poisoned is still a turn.
32fn turn() -> MutexGuard<'static, ()> {
33 VFS_TURN.lock().unwrap_or_else(|p| p.into_inner())
34}
35
36const MAIN: &str = "/proj/main.typ";
37
38/// What a compile of the installed project returned: its report and PDF, or the placed hard error.
39enum Compiled {
40 Done(Report, Vec<u8>),
41 Failed(Diagnostic),
42}
43
44/// Installs `files`, compiles `MAIN` to PDF as the wasm `compileProject` does, and clears the map.
45fn compile_pdf(files: &[(&str, &[u8])]) -> Outcome<Compiled> {
46 let mut map: HashMap<PathBuf, Vec<u8>> = HashMap::new();
47 for (p, b) in files {
48 map.insert(PathBuf::from(p), b.to_vec());
49 }
50 let sources: Vec<PathBuf> = map.keys().cloned().collect();
51 res!(vfs::install(map));
52 let main = PathBuf::from(MAIN);
53 let fonts = Arc::new(res!(fonts::libertinus()));
54 let run = || -> Outcome<(Report, Vec<u8>)> {
55 let (assembled, refusals, skip) = res!(compile::assemble(&main, || Ok(fonts.clone())));
56 let empty = assembled.blocks.is_empty();
57 let mut rendered = res!(compile::author_and_run(assembled));
58 let report = Report::new(rendered.out.pages.len(), &refusals, skip, empty);
59 let pdf = res!(compile::emit_pdf(&mut rendered.out, &rendered.heads));
60 Ok((report, pdf))
61 };
62 let out = match run() {
63 Ok((r, pdf)) => Compiled::Done(r, pdf),
64 Err(e) => Compiled::Failed(compile::locate_error(&e, &main, &sources)),
65 };
66 res!(vfs::clear());
67 Ok(out)
68}
69
70fn done(c: Compiled) -> Outcome<(Report, Vec<u8>)> {
71 match c {
72 Compiled::Done(r, pdf) => Ok((r, pdf)),
73 Compiled::Failed(d) => Err(err!("Expected a compile, got the error {}.", d; Test)),
74 }
75}
76
77fn failed(c: Compiled) -> Outcome<Diagnostic> {
78 match c {
79 Compiled::Failed(d) => Ok(d),
80 Compiled::Done(r, _) => Err(err!("Expected an error, got {} page(s).", r.pages; Test)),
81 }
82}
83
84/// The `Pages:` line `pdfinfo` reads from a PDF written to the scratch target directory.
85fn pdfinfo_pages(pdf: &[u8], name: &str) -> Outcome<usize> {
86 let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR"));
87 let path = dir.join(fmt!("{}.pdf", name));
88 res!(std::fs::write(&path, pdf));
89 let out = res!(Command::new("pdfinfo").arg(&path).output());
90 let text = res!(String::from_utf8(out.stdout));
91 for line in text.lines() {
92 if let Some(rest) = line.strip_prefix("Pages:") {
93 return Ok(res!(rest.trim().parse::<usize>()));
94 }
95 }
96 Err(err!("pdfinfo printed no Pages line for {:?}: {}", path, text; Test))
97}
98
99fn prose(paras: usize) -> String {
100 let mut s = String::from("= Report\n\n");
101 for i in 0..paras {
102 s.push_str(&fmt!("Paragraph {} sets enough ordinary words to fill a few lines of the measure, so \
103 that forty of them spill well past a single A4 page of body text.\n\n", i));
104 }
105 s
106}
107
108#[test]
109fn reported_pages_match_the_pdf_and_a_clean_compile_passes_strict() -> Outcome<()> {
110 let _turn = turn();
111 let src = prose(40);
112 let (report, pdf) = res!(done(res!(compile_pdf(&[(MAIN, src.as_bytes())]))));
113 let external = res!(pdfinfo_pages(&pdf, "wasm_api_pages"));
114 assert!(report.pages > 1, "the fixture must paginate, found {} page(s)", report.pages);
115 assert_eq!(report.pages, external, "the reported page count must be the PDF's own");
116 assert!(report.diagnostics.is_empty(), "a clean compile has no diagnostics: {:?}", report.diagnostics);
117 assert_eq!(report.skipped, None);
118 assert_eq!(report.strict_failure(Path::new(MAIN)), None, "a clean compile passes strict");
119 Ok(())
120}
121
122#[test]
123fn a_skipped_construct_is_positioned_and_refused_under_strict() -> Outcome<()> {
124 let _turn = turn();
125 // `#columns` opens line 5.
126 let src = "= H\n\nBody.\n\n#columns(2)[a]\n\nMore body.\n";
127 let (report, pdf) = res!(done(res!(compile_pdf(&[(MAIN, src.as_bytes())]))));
128 assert!(pdf.starts_with(b"%PDF-"), "a non-strict compile still produces its PDF");
129 assert_eq!(report.diagnostics.len(), 1, "one refused site: {:?}", report.diagnostics);
130 let d = &report.diagnostics[0];
131 assert_eq!((d.file.as_str(), d.line, d.col), (MAIN, 5, 1), "the site's real position: {}", d);
132 assert!(d.message.contains("#columns"), "the message names the construct: {}", d);
133 assert_eq!(report.skipped.as_deref(), Some("skipped: #columns ×1"));
134
135 let head = match report.strict_failure(Path::new(MAIN)) {
136 Some(h) => h,
137 None => return Err(err!("strict must refuse a compile that skipped a construct"; Test)),
138 };
139 assert_eq!((head.line, head.col), (5, 1), "strict reports at the first site: {}", head);
140 assert!(fmt!("{}", head).starts_with("/proj/main.typ:5:1: strict:"), "the error line: {}", head);
141 Ok(())
142}
143
144#[test]
145fn a_failed_import_is_a_diagnostic_and_a_strict_error() -> Outcome<()> {
146 let _turn = turn();
147 let src = "= H\n\n#import \"template.typ\": *\n\nBody.\n";
148 let (report, _) = res!(done(res!(compile_pdf(&[(MAIN, src.as_bytes())]))));
149 let first = match report.diagnostics.first() {
150 Some(d) => d.clone(),
151 None => return Err(err!("an unresolved #import must be reported"; Test)),
152 };
153 assert_eq!((first.line, first.col), (3, 1), "{}", first);
154 assert!(first.message.contains("#import"), "{}", first);
155 assert!(report.strict_failure(Path::new(MAIN)).is_some());
156 Ok(())
157}
158
159#[test]
160fn strict_refuses_an_empty_source_and_zero_pages() -> Outcome<()> {
161 let _turn = turn();
162 let (report, _) = res!(done(res!(compile_pdf(&[(MAIN, b"")]))));
163 assert!(report.empty, "an empty main reads no content block");
164 assert!(report.diagnostics.is_empty(), "and refuses nothing, so only the emptiness catches it");
165 let head = match report.strict_failure(Path::new(MAIN)) {
166 Some(h) => h,
167 None => return Err(err!("strict must refuse a source that sets nothing"; Test)),
168 };
169 assert_eq!(fmt!("{}", head), "/proj/main.typ:1:1: strict: the source sets no content.");
170
171 let none = Report { pages: 0, diagnostics: Vec::new(), skipped: None, empty: false };
172 let head = match none.strict_failure(Path::new(MAIN)) {
173 Some(h) => h,
174 None => return Err(err!("strict must refuse zero pages"; Test)),
175 };
176 assert!(head.message.contains("no pages"), "{}", head);
177 Ok(())
178}
179
180#[test]
181fn a_missing_include_is_placed_at_the_line_that_cites_it() -> Outcome<()> {
182 let _turn = turn();
183 // Cited from the root.
184 let root = "= H\n\nBody.\n#include \"ch1.typ\"\n";
185 let d = res!(failed(res!(compile_pdf(&[(MAIN, root.as_bytes())]))));
186 assert_eq!((d.file.as_str(), d.line, d.col), (MAIN, 4, 10), "{}", d);
187 assert!(d.message.contains("ch1.typ") && !d.message.contains(".rs:"), "plain words: {}", d);
188
189 // Cited from a chapter one directory down, resolved against the chapter's own directory.
190 let root = "= H\n\n#include \"parts/a.typ\"\n";
191 let chap = "== A\n\nText.\n\n#include \"../gone.typ\"\n";
192 let d = res!(failed(res!(compile_pdf(&[(MAIN, root.as_bytes()), ("/proj/parts/a.typ", chap.as_bytes())]))));
193 assert_eq!((d.file.as_str(), d.line, d.col), ("/proj/parts/a.typ", 5, 10), "{}", d);
194 Ok(())
195}
196
197#[test]
198fn an_error_citing_no_source_path_reports_zero_zero() -> Outcome<()> {
199 let e = err!("Something failed with no path."; Test);
200 let d = compile::locate_error(&e, Path::new(MAIN), &[]);
201 assert_eq!(fmt!("{}", d), "/proj/main.typ:0:0: Something failed with no path.");
202 Ok(())
203}
204
205/// The family name `fc-scan` reads from a font file's own name table.
206fn fc_family(path: &Path) -> Outcome<String> {
207 let out = res!(Command::new("fc-scan").arg("--format").arg("%{family[0]}").arg(path).output());
208 Ok(res!(String::from_utf8(out.stdout)).trim().to_string())
209}
210
211#[test]
212fn embedded_families_are_the_names_in_the_font_files() -> Outcome<()> {
213 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fonts");
214 let mut read = Vec::new();
215 for f in ["LibertinusSerif-Regular.otf", "LibertinusMono-Regular.otf", "NewCMMath-Regular.otf"] {
216 read.push(res!(fc_family(&dir.join(f))));
217 }
218 let mut listed: Vec<String> = compile::EMBEDDED_FAMILIES.iter().map(|s| s.to_string()).collect();
219 read.sort();
220 listed.sort();
221 // Same-family rather than a literal match: the New Computer Modern files declare `NewComputerModern
222 // Math` in their own name table, where the embedded list and every document write it `New Computer
223 // Modern Math` (see `fonts::same_family`'s own doc comment for why).
224 assert_eq!(listed.len(), read.len(), "the embedded list must name exactly the files' own families");
225 for (l, r) in listed.iter().zip(read.iter()) {
226 assert!(fonts::same_family(l, r), "{:?} is not the family {:?} declares", l, r);
227 }
228 Ok(())
229}
230
231#[test]
232fn injected_families_are_listed_only_when_the_resolver_loads_them() -> Outcome<()> {
233 let _turn = turn();
234 let face = res!(std::fs::read(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fonts").join("LibertinusSerif-Regular.otf")));
235 let main = PathBuf::from(MAIN);
236 let given = [
237 PathBuf::from("/fonts/Testface-Regular.otf"), // a usable face under an arbitrary path
238 PathBuf::from("Otherface-Bold.ttf"), // a bare basename, bold only
239 PathBuf::from("/fonts/Broken-Regular.ttf"), // will not parse
240 PathBuf::from("/fonts/nameless.otf"), // no <Family>-<Variant> name
241 ];
242 let mut map: HashMap<PathBuf, Vec<u8>> = HashMap::new();
243 map.insert(main.clone(), b"= H\n".to_vec());
244 for g in &given {
245 let bytes = if g.to_string_lossy().contains("Broken") { b"not a font".to_vec() } else { face.clone() };
246 // Installed as the wasm surface installs a font: at the given path and at the resolver's path.
247 if let Some(routed) = oxedyne_fe2o3_austenite::book::project_font_path(&main, g) {
248 map.insert(routed, bytes.clone());
249 }
250 map.insert(g.clone(), bytes);
251 }
252 let bare = compile::font_families(&main, &[]);
253 res!(vfs::install(map));
254 let families = compile::font_families(&main, &given);
255 res!(vfs::clear());
256
257 let mut want: Vec<String> = compile::EMBEDDED_FAMILIES.iter().map(|s| s.to_string()).collect();
258 want.sort();
259 assert_eq!(bare, want, "with nothing injected only the embedded families are listed");
260 want.push("Otherface".to_string());
261 want.push("Testface".to_string());
262 want.sort();
263 assert_eq!(families, want);
264 Ok(())
265}
266
267#[test]
268fn engine_identity_is_the_crate_version_and_the_built_commit() -> Outcome<()> {
269 let manifest = res!(std::fs::read_to_string(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml")));
270 let version = manifest.lines()
271 .find_map(|l| l.strip_prefix("version = \"").and_then(|r| r.strip_suffix('"')));
272 assert_eq!(Some(compile::engine_version()), version);
273
274 let out = res!(Command::new("git").args(["rev-parse", "--short=12", "HEAD"])
275 .current_dir(env!("CARGO_MANIFEST_DIR")).output());
276 let head = res!(String::from_utf8(out.stdout)).trim().to_string();
277 let hash = compile::engine_git_hash();
278 assert_eq!(head.len(), 12, "git must name the commit: {:?}", head);
279 assert!(hash == head || hash == fmt!("{}-dirty", head), "built from {:?}, HEAD is {:?}", hash, head);
280 Ok(())
281}