Files
curriculum-project-hub/crates/cph-typst/tests/bundle.rs
T
ecc92c9a87 docs(adr): cph 的 nested-manifest / batch-export 改号 0036/0037
上游 0029/0030 与本地 filelib 侧同号 ADR 相撞,cph 两份改到本地空闲号段,
代码锚点引用一并跟随。
2026-08-06 00:51:33 +08:00

123 lines
4.7 KiB
Rust

//! Integration tests for the bundle build path (ADR-0037): compiling a bundle
//! target's template (`exports/<target>.typ` under a `bundle.toml` root) as
//! main, injecting the augmented **bundle** manifest, against the real
//! `render/` package.
use std::path::PathBuf;
use cph_diag::Severity;
use cph_typst::{build_augmented_bundle_manifest, Engine};
fn fixture_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/bundle")
}
fn real_render_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("render")
}
fn load_bundle() -> cph_model::Bundle {
let (bundle, diags) = cph_model::load_bundle(&fixture_root());
let bundle = bundle.expect("bundle fixture loads into a Bundle");
let errors: Vec<_> = diags
.iter()
.filter(|d| d.severity == Severity::Error)
.collect();
assert!(errors.is_empty(), "fixture has loader errors: {errors:?}");
bundle
}
/// PURE UNIT TEST (no fonts, no render package): the augmented bundle manifest
/// carries the bundle's own `[info]` and an ordered `[[lessons]]`, each with
/// that member's own `info`/`target` and a `path`-prefixed outline (ADR-0037).
#[test]
fn augmented_bundle_manifest_prefixes_member_paths() {
let bundle = load_bundle();
let src = build_augmented_bundle_manifest(&bundle);
assert!(
src.contains("测试合集"),
"bundle info.title present:\n{src}"
);
let doc: toml::Value = toml::from_str(&src).expect("augmented bundle manifest is valid TOML");
let lessons = doc
.get("lessons")
.and_then(|l| l.as_array())
.expect("lessons array present");
assert_eq!(lessons.len(), 2, "two member lessons:\n{src}");
// lesson-a: target defaults to its own first declared target ("student"),
// outline paths are prefixed with "lesson-a/".
let a_target = lessons[0].get("target").unwrap().as_str().unwrap();
assert_eq!(a_target, "student");
let a_outline = lessons[0].get("outline").unwrap().as_array().unwrap();
// segment + section heading + lemma = 3 outline entries.
assert_eq!(a_outline.len(), 3);
let a_seg_path = a_outline[0].get("path").unwrap().as_str().unwrap();
assert_eq!(a_seg_path, "lesson-a/segments/a");
let a_section_path = a_outline[1].get("path").unwrap().as_str().unwrap();
assert_eq!(a_section_path, "lesson-a/小节");
let a_lemma_path = a_outline[2].get("path").unwrap().as_str().unwrap();
assert_eq!(a_lemma_path, "lesson-a/小节/lemmas/引理甲");
// lesson-b: bundle.toml overrides `target = "teacher"`.
let b_target = lessons[1].get("target").unwrap().as_str().unwrap();
assert_eq!(b_target, "teacher");
let b_outline = lessons[1].get("outline").unwrap().as_array().unwrap();
assert_eq!(b_outline.len(), 1);
let b_seg_path = b_outline[0].get("path").unwrap().as_str().unwrap();
assert_eq!(b_seg_path, "lesson-b/segments/b");
}
/// THROUGH-TEMPLATE compile-check against the REAL render package: compiling
/// the bundle's `merged` target as main with the injected augmented bundle
/// manifest is clean.
#[test]
fn compile_check_clean_through_bundle_template() {
let bundle = load_bundle();
let engine = Engine::with_render_dir(real_render_dir());
let diags = engine.compile_check_bundle(&bundle, "merged");
let errors: Vec<_> = diags
.iter()
.filter(|d| d.severity == Severity::Error)
.collect();
assert!(errors.is_empty(), "unexpected compile errors: {errors:#?}");
}
/// THROUGH-TEMPLATE PDF export, fully offline: a non-trivial combined PDF is
/// produced from the two member lessons through the real bundle template.
#[test]
fn build_bundle_pdf_through_template_offline() {
let bundle = load_bundle();
let engine = Engine::with_render_dir(real_render_dir());
let pdf = engine
.build_bundle_pdf(&bundle, "merged")
.unwrap_or_else(|d| panic!("bundle PDF build failed: {d:#?}"));
assert!(pdf.starts_with(b"%PDF"), "output is a PDF");
assert!(
pdf.len() > 1024,
"bundle PDF is non-trivial (got {} bytes)",
pdf.len()
);
}
/// An undeclared bundle target name is a blocking `SchemaViolation`, exactly
/// like a lesson's own unknown-target path.
#[test]
fn unknown_bundle_target_is_blocking() {
let bundle = load_bundle();
let engine = Engine::with_render_dir(real_render_dir());
let diags = engine.compile_check_bundle(&bundle, "nonexistent");
assert_eq!(diags.len(), 1, "one blocking diagnostic: {diags:#?}");
assert_eq!(diags[0].severity, Severity::Error);
assert!(
diags[0].message.contains("not declared"),
"expected an undeclared-target error: {diags:#?}"
);
}