Skip to main content

max / ripgrow

pdf: end-to-end test + relative data path Typst confines file reads to the template's root and treats paths passed via --input as relative to it. Passing an absolute data-path made it prepend the scratch dir twice and fail with "file not found". Fix: hand the template a bare filename since the data and template live in the same scratch dir. Added an end-to-end test that actually runs the typst subprocess against a sample SessionExport and asserts the output has the %PDF- header. Skips when typst is not on PATH so builds without it still pass. 84 tests total (49 core + 35 tui), all passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-18 17:09 UTC
Commit: e21648c73a4358f88b0ed17174ccf81c61b3eae6
Parent: d1c2a57
1 file changed, +59 insertions, -11 deletions
@@ -59,10 +59,13 @@ pub fn export_to_pdf(
59 59 fs::write(&data_path, render_json(session))?;
60 60 fs::write(&template_path, TEMPLATE)?;
61 61
62 + // Typst confines file reads to the template's root and treats any path
63 + // passed via --input as relative to it, so hand the template a bare
64 + // filename; both live in the same scratch dir.
62 65 let status = Command::new("typst")
63 66 .arg("compile")
64 67 .arg("--input")
65 - .arg(format!("data-path={}", data_path.display()))
68 + .arg("data-path=data.json")
66 69 .arg(&template_path)
67 70 .arg(&output_path)
68 71 .output();
@@ -291,18 +294,63 @@ mod tests {
291 294 assert!(out.contains(r#"back\\slash"#));
292 295 }
293 296
297 + fn typst_available() -> bool {
298 + std::process::Command::new("typst")
299 + .arg("--version")
300 + .output()
301 + .map(|o| o.status.success())
302 + .unwrap_or(false)
303 + }
304 +
305 + #[test]
306 + fn export_produces_pdf_end_to_end() {
307 + if !typst_available() {
308 + eprintln!("skipping: typst not on PATH");
309 + return;
310 + }
311 + let s = SessionExport {
312 + profile: "self".into(),
313 + date: NaiveDate::from_ymd_opt(2026, 7, 18).unwrap(),
314 + warmup: vec!["chest".into(), "shoulders".into(), "core".into()],
315 + slots: vec![
316 + SlotExport {
317 + name: "bench".into(),
318 + prescription: "3 x 5 @ 82.5 kg".into(),
319 + glyph: "up".into(),
320 + },
321 + SlotExport {
322 + name: "pullup".into(),
323 + prescription: "3 x 8".into(),
324 + glyph: "flat".into(),
325 + },
326 + SlotExport {
327 + name: "row \"paused\"".into(),
328 + prescription: "3 x 6 @ 60 kg".into(),
329 + glyph: "".into(),
330 + },
331 + ],
332 + };
333 + let dir = std::env::temp_dir().join(format!(
334 + "ripgrow-pdf-e2e-{}",
335 + std::time::SystemTime::now()
336 + .duration_since(std::time::UNIX_EPOCH)
337 + .unwrap()
338 + .as_nanos()
339 + ));
340 + std::fs::create_dir_all(&dir).unwrap();
341 + let out = export_to_pdf(&s, &dir).unwrap();
342 + assert!(out.exists(), "output PDF should exist at {}", out.display());
343 + let bytes = std::fs::read(&out).unwrap();
344 + assert!(bytes.starts_with(b"%PDF-"), "output should be a valid PDF");
345 + let _ = std::fs::remove_dir_all(&dir);
346 + }
347 +
294 348 #[test]
295 349 fn export_returns_typst_missing_when_absent() {
296 - // typst is not installed in test env; ensure we surface a clear
297 - // error rather than a raw io::Error. If typst IS installed, this
298 - // test correctly fails — remove the guard or install per-CI norms.
299 - if std::process::Command::new("which")
300 - .arg("typst")
301 - .output()
302 - .ok()
303 - .and_then(|o| o.status.success().then_some(()))
304 - .is_some()
305 - {
350 + // Guards against a regression in the missing-binary error path.
351 + // Skips when typst is present; the end-to-end test covers the
352 + // happy path.
353 + if typst_available() {
306 354 eprintln!("skipping: typst is on PATH in this environment");
307 355 return;
308 356 }