chore: restore checker CI gate

This commit is contained in:
2026-07-10 13:46:22 +08:00
parent 420c1c40bc
commit 5f791c5ce4
12 changed files with 147 additions and 44 deletions
+29 -7
View File
@@ -360,8 +360,13 @@ pub fn target_is_shell(root: &Path, target: &str) -> bool {
.iter()
.find(|t| t.name == target)
.is_some_and(|t| {
t.steps.iter().any(|s| matches!(s, cph_model::Step::Shell { .. }))
&& !t.steps.iter().any(|s| matches!(s, cph_model::Step::TypstCompile { .. }))
t.steps
.iter()
.any(|s| matches!(s, cph_model::Step::Shell { .. }))
&& !t
.steps
.iter()
.any(|s| matches!(s, cph_model::Step::TypstCompile { .. }))
})
}
@@ -370,9 +375,17 @@ fn run_one_shell(cwd: &Path, run: &str) -> ShellStepOutcome {
use std::process::Command;
let output = if cfg!(target_os = "windows") {
Command::new("cmd").arg("/C").arg(run).current_dir(cwd).output()
Command::new("cmd")
.arg("/C")
.arg(run)
.current_dir(cwd)
.output()
} else {
Command::new("sh").arg("-c").arg(run).current_dir(cwd).output()
Command::new("sh")
.arg("-c")
.arg(run)
.current_dir(cwd)
.output()
};
match output {
@@ -704,7 +717,11 @@ fn collect_image_assets(eng_root: &Path, build_root: &Path, body: &str) -> Resul
}
}
if let Err(e) = std::fs::copy(&src, &dst) {
return Err(format!("failed to copy '{}' → '{}': {e}", src.display(), dst.display()));
return Err(format!(
"failed to copy '{}' → '{}': {e}",
src.display(),
dst.display()
));
}
}
Ok(())
@@ -763,7 +780,8 @@ pub fn target_is_markdown_assemble(root: &Path, target: &str) -> bool {
t.steps
.iter()
.any(|s| matches!(s, cph_model::Step::AssembleMarkdown { .. }))
&& !t.steps
&& !t
.steps
.iter()
.any(|s| matches!(s, cph_model::Step::TypstCompile { .. }))
})
@@ -833,7 +851,11 @@ fn compile_targets(lesson: &cph_model::Lesson) -> Vec<&str> {
lesson
.targets
.iter()
.filter(|t| t.steps.iter().any(|s| matches!(s, cph_model::Step::TypstCompile { .. })))
.filter(|t| {
t.steps
.iter()
.any(|s| matches!(s, cph_model::Step::TypstCompile { .. }))
})
.map(|t| t.name.as_str())
.collect()
}
+14 -11
View File
@@ -5,7 +5,7 @@
//! 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 std::path::{Path, PathBuf};
use cph_diag::DiagCode;
use cph_typst::Engine;
@@ -138,7 +138,7 @@ path = "segments/does-not-exist"
/// Write a throwaway one-segment lesson into `tmp`, with the given `[targets.x]`
/// body spliced in. Returns nothing; the caller runs `check`.
fn write_segment_lesson_with_target(tmp: &PathBuf, target_body: &str) {
fn write_segment_lesson_with_target(tmp: &Path, target_body: &str) {
std::fs::write(
tmp.join("manifest.toml"),
format!(
@@ -264,7 +264,7 @@ template = "exports/handout.typ"
/// Write a one-segment lesson with a `file-tree` shell target whose `run` is the
/// given command. Returns nothing; the caller runs `run_shell_target`.
fn write_shell_target_lesson(tmp: &PathBuf, run: &str) {
fn write_shell_target_lesson(tmp: &Path, run: &str) {
std::fs::write(
tmp.join("manifest.toml"),
format!(
@@ -365,7 +365,10 @@ run = "printf SHOULD_NOT_RUN > build/ran.txt"
let report = cph_check::run_shell_target(&tmp, &engine(), "assets");
assert!(!report.ok);
assert!(report.outcomes.is_empty(), "no command should run on a broken lesson");
assert!(
report.outcomes.is_empty(),
"no command should run on a broken lesson"
);
assert!(report.check.has_errors());
assert!(
!tmp.join("build").join("ran.txt").exists(),
@@ -380,7 +383,7 @@ run = "printf SHOULD_NOT_RUN > build/ran.txt"
/// content file, plus a `slides` target that assembles them. The `which` slice
/// controls which segments get a `slides.md` (so the "skip absent" path can be
/// exercised). Returns nothing; the caller runs `run_markdown_assemble_target`.
fn write_markdown_assemble_target_lesson(tmp: &PathBuf, slides: &[(&str, &str)]) {
fn write_markdown_assemble_target_lesson(tmp: &Path, slides: &[(&str, &str)]) {
let mut parts = String::new();
for (i, (name, _)) in slides.iter().enumerate() {
if i > 0 {
@@ -424,10 +427,7 @@ field = "slides"
#[test]
fn target_is_markdown_assemble_detects_shape() {
let tmp = tempdir();
write_markdown_assemble_target_lesson(
&tmp,
&[("a", "# A"), ("b", "# B")],
);
write_markdown_assemble_target_lesson(&tmp, &[("a", "# A"), ("b", "# B")]);
assert!(cph_check::target_is_markdown_assemble(&tmp, "slides"));
// The student target doesn't exist here → falls through to typst path → false.
assert!(!cph_check::target_is_markdown_assemble(&tmp, "student"));
@@ -439,7 +439,10 @@ fn run_markdown_assemble_target_concatenates_in_parts_order() {
let tmp = tempdir();
write_markdown_assemble_target_lesson(
&tmp,
&[("intro", "# 规则一\n- 范围"), ("rule2", "# 规则二\n$8\\times$")],
&[
("intro", "# 规则一\n- 范围"),
("rule2", "# 规则二\n$8\\times$"),
],
);
let report = cph_check::run_markdown_assemble_target(&tmp, &engine(), "slides");
@@ -667,6 +670,6 @@ fn tempdir() -> PathBuf {
p
}
fn cleanup(p: &PathBuf) {
fn cleanup(p: &Path) {
let _ = std::fs::remove_dir_all(p);
}
+41 -16
View File
@@ -51,12 +51,12 @@ enum Command {
/// completion file, e.g. `cph completions zsh > ~/.zfunc/_cph`.
Completions {
/// Which shell to generate completions for.
shell: Shell,
shell: CompletionTarget,
},
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
enum Shell {
enum CompletionTarget {
Bash,
Zsh,
Fish,
@@ -82,16 +82,16 @@ fn main() -> ExitCode {
/// Emit a shell-completion script for `shell` to stdout. The script is built
/// from the same `Cli` clap definition above, so it tracks subcommands/flags as
/// they evolve.
fn run_completions(shell: Shell) -> ExitCode {
fn run_completions(shell: CompletionTarget) -> ExitCode {
use clap::CommandFactory;
use clap_complete::Shell as CompShell;
let sh = match shell {
Shell::Bash => CompShell::Bash,
Shell::Zsh => CompShell::Zsh,
Shell::Fish => CompShell::Fish,
Shell::PowerShell => CompShell::PowerShell,
Shell::Elvish => CompShell::Elvish,
CompletionTarget::Bash => CompShell::Bash,
CompletionTarget::Zsh => CompShell::Zsh,
CompletionTarget::Fish => CompShell::Fish,
CompletionTarget::PowerShell => CompShell::PowerShell,
CompletionTarget::Elvish => CompShell::Elvish,
};
let mut cmd = Cli::command();
clap_complete::generate(sh, &mut cmd, "cph", &mut std::io::stdout());
@@ -188,7 +188,10 @@ fn run_build(
/// explicit `cph build --target <name>`, and each command is printed before it
/// runs so the user sees exactly what is executed.
fn run_shell_build(path: &std::path::Path, engine: &Engine, target: &str) -> ExitCode {
eprintln!("running shell-step target '{target}' (commands execute in {})", path.display());
eprintln!(
"running shell-step target '{target}' (commands execute in {})",
path.display()
);
let report = cph_check::run_shell_target(path, engine, target);
print_diagnostics(&report.check);
@@ -196,7 +199,10 @@ fn run_shell_build(path: &std::path::Path, engine: &Engine, target: &str) -> Exi
// Refused before running anything (check errors / unknown target / no
// shell steps): the diagnostics above explain why.
if report.check.has_errors() {
eprintln!("build refused: {} errors (fix the lesson first)", report.check.error_count());
eprintln!(
"build refused: {} errors (fix the lesson first)",
report.check.error_count()
);
} else {
eprintln!("build failed: target '{target}' has no shell steps to run");
}
@@ -212,13 +218,19 @@ fn run_shell_build(path: &std::path::Path, engine: &Engine, target: &str) -> Exi
eprint!("{}", outcome.stderr);
eprintln!(
"step failed (exit {})",
outcome.status.map(|c| c.to_string()).unwrap_or_else(|| "signal".into())
outcome
.status
.map(|c| c.to_string())
.unwrap_or_else(|| "signal".into())
);
}
}
if report.ok {
println!("shell-step target '{target}' completed: {} step(s) ok", report.outcomes.len());
println!(
"shell-step target '{target}' completed: {} step(s) ok",
report.outcomes.len()
);
ExitCode::SUCCESS
} else {
eprintln!("shell-step target '{target}' failed");
@@ -231,13 +243,19 @@ fn run_shell_build(path: &std::path::Path, engine: &Engine, target: &str) -> Exi
/// shell path, this is opt-in — only on an explicit `cph build --target <name>`,
/// and never in `check`.
fn run_markdown_assemble_build(path: &std::path::Path, engine: &Engine, target: &str) -> ExitCode {
eprintln!("assembling markdown target '{target}' (reading parts under {})", path.display());
eprintln!(
"assembling markdown target '{target}' (reading parts under {})",
path.display()
);
let report = cph_check::run_markdown_assemble_target(path, engine, target);
print_diagnostics(&report.check);
if report.outcomes.is_empty() && !report.ok {
if report.check.has_errors() {
eprintln!("build refused: {} errors (fix the lesson first)", report.check.error_count());
eprintln!(
"build refused: {} errors (fix the lesson first)",
report.check.error_count()
);
} else {
eprintln!("build failed: target '{target}' has no assemble-markdown steps to run");
}
@@ -250,7 +268,11 @@ fn run_markdown_assemble_build(path: &std::path::Path, engine: &Engine, target:
outcome.field, outcome.parts_read
);
if let Some(out_rel) = &outcome.written {
println!("wrote {} ({} bytes)", path.join(out_rel).display(), outcome.body.len());
println!(
"wrote {} ({} bytes)",
path.join(out_rel).display(),
outcome.body.len()
);
}
if let Some(err) = &outcome.error {
eprintln!("assemble failed: {err}");
@@ -258,7 +280,10 @@ fn run_markdown_assemble_build(path: &std::path::Path, engine: &Engine, target:
}
if report.ok {
println!("assemble-markdown target '{target}' completed: {} step(s) ok", report.outcomes.len());
println!(
"assemble-markdown target '{target}' completed: {} step(s) ok",
report.outcomes.len()
);
ExitCode::SUCCESS
} else {
eprintln!("assemble-markdown target '{target}' failed");
+5 -1
View File
@@ -737,7 +737,11 @@ fn parse_target(name: String, value: toml::Value, diags: &mut Vec<Diagnostic>) -
/// A well-formed but empty `covers = []` yields `Some(vec![])`: a target that
/// explicitly renders no kind (every used kind then draws a `renderIgnored`
/// warning) — distinct from an absent key.
fn parse_covers(name: &str, value: toml::Value, diags: &mut Vec<Diagnostic>) -> Option<Vec<String>> {
fn parse_covers(
name: &str,
value: toml::Value,
diags: &mut Vec<Diagnostic>,
) -> Option<Vec<String>> {
let items = match value {
toml::Value::Array(items) => items,
_ => {
+5 -1
View File
@@ -324,7 +324,11 @@ fn cph_version_mismatch_is_an_error_diagnostic() {
.iter()
.filter(|d| d.code == DiagCode::CphVersionMismatch)
.collect();
assert_eq!(mm.len(), 1, "expected one CphVersionMismatch, got {diags:?}");
assert_eq!(
mm.len(),
1,
"expected one CphVersionMismatch, got {diags:?}"
);
assert!(
mm[0].message.contains("99.99.99") && mm[0].message.contains(CPH_VERSION),
"diagnostic should name both versions, got: {}",
+5 -1
View File
@@ -526,7 +526,11 @@ mod tests {
// ADR-0015: `slides`/`transcript` are markdown content fields (`x-cph-content: "md"`),
// distinct from the `.typ` content fields.
let s = schema_for("segment").unwrap();
let slides = s.content_fields.iter().find(|f| f.name == "slides").unwrap();
let slides = s
.content_fields
.iter()
.find(|f| f.name == "slides")
.unwrap();
assert_eq!(slides.ext, "md");
assert!(!slides.required);
let transcript = s
+2 -1
View File
@@ -48,7 +48,8 @@ pub fn resolve_render_dir() -> PathBuf {
ensure_extracted().unwrap_or_else(|_| {
// Last-resort: extract under the OS temp dir. Still correct, just not
// cached across processes.
let fallback = std::env::temp_dir().join(format!("cph-render-{}", env!("CARGO_PKG_VERSION")));
let fallback =
std::env::temp_dir().join(format!("cph-render-{}", env!("CARGO_PKG_VERSION")));
let _ = extract_to(&fallback);
fallback
})