aboutsummaryrefslogtreecommitdiff
path: root/mingling_ci/src/markdown/test.rs
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_ci/src/markdown/test.rs')
-rw-r--r--mingling_ci/src/markdown/test.rs152
1 files changed, 152 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..8ecf18d
--- /dev/null
+++ b/mingling_ci/src/markdown/test.rs
@@ -0,0 +1,152 @@
+//! Parallel execution of markdown test projects.
+
+use std::collections::BTreeMap;
+use std::path::{Path, PathBuf};
+
+use colored::Colorize;
+
+use crate::progress::task_progress_bar;
+
+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";
+
+/// Outcome of testing one code block.
+pub(crate) struct MarkdownBlockOutcome {
+ pub source_file: String,
+ pub line: usize,
+ pub ok: bool,
+ /// Failure detail; empty when `ok`.
+ pub output: String,
+}
+
+/// 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 there too. Returns one outcome per block.
+pub(crate) async fn try_test_markdown_project(
+ projs: Vec<MarkdownTestProject>,
+) -> Vec<MarkdownBlockOutcome> {
+ // 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 = task_progress_bar(total, "Testing");
+ 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 group_outcomes = Vec::new();
+ 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 {
+ // Plain stderr: `pb.println` is swallowed on non-TTY (CI).
+ eprintln!(" {} {label}", "failed".bold().bright_red());
+ eprintln!(" {label} FAILED:\n{err}");
+ }
+ group_outcomes.push(MarkdownBlockOutcome {
+ source_file: proj.source_file.clone(),
+ line: proj.line,
+ ok,
+ output: err,
+ });
+ }
+ group_outcomes
+ }));
+ }
+
+ let mut all_outcomes = Vec::new();
+ for handle in handles {
+ if let Ok(group_outcomes) = handle.await {
+ all_outcomes.extend(group_outcomes);
+ }
+ }
+
+ pb.finish_and_clear();
+ all_outcomes
+}
+
+/// 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}")),
+ }
+}