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. --- 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 ++++++++++++++++----------- 4 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 mingling_ci/src/task/cmd_test.rs (limited to 'mingling_ci/src/task') 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(); -- cgit