From 570b2bc1710c0ab8a68ad69a6121144e4f5e3aca Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Tue, 18 Aug 2026 10:01:53 +0800 Subject: feat(ci-new): add test-all task with per-crate config support Add a new `test-all` command that runs tests across all crates with optional per-crate command overrides via `mingling-ci.toml` files. --- arg_picker/mingling-ci.toml | 2 + arg_picker_macros/mingling-ci.toml | 2 + mingling/mingling-ci.toml | 2 + mingling_ci/help.txt | 1 + mingling_ci/src/bin/ci.rs | 1 + mingling_ci/src/res.rs | 3 ++ mingling_ci/src/res/crate_config.rs | 79 +++++++++++++++++++++++++++++++++++++ mingling_ci/src/task.rs | 1 + mingling_ci/src/task/cmd_build.rs | 8 +++- mingling_ci/src/task/cmd_clippy.rs | 8 +++- mingling_ci/src/task/cmd_test.rs | 54 +++++++++++++++++++++++++ mingling_ci/src/task/run.rs | 38 ++++++++++-------- mingling_core/mingling-ci.toml | 2 + mingling_macros/mingling-ci.toml | 2 + mingling_pathf/mingling-ci.toml | 2 + 15 files changed, 187 insertions(+), 18 deletions(-) create mode 100644 arg_picker/mingling-ci.toml create mode 100644 arg_picker_macros/mingling-ci.toml create mode 100644 mingling/mingling-ci.toml create mode 100644 mingling_ci/src/res/crate_config.rs create mode 100644 mingling_ci/src/task/cmd_test.rs create mode 100644 mingling_core/mingling-ci.toml create mode 100644 mingling_macros/mingling-ci.toml create mode 100644 mingling_pathf/mingling-ci.toml diff --git a/arg_picker/mingling-ci.toml b/arg_picker/mingling-ci.toml new file mode 100644 index 0000000..73a9935 --- /dev/null +++ b/arg_picker/mingling-ci.toml @@ -0,0 +1,2 @@ +[test] +command = ["cargo", "test", "-p", "arg-picker"] diff --git a/arg_picker_macros/mingling-ci.toml b/arg_picker_macros/mingling-ci.toml new file mode 100644 index 0000000..62fc2b1 --- /dev/null +++ b/arg_picker_macros/mingling-ci.toml @@ -0,0 +1,2 @@ +[test] +command = ["cargo", "test", "-p", "arg-picker-macros"] diff --git a/mingling/mingling-ci.toml b/mingling/mingling-ci.toml new file mode 100644 index 0000000..767c133 --- /dev/null +++ b/mingling/mingling-ci.toml @@ -0,0 +1,2 @@ +[test] +command = ["cargo", "test", "-p", "mingling", "--features", "<<>>"] diff --git a/mingling_ci/help.txt b/mingling_ci/help.txt index 1c59030..85d5c76 100644 --- a/mingling_ci/help.txt +++ b/mingling_ci/help.txt @@ -18,3 +18,4 @@ COMMANDS: TASKS: build-all Build all crates clippy-all Run clippy with -D warnings on all crates + test-all Test all crates diff --git a/mingling_ci/src/bin/ci.rs b/mingling_ci/src/bin/ci.rs index 98e0fd5..5b1d748 100644 --- a/mingling_ci/src/bin/ci.rs +++ b/mingling_ci/src/bin/ci.rs @@ -23,6 +23,7 @@ async fn main() { // CI Plugins program.with_setup(ManifestsSetup); program.with_setup(FeaturesSetup); + program.with_setup(CrateConfigSetup); program.with_setup(ReportSetup); program.exec_and_exit().await; diff --git a/mingling_ci/src/res.rs b/mingling_ci/src/res.rs index f794e7c..54ed503 100644 --- a/mingling_ci/src/res.rs +++ b/mingling_ci/src/res.rs @@ -1,6 +1,9 @@ mod collect_logs; pub use collect_logs::*; +mod crate_config; +pub use crate_config::*; + mod features; pub use features::*; diff --git a/mingling_ci/src/res/crate_config.rs b/mingling_ci/src/res/crate_config.rs new file mode 100644 index 0000000..b20e83d --- /dev/null +++ b/mingling_ci/src/res/crate_config.rs @@ -0,0 +1,79 @@ +use std::collections::HashMap; +use std::path::Path; + +use mingling::{Program, macros::program_setup}; + +use crate::ThisProgram; +use crate::res::{Manifests, ResFeatureList}; + +/// Per-crate CI overrides from `mingling-ci.toml` (optional, crate root). +/// +/// Currently only `[test] command` is read; `clippy.command` / `build.command` +/// will follow the same shape. +#[derive(Default, Clone)] +pub struct ResCrateConfig { + /// Package name -> test command argv (with `<<>>` expanded). + test_commands: HashMap>, +} + +impl ResCrateConfig { + /// The configured `[test] command` for a package, if any. + #[must_use] + pub fn test_command(&self, package: &str) -> Option<&[String]> { + self.test_commands.get(package).map(Vec::as_slice) + } +} + +#[program_setup] +pub fn crate_config_setup(p: &mut Program) { + let features = p + .res::() + .map(|f| f.list.clone()) + .unwrap_or_default(); + let joined_features = features.join(","); + + let Some(manifests) = p.res::() else { + return; + }; + + let mut test_commands = HashMap::new(); + for (name, manifest_path) in &manifests.package_dirs { + let config_path = manifest_path + .parent() + .unwrap_or_else(|| Path::new(".")) + .join("mingling-ci.toml"); + + let Ok(content) = std::fs::read_to_string(&config_path) else { + continue; + }; + + let Ok(table) = content.parse::() else { + continue; + }; + + let Some(command) = table + .get("test") + .and_then(|t| t.get("command")) + .and_then(|c| c.as_array()) + else { + continue; + }; + + let argv: Vec = command + .iter() + .filter_map(|v| v.as_str().map(str::to_string)) + .collect(); + + if argv.is_empty() { + continue; + } + + let argv = argv + .into_iter() + .map(|arg| arg.replace("<<>>", &joined_features)) + .collect(); + test_commands.insert(name.clone(), argv); + } + + p.with_resource(ResCrateConfig { test_commands }); +} diff --git a/mingling_ci/src/task.rs b/mingling_ci/src/task.rs index 07ffb3f..b5161ad 100644 --- a/mingling_ci/src/task.rs +++ b/mingling_ci/src/task.rs @@ -1,3 +1,4 @@ pub(crate) mod cmd_build; pub(crate) mod cmd_clippy; +pub(crate) mod cmd_test; pub(crate) mod run; diff --git a/mingling_ci/src/task/cmd_build.rs b/mingling_ci/src/task/cmd_build.rs index 44be592..a74c323 100644 --- a/mingling_ci/src/task/cmd_build.rs +++ b/mingling_ci/src/task/cmd_build.rs @@ -13,13 +13,19 @@ use crate::task::run::run_parallel_checks; #[command(node = "build-all")] pub async fn build_all(manifests: &Manifests) -> Next { - let fail_count = run_parallel_checks("Build-All", "Building", build_args, manifests).await; + let tasks = manifests + .package_dirs + .iter() + .map(|(name, path)| (name.clone(), build_args(path))) + .collect(); + let fail_count = run_parallel_checks("Build-All", "Building", tasks).await; ResultBuildAll { fail_count }.to_chain() } /// `cargo build --manifest-path ` fn build_args(path: &Path) -> Vec { vec![ + "cargo".into(), "build".into(), "--manifest-path".into(), path.as_os_str().to_os_string(), diff --git a/mingling_ci/src/task/cmd_clippy.rs b/mingling_ci/src/task/cmd_clippy.rs index 11227a2..0a4282b 100644 --- a/mingling_ci/src/task/cmd_clippy.rs +++ b/mingling_ci/src/task/cmd_clippy.rs @@ -13,13 +13,19 @@ use crate::task::run::run_parallel_checks; #[command(node = "clippy-all")] pub async fn clippy_all(manifests: &Manifests) -> Next { - let fail_count = run_parallel_checks("Clippy-All", "Clippy", clippy_args, manifests).await; + let tasks = manifests + .package_dirs + .iter() + .map(|(name, path)| (name.clone(), clippy_args(path))) + .collect(); + let fail_count = run_parallel_checks("Clippy-All", "Clippy", tasks).await; ResultClippyAll { fail_count }.to_chain() } /// `cargo clippy --manifest-path -- -D warnings` fn clippy_args(path: &Path) -> Vec { vec![ + "cargo".into(), "clippy".into(), "--manifest-path".into(), path.as_os_str().to_os_string(), diff --git a/mingling_ci/src/task/cmd_test.rs b/mingling_ci/src/task/cmd_test.rs new file mode 100644 index 0000000..3ed193c --- /dev/null +++ b/mingling_ci/src/task/cmd_test.rs @@ -0,0 +1,54 @@ +use std::ffi::OsString; +use std::path::Path; + +use mingling::{ + Grouped, Routable, + macros::{buffer, command, renderer}, + res::ResExitCode, +}; + +use crate::Next; +use crate::res::{Manifests, ResCrateConfig}; +use crate::task::run::run_parallel_checks; + +#[command(node = "test-all")] +pub async fn test_all(manifests: &Manifests, config: &ResCrateConfig) -> Next { + let tasks = manifests + .package_dirs + .iter() + .map(|(name, path)| { + let args = config.test_command(name).map_or_else( + || test_args(path), + |cmd| cmd.iter().map(|s| OsString::from(s.as_str())).collect(), + ); + (name.clone(), args) + }) + .collect(); + let fail_count = run_parallel_checks("Test-All", "Testing", tasks).await; + ResultTestAll { fail_count }.to_chain() +} + +/// Default: `cargo test --manifest-path ` (crates without a +/// `mingling-ci.toml` override). +fn test_args(path: &Path) -> Vec { + vec![ + "cargo".into(), + "test".into(), + "--manifest-path".into(), + path.as_os_str().to_os_string(), + ] +} + +/// Number of packages that failed tests. +#[derive(Grouped)] +pub struct ResultTestAll { + pub fail_count: usize, +} + +/// Silently sets a non-zero exit code when any test failed. +#[renderer(buffer)] +pub fn render_test_all(r: ResultTestAll, exit_code: &mut ResExitCode) { + if r.fail_count > 0 { + exit_code.exit_code = 1; + } +} diff --git a/mingling_ci/src/task/run.rs b/mingling_ci/src/task/run.rs index eddc356..d00334d 100644 --- a/mingling_ci/src/task/run.rs +++ b/mingling_ci/src/task/run.rs @@ -1,11 +1,9 @@ use std::ffi::OsString; -use std::path::Path; use colored::Colorize; use indicatif::{ProgressBar, ProgressStyle}; use crate::reporter::{self, ReportResult}; -use crate::res::Manifests; /// Outcome of a `cargo` subcommand. struct CargoResult { @@ -14,20 +12,19 @@ struct CargoResult { output: String, } -/// Runs one `cargo` subcommand per manifest in parallel. +/// Runs the given cargo task list in parallel. /// -/// Progress and failures go to stderr: a failing package prints its output -/// immediately and writes its report entry at the same time. Returns the -/// number of failing packages. +/// Each task is a `(name, args)` pair; progress and failures go to stderr: a +/// failing task prints its output immediately and writes its report entry at +/// the same time. Returns the number of failing tasks. pub(crate) async fn run_parallel_checks( task: &str, phase: &str, - args_for: fn(&Path) -> Vec, - manifests: &Manifests, + tasks: Vec<(String, Vec)>, ) -> usize { reporter::set_task(task); - let n = manifests.package_dirs.len(); + let n = tasks.len(); let pb = ProgressBar::new(n as u64); let padding = " ".repeat(12usize.saturating_sub(phase.len())); let styled_prefix = format!("{}{}", padding, phase.bold().bright_cyan()); @@ -40,11 +37,9 @@ pub(crate) async fn run_parallel_checks( .progress_chars("=> "), ); - // Run one cargo invocation per manifest in parallel. + // Run each task in parallel. let mut set = tokio::task::JoinSet::new(); - for (name, path) in &manifests.package_dirs { - let (name, path) = (name.clone(), path.clone()); - let args = args_for(&path); + for (name, args) in tasks { set.spawn(async move { (name, run_cargo(args).await) }); } @@ -85,11 +80,22 @@ pub(crate) async fn run_parallel_checks( } /// Runs a `cargo` subcommand, capturing its output. -async fn run_cargo(args: Vec) -> CargoResult { - let output = tokio::process::Command::new("cargo") - .args(args) +/// Runs a cargo subcommand (`argv[0]` is the program), capturing its output. +async fn run_cargo(argv: Vec) -> CargoResult { + let mut argv = argv.into_iter(); + let Some(program) = argv.next() else { + return CargoResult { + ok: false, + exit_code: None, + output: "empty command".to_string(), + }; + }; + + let output = tokio::process::Command::new(program) + .args(argv) .output() .await; + match output { Ok(output) => { let mut log = String::from_utf8_lossy(&output.stdout).into_owned(); diff --git a/mingling_core/mingling-ci.toml b/mingling_core/mingling-ci.toml new file mode 100644 index 0000000..b243cbe --- /dev/null +++ b/mingling_core/mingling-ci.toml @@ -0,0 +1,2 @@ +[test] +command = ["cargo", "test", "-p", "mingling_core"] diff --git a/mingling_macros/mingling-ci.toml b/mingling_macros/mingling-ci.toml new file mode 100644 index 0000000..3aefc4a --- /dev/null +++ b/mingling_macros/mingling-ci.toml @@ -0,0 +1,2 @@ +[test] +command = ["cargo", "test", "-p", "mingling_macros"] diff --git a/mingling_pathf/mingling-ci.toml b/mingling_pathf/mingling-ci.toml new file mode 100644 index 0000000..59e4e17 --- /dev/null +++ b/mingling_pathf/mingling-ci.toml @@ -0,0 +1,2 @@ +[test] +command = ["cargo", "test", "-p", "mingling_pathf"] -- cgit