Files
curriculum-project-hub/crates/cph-schema/tests/validate.rs
T
hongjr03 5866b3d6cc feat(cli): cph init / cph add 子命令(工程脚手架)
init 生成可 check 的工程根(manifest.toml + .cph-version + 默认 exports/student.typ + 空 kind 目录);
add 按 kind 建 part 目录/element.toml/必填内容文件并追加 [[parts]]。均纯本地,不涉及 hub 语义;
Engine 改惰性构造。cph-schema 新增 required_content_field_names() 作为 add 建文件合同。
2026-08-05 21:22:26 +08:00

99 lines
3.3 KiB
Rust

//! Integration tests for `cph-schema::validate`, driven by tiny fixtures under
//! `tests/fixtures/<case>/` and in-code `ElementDescriptor`s.
use std::path::{Path, PathBuf};
use cph_diag::DiagCode;
use cph_model::ElementDescriptor;
use cph_schema::validate;
/// Absolute path to a fixture directory.
fn fixture(case: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join(case)
}
fn desc(kind: &str, dir: PathBuf, scalars: toml::Table) -> ElementDescriptor {
ElementDescriptor {
kind: kind.to_string(),
dir,
scalars,
}
}
#[test]
fn valid_example_no_diagnostics() {
let mut scalars = toml::Table::new();
scalars.insert("source".into(), toml::Value::String("Textbook §3".into()));
let d = desc("example", fixture("example_valid"), scalars);
let diags = validate(&d);
assert!(diags.is_empty(), "expected no diagnostics, got {diags:?}");
}
#[test]
fn example_missing_solution_typ_is_missing_content_file() {
let d = desc(
"example",
fixture("example_missing_solution"),
toml::Table::new(),
);
let diags = validate(&d);
assert_eq!(diags.len(), 1, "got {diags:?}");
assert_eq!(diags[0].code, DiagCode::MissingContentFile);
assert!(diags[0].message.contains("solution.typ"));
assert!(diags[0].hint.is_some());
}
#[test]
fn lemma_without_proof_typ_is_ok() {
// stmt.typ exists, proof.typ does not — proof is optional, so no diagnostic.
let d = desc("lemma", fixture("lemma_no_proof"), toml::Table::new());
let diags = validate(&d);
assert!(diags.is_empty(), "expected no diagnostics, got {diags:?}");
}
#[test]
fn unknown_kind_is_reported() {
let d = desc("frob", fixture("example_valid"), toml::Table::new());
let diags = validate(&d);
assert_eq!(diags.len(), 1, "got {diags:?}");
assert_eq!(diags[0].code, DiagCode::UnknownKind);
assert!(diags[0].message.contains("frob"));
}
#[test]
fn example_source_non_string_is_schema_violation() {
let mut scalars = toml::Table::new();
scalars.insert("source".into(), toml::Value::Integer(42));
let d = desc("example", fixture("example_valid"), scalars);
let diags = validate(&d);
assert_eq!(diags.len(), 1, "got {diags:?}");
assert_eq!(diags[0].code, DiagCode::SchemaViolation);
assert!(diags[0].message.contains("source"));
assert!(diags[0].message.contains("string"));
}
/// `required_content_field_names` is the `cph-cli add` authoring contract: the
/// set of sibling `.typ` files a new part of a kind must have to be
/// schema-legal (ADR-0008). It must be the schema `required` content fields —
/// not the optional ones (e.g. a lemma's `proof`) and not the scalar fields.
#[test]
fn required_content_field_names_match_schema_required() {
let case = |kind: &str, expected: Vec<&str>| {
let mut got: Vec<&str> = cph_schema::schema_for(kind)
.unwrap()
.required_content_field_names();
got.sort_unstable();
let mut want = expected;
want.sort_unstable();
assert_eq!(got, want, "required content fields for '{kind}'");
};
case("segment", vec!["textbook"]);
case("lemma", vec!["stmt"]);
case("example", vec!["problem", "solution"]);
case("sop", vec!["sop"]);
}