forked from bai/curriculum-project-hub
feat(checker): cph-check orchestrator + cph-cli cph binary (WU-5)
Closes the core pipeline: the `cph` CLI checks an engineering file and compiles a PDF. cph-check — phases (a) load → (b) structural (known-kind set) → (c) schema → (d) typst compile → (e) render-coverage. Compile is gated: skipped if (a)-(c) produced any Error (broken input would only add noise); `check` compiles every declared target (dedup identical diags), `build` compiles the one requested. Render-coverage emits a non-blocking RenderIgnored *warning* when a (kind, target) has no render rule — severity pinned via RENDER_IGNORED_SEVERITY const citing spec Diagnostic.renderIgnoredSeverity + ADR-0005. No double-emit: unknown/missing parts are skipped downstream. API: check(root, engine) -> CheckReport, build(root, engine, target) -> (Option<pdf>, CheckReport). cph-cli — clap: `cph check <path>` (exit 1 on any Error, warnings → 0) and `cph build <path> --target <name> [-o out]` (default target student, default out <path>/build/<target>.pdf). Diagnostics → stderr via Display, results → stdout. --render-dir override. End-to-end on examples/TH-141 (39 parts): `check` → 0 errors/0 warnings; `build` → real PDF student 9pp/395KB, teacher 12pp/474KB. Broken-element check (unclosed delimiter in a stmt.typ) → span-accurate `error[E-TYPST-COMPILE]: unclosed delimiter --> lemmas/…/stmt.typ:7:8`, exit 1. Workspace: fmt + clippy -D warnings + test all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
//! Integration tests for the `cph-check` orchestrator.
|
||||
//!
|
||||
//! These exercise phase gating and the public API against the shared `mini`
|
||||
//! fixture (`crates/cph-typst/tests/fixtures/mini/`) and small throwaway
|
||||
//! fixtures built in a temp dir. The typst Engine is pointed at the repo's real
|
||||
//! `render/` package via `Engine::with_render_dir`.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cph_diag::DiagCode;
|
||||
use cph_typst::Engine;
|
||||
|
||||
/// `<this crate>/../cph-typst/tests/fixtures/mini` — the known-good lesson.
|
||||
fn mini_fixture() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("cph-typst")
|
||||
.join("tests")
|
||||
.join("fixtures")
|
||||
.join("mini")
|
||||
}
|
||||
|
||||
/// `<this crate>/../../render` — the repo render package.
|
||||
fn render_dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("render")
|
||||
}
|
||||
|
||||
fn engine() -> Engine {
|
||||
Engine::with_render_dir(render_dir())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn good_fixture_has_no_errors() {
|
||||
let report = cph_check::check(&mini_fixture(), &engine());
|
||||
assert!(report.lesson_loaded, "mini fixture should load");
|
||||
assert_eq!(
|
||||
report.error_count(),
|
||||
0,
|
||||
"mini fixture should have zero errors, got: {:#?}",
|
||||
report.diagnostics
|
||||
);
|
||||
assert!(!report.has_errors());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_kind_is_an_error() {
|
||||
// Build a throwaway lesson whose part declares kind "frob".
|
||||
let tmp = tempdir();
|
||||
std::fs::write(
|
||||
tmp.join("manifest.toml"),
|
||||
r#"
|
||||
[project]
|
||||
id = "broken"
|
||||
name = "broken"
|
||||
|
||||
[info]
|
||||
title = "broken"
|
||||
|
||||
[[parts]]
|
||||
kind = "frob"
|
||||
path = "elements/widget"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let part_dir = tmp.join("elements").join("widget");
|
||||
std::fs::create_dir_all(&part_dir).unwrap();
|
||||
std::fs::write(part_dir.join("element.toml"), "kind = \"frob\"\n").unwrap();
|
||||
|
||||
let report = cph_check::check(&tmp, &engine());
|
||||
assert!(report.lesson_loaded);
|
||||
assert!(report.has_errors());
|
||||
assert!(
|
||||
report
|
||||
.diagnostics
|
||||
.iter()
|
||||
.any(|d| d.code == DiagCode::UnknownKind && d.message.contains("frob")),
|
||||
"expected an UnknownKind error mentioning 'frob', got: {:#?}",
|
||||
report.diagnostics
|
||||
);
|
||||
cleanup(&tmp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hard_manifest_failure_sets_lesson_not_loaded() {
|
||||
let tmp = tempdir();
|
||||
// No manifest.toml at all → cph_model::load returns None.
|
||||
let report = cph_check::check(&tmp, &engine());
|
||||
assert!(!report.lesson_loaded, "missing manifest is a hard failure");
|
||||
assert!(report.has_errors());
|
||||
cleanup(&tmp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_returns_pdf_on_good_fixture() {
|
||||
let (pdf, report) = cph_check::build(&mini_fixture(), &engine(), "student");
|
||||
assert!(report.lesson_loaded);
|
||||
assert_eq!(
|
||||
report.error_count(),
|
||||
0,
|
||||
"no check errors expected, got: {:#?}",
|
||||
report.diagnostics
|
||||
);
|
||||
let pdf = pdf.expect("expected PDF bytes for the good fixture");
|
||||
assert!(pdf.starts_with(b"%PDF"), "output should be a PDF");
|
||||
assert!(pdf.len() > 1000, "PDF should be non-trivial");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_returns_none_on_broken_fixture() {
|
||||
// A part folder is missing → cph-model flags PartPathMissing (an error),
|
||||
// so build refuses before ever compiling.
|
||||
let tmp = tempdir();
|
||||
std::fs::write(
|
||||
tmp.join("manifest.toml"),
|
||||
r#"
|
||||
[project]
|
||||
id = "broken"
|
||||
name = "broken"
|
||||
|
||||
[info]
|
||||
title = "broken"
|
||||
|
||||
[[parts]]
|
||||
kind = "segment"
|
||||
path = "segments/does-not-exist"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (pdf, report) = cph_check::build(&tmp, &engine(), "student");
|
||||
assert!(pdf.is_none(), "broken lesson must not produce a PDF");
|
||||
assert!(report.has_errors());
|
||||
cleanup(&tmp);
|
||||
}
|
||||
|
||||
// --- tiny temp-dir helpers (no external dev-dep) ------------------------------
|
||||
|
||||
fn tempdir() -> PathBuf {
|
||||
let mut p = std::env::temp_dir();
|
||||
let nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
p.push(format!(
|
||||
"cph-check-test-{nanos}-{:?}",
|
||||
std::thread::current().id()
|
||||
));
|
||||
std::fs::create_dir_all(&p).unwrap();
|
||||
p
|
||||
}
|
||||
|
||||
fn cleanup(p: &PathBuf) {
|
||||
let _ = std::fs::remove_dir_all(p);
|
||||
}
|
||||
Reference in New Issue
Block a user