aboutsummaryrefslogtreecommitdiff
path: root/mingling_ci
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_ci')
-rw-r--r--mingling_ci/help.txt1
-rw-r--r--mingling_ci/src/bin/ci.rs1
-rw-r--r--mingling_ci/src/res.rs3
-rw-r--r--mingling_ci/src/res/crate_config.rs79
-rw-r--r--mingling_ci/src/task.rs1
-rw-r--r--mingling_ci/src/task/cmd_build.rs8
-rw-r--r--mingling_ci/src/task/cmd_clippy.rs8
-rw-r--r--mingling_ci/src/task/cmd_test.rs54
-rw-r--r--mingling_ci/src/task/run.rs38
9 files changed, 175 insertions, 18 deletions
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 `<<<features>>>` expanded).
+ test_commands: HashMap<String, Vec<String>>,
+}
+
+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<ThisProgram>) {
+ let features = p
+ .res::<ResFeatureList>()
+ .map(|f| f.list.clone())
+ .unwrap_or_default();
+ let joined_features = features.join(",");
+
+ let Some(manifests) = p.res::<Manifests>() 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::<toml::Value>() 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<String> = 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("<<<features>>>", &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 <path>`
fn build_args(path: &Path) -> Vec<OsString> {
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 <path> -- -D warnings`
fn clippy_args(path: &Path) -> Vec<OsString> {
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 <path>` (crates without a
+/// `mingling-ci.toml` override).
+fn test_args(path: &Path) -> Vec<OsString> {
+ 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<OsString>,
- manifests: &Manifests,
+ tasks: Vec<(String, Vec<OsString>)>,
) -> 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<OsString>) -> 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<OsString>) -> 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();