diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-08-18 10:29:25 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-08-18 10:31:23 +0800 |
| commit | 721536eae99d468299c6d7f5ecd6b7dbeac4c34a (patch) | |
| tree | c9810fbd83630ea57d6e08c3a5d0e0c0d9672de5 /mingling_ci/src/markdown/test.rs | |
| parent | 8ea8e13f1a6b2a2942b78127e23d6783c5188ca5 (diff) | |
feat(ci-new): add markdown code block verification commands
Add `markdown-check` and `markdown-check-all` commands to verify Rust
code blocks in markdown files. Code blocks are compiled as test
projects, sharing temporary crates based on dependency configuration for
parallel execution.
Diffstat (limited to 'mingling_ci/src/markdown/test.rs')
| -rw-r--r-- | mingling_ci/src/markdown/test.rs | 144 |
1 files changed, 144 insertions, 0 deletions
diff --git a/mingling_ci/src/markdown/test.rs b/mingling_ci/src/markdown/test.rs new file mode 100644 index 0000000..c010e8c --- /dev/null +++ b/mingling_ci/src/markdown/test.rs @@ -0,0 +1,144 @@ +//! Parallel execution of markdown test projects. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use colored::Colorize; +use indicatif::{ProgressBar, ProgressStyle}; + +use super::project::{ + MarkdownTestProject, generate_build_rs, generate_cargo_toml, generate_main_rs, +}; + +/// Temporary root for the generated test crates. +const TEMP_BASE: &str = ".temp/doc-test"; + +/// Runs the given projects in parallel. +/// +/// Projects sharing a dependency hash share one temporary crate (written +/// serially within the group); groups run in parallel. Progress is shown on +/// stderr; failures print immediately. Returns the number of failed blocks. +pub(crate) async fn try_test_markdown_project(projs: Vec<MarkdownTestProject>) -> usize { + // Group by dependency hash for crate sharing. + let mut groups: BTreeMap<String, Vec<MarkdownTestProject>> = BTreeMap::new(); + for proj in projs { + groups.entry(proj.compute_hash()).or_default().push(proj); + } + + let total: usize = groups.values().map(Vec::len).sum(); + let pb = ProgressBar::new(total as u64); + pb.set_style( + ProgressStyle::default_bar() + .template(&format!( + "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}", + " Testing".bold().bright_cyan() + )) + .unwrap() + .progress_chars("=> "), + ); + pb.set_message("blocks"); + + // One blocking task per group; blocks within a group are serial because + // they share the same crate directory. + let mut handles = Vec::new(); + for (hash, blocks) in groups { + let pb = pb.clone(); + handles.push(tokio::task::spawn_blocking(move || { + let crate_dir = PathBuf::from(TEMP_BASE).join(&hash); + let src_dir = crate_dir.join("src"); + let manifest_path = crate_dir.join("Cargo.toml"); + let cargo_toml = generate_cargo_toml(&blocks[0], &manifest_path); + + let mut failed = 0; + for proj in &blocks { + let label = format!("{}:{}", proj.source_file, proj.line); + pb.set_message(label.clone()); + + let main_rs = if proj.is_build_time { + generate_build_rs(proj) + } else { + generate_main_rs(proj) + }; + let (ok, err) = build_block( + &src_dir, + &manifest_path, + &cargo_toml, + &main_rs, + proj.is_build_time, + ); + pb.inc(1); + + if !ok { + failed += 1; + // Plain stderr: `pb.println` is swallowed on non-TTY (CI). + eprintln!(" {} {label}", "failed".bold().bright_red()); + eprintln!(" {label} FAILED:\n{err}"); + } + } + failed + })); + } + + let mut fail_count = 0; + for handle in handles { + if let Ok(failed) = handle.await { + fail_count += failed; + } + } + + pb.finish_and_clear(); + fail_count +} + +/// Writes the temporary crate files and runs `cargo check`. +/// +/// When `is_build_time` is true, the content goes to `build.rs` with a stub +/// `main.rs`; otherwise it goes to `src/main.rs`. +fn build_block( + src_dir: &Path, + manifest_path: &Path, + cargo_toml: &str, + content: &str, + is_build_time: bool, +) -> (bool, String) { + if let Err(e) = std::fs::create_dir_all(src_dir) { + return (false, format!("mkdir: {e}")); + } + if let Err(e) = std::fs::write(manifest_path, cargo_toml) { + return (false, format!("write Cargo.toml: {e}")); + } + + if is_build_time { + let crate_dir = manifest_path + .parent() + .expect("manifest path has a parent directory"); + if let Err(e) = std::fs::write(crate_dir.join("build.rs"), content) { + return (false, format!("write build.rs: {e}")); + } + if let Err(e) = std::fs::write(src_dir.join("main.rs"), "fn main() {}\n") { + return (false, format!("write main.rs: {e}")); + } + } else if let Err(e) = std::fs::write(src_dir.join("main.rs"), content) { + return (false, format!("write main.rs: {e}")); + } + + let output = std::process::Command::new("cargo") + .args(["check", "--color=always", "--manifest-path"]) + .arg(manifest_path) + .output(); + match output { + Ok(output) if output.status.success() => (true, String::new()), + Ok(output) => { + let mut log = String::from_utf8_lossy(&output.stdout).into_owned(); + log.push_str(&String::from_utf8_lossy(&output.stderr)); + let lines: Vec<&str> = log.lines().collect(); + let tail = &lines[lines.len().saturating_sub(20)..]; + let exit = output + .status + .code() + .map_or_else(|| "?".to_string(), |c| c.to_string()); + (false, format!("exit code {exit}\n{}", tail.join("\n"))) + } + Err(e) => (false, format!("failed to run cargo: {e}")), + } +} |
