aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--dev_tools/Cargo.lock176
-rw-r--r--dev_tools/Cargo.toml1
-rw-r--r--dev_tools/src/bin/ci.rs55
-rw-r--r--dev_tools/src/bin/test-all-markdown-code.rs53
-rw-r--r--dev_tools/src/bin/test-examples.rs65
-rw-r--r--dev_tools/src/lib.rs122
6 files changed, 394 insertions, 78 deletions
diff --git a/dev_tools/Cargo.lock b/dev_tools/Cargo.lock
index 67acfc9..bb510bd 100644
--- a/dev_tools/Cargo.lock
+++ b/dev_tools/Cargo.lock
@@ -3,6 +3,18 @@
version = 4
[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
name = "colored"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -12,12 +24,54 @@ dependencies = [
]
[[package]]
+name = "console"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "unicode-width",
+ "windows-sys",
+]
+
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
+[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -34,12 +88,36 @@ dependencies = [
]
[[package]]
+name = "indicatif"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb"
+dependencies = [
+ "console",
+ "portable-atomic",
+ "unicode-width",
+ "unit-prefix",
+ "web-time",
+]
+
+[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
+name = "js-sys"
+version = "0.3.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "just_fmt"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -55,18 +133,36 @@ dependencies = [
]
[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
+name = "portable-atomic"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+
+[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -85,6 +181,12 @@ dependencies = [
]
[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -137,6 +239,12 @@ dependencies = [
]
[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -214,6 +322,7 @@ name = "tools"
version = "0.1.0"
dependencies = [
"colored",
+ "indicatif",
"just_fmt",
"just_template",
"serde",
@@ -229,6 +338,73 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "unit-prefix"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/dev_tools/Cargo.toml b/dev_tools/Cargo.toml
index 7e9e332..9855580 100644
--- a/dev_tools/Cargo.toml
+++ b/dev_tools/Cargo.toml
@@ -18,3 +18,4 @@ toml = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
+indicatif = "0.18.4"
diff --git a/dev_tools/src/bin/ci.rs b/dev_tools/src/bin/ci.rs
index d1071ab..1f536c9 100644
--- a/dev_tools/src/bin/ci.rs
+++ b/dev_tools/src/bin/ci.rs
@@ -2,7 +2,7 @@ use std::io::Write as _;
use std::process::exit;
use tools::{
- cargo_tomls, eprintln_cargo_style, println_cargo_style, run_cmd, wprintln_cargo_style,
+ cargo_tomls, crate_name_from, eprintln_cargo_style, println_cargo_style, run_cmd, run_parallel,
};
fn get_ignore_dirs() -> Vec<String> {
@@ -128,73 +128,76 @@ fn ci(test_docs: bool, test_codes: bool, run_all: bool) -> Result<(), i32> {
}
fn test_examples() -> Result<(), i32> {
- println_cargo_style!("Testing: examples");
- run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin test-examples")
+ run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --color always --bin test-examples")
}
fn test_docs_code_blocks() -> Result<(), i32> {
- println_cargo_style!("Testing: documentation code blocks");
- run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin test-all-markdown-code")
+ run_cmd!(
+ "cargo run --manifest-path dev_tools/Cargo.toml --color always --bin test-all-markdown-code"
+ )
}
fn build_all() -> Result<(), i32> {
let ignore_dirs = get_ignore_dirs();
let cargo_tomls = cargo_tomls();
+ let mut tasks = Vec::new();
for cargo_toml in cargo_tomls {
let path = cargo_toml.parent().unwrap_or(std::path::Path::new(""));
let path_str = path.to_string_lossy();
if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) {
- wprintln_cargo_style!("Skipping: {} (ignored dir)", cargo_toml.to_string_lossy());
continue;
}
- println_cargo_style!("Build: {}", cargo_toml.to_string_lossy());
- run_cmd!(
- "cargo build --manifest-path {}",
+ let label = format!("Build: {}", cargo_toml.to_string_lossy());
+ let crate_name = crate_name_from(&cargo_toml);
+ let cmd = format!(
+ "cargo build --manifest-path {} --color always",
cargo_toml.to_string_lossy()
- )?;
+ );
+ tasks.push((label, crate_name, cmd));
}
-
- Ok(())
+ run_parallel("Building", tasks)
}
fn clippy_all() -> Result<(), i32> {
let ignore_dirs = get_ignore_dirs();
let cargo_tomls = cargo_tomls();
+ let mut tasks = Vec::new();
for cargo_toml in cargo_tomls {
let path = cargo_toml.parent().unwrap_or(std::path::Path::new(""));
let path_str = path.to_string_lossy();
if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) {
- println_cargo_style!("Skipping: {} (ignored dir)", cargo_toml.to_string_lossy());
continue;
}
- println_cargo_style!("Clippy: {}", cargo_toml.to_string_lossy());
- run_cmd!(
- "cargo clippy --manifest-path {} -- -D warnings",
+ let label = format!("Clippy: {}", cargo_toml.to_string_lossy());
+ let crate_name = crate_name_from(&cargo_toml);
+ let cmd = format!(
+ "cargo clippy --manifest-path {} --color always -- -D warnings",
cargo_toml.to_string_lossy()
- )?;
+ );
+ tasks.push((label, crate_name, cmd));
}
-
- Ok(())
+ run_parallel("Clippy", tasks)
}
fn test_all() -> Result<(), i32> {
let ignore_dirs = get_ignore_dirs();
let cargo_tomls = cargo_tomls();
+ let mut tasks = Vec::new();
for cargo_toml in cargo_tomls {
let path = cargo_toml.parent().unwrap_or(std::path::Path::new(""));
let path_str = path.to_string_lossy();
if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) {
- println_cargo_style!("Skipping: {} (ignored dir)", cargo_toml.to_string_lossy());
continue;
}
- println_cargo_style!("Testing: {}", cargo_toml.to_string_lossy());
- run_cmd!(
- "cargo test --manifest-path {}",
+ let label = format!("Testing: {}", cargo_toml.to_string_lossy());
+ let crate_name = crate_name_from(&cargo_toml);
+ let cmd = format!(
+ "cargo test --manifest-path {} --color always",
cargo_toml.to_string_lossy()
- )?;
+ );
+ tasks.push((label, crate_name, cmd));
}
-
- Ok(())
+ run_parallel("Testing", tasks)
}
fn docs_refresh() -> Result<(), i32> {
diff --git a/dev_tools/src/bin/test-all-markdown-code.rs b/dev_tools/src/bin/test-all-markdown-code.rs
index 31aebdd..a1acb22 100644
--- a/dev_tools/src/bin/test-all-markdown-code.rs
+++ b/dev_tools/src/bin/test-all-markdown-code.rs
@@ -1,9 +1,9 @@
use std::collections::HashMap;
use std::env;
-use std::io::Write;
use std::path::{Path, PathBuf};
use colored::Colorize;
+use indicatif::ProgressBar;
use tools::verify::{
build_block, compute_block_hash, generate_cargo_toml, generate_main_rs, is_block_testable,
parse_code_blocks, write_summary_report,
@@ -70,7 +70,6 @@ async fn main() {
let readme_path = PathBuf::from(&config.verified.readme);
if readme_path.exists() {
files.push(("README".to_string(), readme_path));
- println_cargo_style!("Source: found README.md");
}
// English docs
@@ -79,7 +78,6 @@ async fn main() {
let dir = PathBuf::from(base);
if dir.exists() && dir.is_dir() {
collect_md_files(&dir, &mut files, "en");
- println_cargo_style!("Source: found docs/pages/");
}
}
@@ -89,13 +87,11 @@ async fn main() {
let dir = PathBuf::from(base);
if dir.exists() && dir.is_dir() {
collect_md_files(&dir, &mut files, "zh_CN");
- println_cargo_style!("Source: found docs/_zh_CN/pages/");
}
}
// If a single file was specified, filter the list to only that file
if let Some(ref target) = single_file {
- // Canonicalize to compare paths reliably
let target_canon = std::fs::canonicalize(target).unwrap_or_else(|_| target.clone());
files.retain(|(_, path)| {
std::fs::canonicalize(path)
@@ -109,7 +105,6 @@ async fn main() {
);
std::process::exit(1);
}
- println_cargo_style!("Source: testing only file '{}'", target.display());
}
if files.is_empty() {
@@ -141,10 +136,18 @@ async fn main() {
return;
}
- println_cargo_style!(
- "Test: found {total_testable} testable code blocks across {} files",
- files.len()
+ // Create a shared progress bar
+ let bar = ProgressBar::new(total_testable as u64);
+ bar.set_style(
+ indicatif::ProgressStyle::default_bar()
+ .template(&format!(
+ "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}",
+ " Testing".bold().bright_cyan()
+ ))
+ .unwrap()
+ .progress_chars("=> "),
);
+ bar.set_message("blocks");
// Group blocks by dependency hash
let mut groups: HashMap<String, Vec<(usize, tools::verify::CodeBlock)>> = HashMap::new();
@@ -153,11 +156,6 @@ async fn main() {
groups.entry(hash).or_default().push((idx, block));
}
- println_cargo_style!(
- "Cache: grouped into {} unique dependency configurations",
- groups.len()
- );
-
let temp_base = PathBuf::from(".temp/doc-test");
// Sort groups by hash for deterministic output order
@@ -169,14 +167,12 @@ async fn main() {
let mut handles = Vec::new();
for (hash, blocks) in group_vec {
let temp_base = temp_base.clone();
+ let bar = bar.clone(); // clone shares the same underlying progress
let handle = tokio::task::spawn_blocking(move || {
let crate_dir = temp_base.join(&hash);
let src_dir = crate_dir.join("src");
let manifest_path = crate_dir.join("Cargo.toml");
- // Buffer all output for this group so it prints contiguously
- let mut output = String::new();
-
// Generate a single Cargo.toml for the whole group (all blocks share same deps)
let first_block = &blocks[0].1;
let cargo_toml = generate_cargo_toml(first_block, "test-doc", &manifest_path);
@@ -186,23 +182,23 @@ async fn main() {
let block_label =
format!("Block {block_idx} ({}:{})", block.source_file, block.line);
+ bar.set_message(block_label.clone());
+
let main_rs = generate_main_rs(block);
let (ok, err) = build_block(&src_dir, &manifest_path, &cargo_toml, &main_rs);
if ok {
- output.push_str(&format!(
- " Testing {block_label} ... {}\n",
- "passed".bold().bright_green()
- ));
+ bar.inc(1);
} else {
- output.push_str(&format!(
- " Testing {block_label} ... {}\n",
+ bar.inc(1);
+ bar.println(format!(
+ " {} {block_label}",
"failed".bold().bright_red()
));
- output.push_str(&format!(" {block_label} FAILED:\n{err}\n"));
+ bar.println(format!(" {block_label} FAILED:\n{err}"));
}
group_results.push((block.source_file.clone(), block.line, ok, err));
}
- (output, group_results)
+ group_results
});
handles.push(handle);
}
@@ -214,10 +210,7 @@ async fn main() {
for handle in handles {
match handle.await {
- Ok((output, group_results)) => {
- // Print entire group output at once, avoiding interleaving
- print!("{output}");
- let _ = std::io::stdout().flush();
+ Ok(group_results) => {
for (file, line, ok, err) in group_results {
if ok {
passed += 1;
@@ -234,6 +227,8 @@ async fn main() {
}
}
+ bar.finish_and_clear();
+
let result_msg = format!("Result: {passed}/{total_testable} blocks passed");
println_cargo_style!(result_msg);
diff --git a/dev_tools/src/bin/test-examples.rs b/dev_tools/src/bin/test-examples.rs
index ddf5f7c..5153709 100644
--- a/dev_tools/src/bin/test-examples.rs
+++ b/dev_tools/src/bin/test-examples.rs
@@ -1,7 +1,9 @@
use std::collections::HashMap;
+use colored::Colorize;
+use indicatif::ProgressBar;
use serde::Deserialize;
-use tools::{eprintln_cargo_style, println_cargo_style, run_cmd};
+use tools::{eprintln_cargo_style, println_cargo_style};
#[derive(Deserialize)]
struct TestConfig {
@@ -26,7 +28,24 @@ fn main() {
let _ = colored::control::set_virtual_terminal(true);
let config = load_config();
- let (passed, total) = run_all_tests(&config);
+
+ // Count total test cases upfront
+ let total: usize = config.test.values().map(|cases| cases.len()).sum();
+ let bar = ProgressBar::new(total as u64);
+ bar.set_style(
+ indicatif::ProgressStyle::default_bar()
+ .template(&format!(
+ "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}",
+ " Testing".bold().bright_cyan()
+ ))
+ .unwrap()
+ .progress_chars("=> "),
+ );
+ bar.set_message("examples");
+
+ let passed = run_all_tests(&config, &bar);
+
+ bar.finish_and_clear();
println_cargo_style!("Result: {}/{} tests passed", passed, total);
@@ -49,38 +68,40 @@ fn load_config() -> TestConfig {
})
}
-/// Run all example test groups, return (passed, total)
-fn run_all_tests(config: &TestConfig) -> (usize, usize) {
- let mut total = 0;
+/// Run all example test groups, return number passed
+fn run_all_tests(config: &TestConfig, bar: &ProgressBar) -> usize {
let mut passed = 0;
for (example_name, test_cases) in &config.test {
- println_cargo_style!("Test: {}", example_name);
+ bar.set_message(example_name.clone());
if !build_example(example_name) {
- total += test_cases.len();
+ bar.inc(test_cases.len() as u64);
continue;
}
for test_case in test_cases {
- total += 1;
- if run_single_test(example_name, test_case) {
+ if run_single_test(example_name, test_case, bar) {
passed += 1;
}
+ bar.inc(1);
}
}
- (passed, total)
+ passed
}
/// Build the example binary, return true on success
fn build_example(example_name: &str) -> bool {
let manifest = format!("examples/{example_name}/Cargo.toml");
- run_cmd!("cargo build --manifest-path {}", manifest).is_ok()
+ tools::run_cmd_capture(&format!(
+ "cargo build --manifest-path {manifest} --color always",
+ ))
+ .is_ok()
}
/// Run a single test case, return true on pass
-fn run_single_test(example_name: &str, test_case: &TestCase) -> bool {
+fn run_single_test(example_name: &str, test_case: &TestCase, bar: &ProgressBar) -> bool {
let binary_path = format!(".temp/target/debug/{}", get_binary_name(example_name));
let args: Vec<&str> = test_case.command.split_whitespace().collect();
@@ -90,7 +111,7 @@ fn run_single_test(example_name: &str, test_case: &TestCase) -> bool {
{
Ok(o) => o,
Err(e) => {
- eprintln_cargo_style!("'{}' - failed to run: {}", test_case.command, e);
+ bar.println(format!("'{}' - failed to run: {}", test_case.command, e));
return false;
}
};
@@ -104,22 +125,20 @@ fn run_single_test(example_name: &str, test_case: &TestCase) -> bool {
|| actual_stdout.contains(&test_case.expect.result);
if exit_ok && result_ok {
- println_cargo_style!("Passed: '{}'", test_case.command);
true
} else {
- eprintln_cargo_style!("'{}'", test_case.command);
+ bar.println(format!("failed: '{}'", test_case.command));
if !exit_ok {
- eprintln_cargo_style!(
- "Expected exit code: {}, actual: {}",
- test_case.expect.exit_code,
- actual_exit_code
- );
+ bar.println(format!(
+ " Expected exit code: {}, actual: {}",
+ test_case.expect.exit_code, actual_exit_code
+ ));
}
if !result_ok {
- eprintln_cargo_style!("Expected output: {:?}", test_case.expect.result);
- eprintln_cargo_style!("Actual stdout: {:?}", actual_stdout);
+ bar.println(format!(" Expected output: {:?}", test_case.expect.result));
+ bar.println(format!(" Actual stdout: {:?}", actual_stdout));
if !actual_stderr.is_empty() {
- eprintln_cargo_style!("Actual stderr: {:?}", actual_stderr);
+ bar.println(format!(" Actual stderr: {:?}", actual_stderr));
}
}
false
diff --git a/dev_tools/src/lib.rs b/dev_tools/src/lib.rs
index 13ed71f..2bd55a2 100644
--- a/dev_tools/src/lib.rs
+++ b/dev_tools/src/lib.rs
@@ -183,6 +183,128 @@ pub fn run_cmd_capture(cmd: impl Into<String>) -> Result<String, (i32, String)>
}
}
+/// Extract a crate-style name from a `Cargo.toml` path.
+///
+/// Examples:
+/// - `mingling_core/Cargo.toml` → `mingling_core`
+/// - `.` → `(root)`
+pub fn crate_name_from(path: &std::path::Path) -> String {
+ path.parent()
+ .and_then(|p| p.file_name())
+ .and_then(|n| n.to_str())
+ .unwrap_or("(root)")
+ .to_string()
+}
+
+/// Run a list of `(label_for_errors, crate_name_for_bar, shell_command)` tuples
+/// in parallel with a progress bar.
+///
+/// - Success: silent, the bar tracks progress:
+/// ` Building [============================] 32/32: mingling_core`
+/// - Failure: `pb.println()` prints the error immediately above the bar.
+pub fn run_parallel(phase: &str, tasks: Vec<(String, String, String)>) -> Result<(), i32> {
+ let n = tasks.len();
+ if n == 0 {
+ return Ok(());
+ }
+
+ // Cargo-style prefix: right-aligned to 12 chars, bold bright cyan
+ let padding = " ".repeat(12 - phase.len());
+ let styled_prefix = format!("{}{}", padding, phase.bold().bright_cyan());
+
+ let pb = indicatif::ProgressBar::new(n as u64);
+ pb.set_style(
+ indicatif::ProgressStyle::default_bar()
+ .template(&format!(
+ "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}",
+ styled_prefix
+ ))
+ .unwrap()
+ .progress_chars("=> "),
+ );
+ pb.set_position(0);
+
+ // Pre-extract labels for error messages
+ let labels: Vec<String> = tasks.iter().map(|(l, _, _)| l.clone()).collect();
+
+ let (tx, rx) = std::sync::mpsc::channel::<(usize, String, Result<String, (i32, String)>)>();
+
+ for (i, (_label, crate_name, cmd)) in tasks.into_iter().enumerate() {
+ let tx = tx.clone();
+ std::thread::spawn(move || {
+ let result = run_cmd_capture(&cmd);
+ let _ = tx.send((i, crate_name, result));
+ });
+ }
+ drop(tx);
+
+ let mut first_exit_code = 0;
+
+ while let Ok((i, crate_name, result)) = rx.recv() {
+ pb.inc(1);
+ pb.set_message(crate_name);
+
+ if let Err((code, output)) = result {
+ if first_exit_code == 0 {
+ first_exit_code = code;
+ }
+ pb.println(format!(
+ "{}: {} failed (exit code {})",
+ "error".bright_red().bold(),
+ labels[i],
+ code,
+ ));
+ if !output.is_empty() {
+ pb.println(output.trim_end().to_string());
+ }
+ }
+ }
+
+ pb.finish_and_clear();
+
+ if first_exit_code != 0 {
+ Err(first_exit_code)
+ } else {
+ Ok(())
+ }
+}
+
+/// Run a single shell command with a progress bar, capturing its output.
+///
+/// - Success: bar clears silently.
+/// - Failure: error is printed above the bar, then the bar clears.
+pub fn run_cmd_with_progress(phase: &str, label: &str, cmd: String) -> Result<(), i32> {
+ let padding = " ".repeat(12 - phase.len());
+ let styled_prefix = format!("{}{}", padding, phase.bold().bright_cyan());
+
+ let pb = indicatif::ProgressBar::new(1);
+ pb.set_style(
+ indicatif::ProgressStyle::default_bar()
+ .template(&format!(
+ "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}",
+ styled_prefix
+ ))
+ .unwrap()
+ .progress_chars("=> "),
+ );
+ pb.set_message(label.to_owned());
+
+ let result = run_cmd_capture(&cmd);
+ pb.inc(1);
+ pb.finish_and_clear();
+
+ match result {
+ Ok(_) => Ok(()),
+ Err((code, output)) => {
+ eprintln_cargo_style(format!("{} failed (exit code {})", label, code));
+ if !output.is_empty() {
+ println!("{}", output.trim_end());
+ }
+ Err(code)
+ }
+ }
+}
+
#[must_use]
pub fn cargo_tomls() -> Vec<std::path::PathBuf> {
let mut cargo_tomls = Vec::new();