diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-08-18 10:13:39 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-08-18 10:13:39 +0800 |
| commit | 8ea8e13f1a6b2a2942b78127e23d6783c5188ca5 (patch) | |
| tree | 01cc8dde8cb8c90529c8d5f66dc57a08ed7cdc26 | |
| parent | 570b2bc1710c0ab8a68ad69a6121144e4f5e3aca (diff) | |
refactor(ci-new): generalize report entries from packages to items
Rename package-based terminology and structures to item-based, allowing
arbitrary items with associated locations instead of only crate
packages.
Locations are now carried through report files and included in generated
reports, with the fallback to `—` removed.
| -rw-r--r-- | mingling_ci/src/cmd/cmd_report_collect.rs | 47 | ||||
| -rw-r--r-- | mingling_ci/src/reporter.rs | 72 | ||||
| -rw-r--r-- | mingling_ci/src/res/collect_logs.rs | 35 | ||||
| -rw-r--r-- | mingling_ci/src/task/cmd_build.rs | 4 | ||||
| -rw-r--r-- | mingling_ci/src/task/cmd_clippy.rs | 4 | ||||
| -rw-r--r-- | mingling_ci/src/task/cmd_test.rs | 4 | ||||
| -rw-r--r-- | mingling_ci/src/task/run.rs | 30 | ||||
| -rw-r--r-- | mingling_ci/tmpls/task_section.md | 8 |
8 files changed, 120 insertions, 84 deletions
diff --git a/mingling_ci/src/cmd/cmd_report_collect.rs b/mingling_ci/src/cmd/cmd_report_collect.rs index 3cebaef..2eff074 100644 --- a/mingling_ci/src/cmd/cmd_report_collect.rs +++ b/mingling_ci/src/cmd/cmd_report_collect.rs @@ -9,7 +9,7 @@ use mingling::{ use crate::Next; use crate::reporter::{COLLECT_DIR, REPORT_PATH}; -use crate::res::{CargoError, Manifests, MessagePrinter, ResCollectLogs}; +use crate::res::{CargoError, MessagePrinter, ResCollectLogs}; const REPORT_TEMPLATE: &str = include_str!("../../tmpls/report.md"); const TASK_SECTION_TEMPLATE: &str = include_str!("../../tmpls/task_section.md"); @@ -17,26 +17,26 @@ const TASK_SECTION_TEMPLATE: &str = include_str!("../../tmpls/task_section.md"); /// Maps a package to its per-OS pass/fail status. type OsStatuses = BTreeMap<String, bool>; -/// A row in a task section: package name and its per-OS statuses. +/// A row in a task section: item name and its per-OS statuses. type TaskRow<'a> = (&'a String, &'a OsStatuses); /// Rows grouped by task name. type RowsByTask<'a> = BTreeMap<&'a String, Vec<TaskRow<'a>>>; #[command(node = "report-collect")] -pub fn report_collect(manifests: &Manifests, logs: &ResCollectLogs) -> Next { +pub fn report_collect(logs: &ResCollectLogs) -> Next { if !PathBuf::from(COLLECT_DIR).is_dir() { return ErrorNoCollectDir.to_chain(); } - // Group rows by task: task -> [(package, os_statuses)]. - let by_task: RowsByTask = logs.statuses.iter().fold( - BTreeMap::new(), - |mut acc, ((task, package), os_statuses)| { - acc.entry(task).or_default().push((package, os_statuses)); - acc - }, - ); + // Group rows by task: task -> [(item, os_statuses)]. + let by_task: RowsByTask = + logs.statuses + .iter() + .fold(BTreeMap::new(), |mut acc, ((task, item), os_statuses)| { + acc.entry(task).or_default().push((item, os_statuses)); + acc + }); // Render one section per task (table rows + this task's failures). let mut fail_count = 0; @@ -44,10 +44,15 @@ pub fn report_collect(manifests: &Manifests, logs: &ResCollectLogs) -> Next { for (task, rows) in by_task { let mut row_arms = Vec::new(); let mut fail_arms = Vec::new(); - for (package, os_statuses) in rows { + for (item, os_statuses) in rows { + let location = logs + .locations + .get(&(task.clone(), item.clone())) + .cloned() + .unwrap_or_default(); row_arms.push(HashMap::from([ - ("package_name".to_string(), package.clone()), - ("package_dir".to_string(), package_dir(manifests, package)), + ("item_name".to_string(), item.clone()), + ("location".to_string(), location), ( "pass_win".to_string(), pass_cell(os_statuses.get("Windows")), @@ -63,11 +68,11 @@ pub fn report_collect(manifests: &Manifests, logs: &ResCollectLogs) -> Next { if !ok { let stdout = logs .err_outputs - .get(&(task.clone(), os.clone(), package.clone())) + .get(&(task.clone(), os.clone(), item.clone())) .cloned() .unwrap_or_default(); fail_arms.push(HashMap::from([ - ("package_name".to_string(), package.clone()), + ("item_name".to_string(), item.clone()), ("stdout".to_string(), stdout), ])); fail_count += 1; @@ -103,16 +108,6 @@ pub fn report_collect(manifests: &Manifests, logs: &ResCollectLogs) -> Next { ResultCollectResults { output, fail_count }.to_chain() } -/// Maps a package name to its manifest directory (e.g. `mingling` → -/// `./mingling`), or `—` when the manifest is unknown. -fn package_dir(manifests: &Manifests, package: &str) -> String { - manifests - .package_dirs - .get(package) - .and_then(|path| path.parent()) - .map_or_else(|| "—".to_string(), |dir| dir.to_string_lossy().into_owned()) -} - fn pass_cell(status: Option<&bool>) -> String { match status { Some(true) => "✅".to_string(), diff --git a/mingling_ci/src/reporter.rs b/mingling_ci/src/reporter.rs index ef626d9..1a1ac08 100644 --- a/mingling_ci/src/reporter.rs +++ b/mingling_ci/src/reporter.rs @@ -46,8 +46,11 @@ pub enum ReportResult { /// Current task name (e.g. `Build-All`); set via [`set_task`]. static CURRENT_TASK: Mutex<Option<String>> = Mutex::new(None); -/// Successful package names pending a [`flush`], grouped by platform. -static OK_BUFFER: LazyLock<Mutex<HashMap<ReportPlatform, Vec<String>>>> = +/// Pending success entries: `(item, location)`. +type PendingOk = (String, String); + +/// Successful items pending a [`flush`], grouped by platform. +static OK_BUFFER: LazyLock<Mutex<HashMap<ReportPlatform, Vec<PendingOk>>>> = LazyLock::new(|| Mutex::new(HashMap::new())); /// Sets the task that subsequent [`export`] calls write under. @@ -59,17 +62,18 @@ pub fn set_task(task: &str) { *CURRENT_TASK.lock().unwrap() = Some(task.to_string()); } -/// Exports one package result to `collect/{task}/{platform}/`. +/// Exports one item result. /// +/// `item` and `location` are free-form strings chosen by the generator. /// Successes are buffered and written to the `ok` file by [`flush`]; failures -/// write `{package}.err` (with the output) immediately. Errors are reported to -/// stderr and otherwise ignored. +/// write `{task}.{platform}.{item}.err` immediately (first line is the +/// location). Errors are reported to stderr and otherwise ignored. /// /// # Panics /// /// Panics if the internal task mutex is poisoned. -pub fn export(package: &str, result: ReportResult) { - export_on(package, current_platform(), result); +pub fn export(item: &str, location: &str, result: ReportResult) { + export_on(item, location, current_platform(), result); } /// The `ReportPlatform` for the currently compiling target. @@ -83,29 +87,30 @@ fn current_platform() -> ReportPlatform { } } -/// Exports one package result for a specific platform. +/// Exports one item result for a specific platform. /// +/// `item` and `location` are free-form strings chosen by the generator. /// Successes are buffered and written to the `ok` file by [`flush`]; failures -/// write `{package}.err` (with the output) immediately. Errors are reported to -/// stderr and otherwise ignored. +/// write `{task}.{platform}.{item}.err` immediately (first line is the +/// location). Errors are reported to stderr and otherwise ignored. /// /// # Panics /// /// Panics if the internal task mutex is poisoned. -pub fn export_on(package: &str, platform: ReportPlatform, result: ReportResult) { +pub fn export_on(item: &str, location: &str, platform: ReportPlatform, result: ReportResult) { match result { ReportResult::Ok => OK_BUFFER .lock() .unwrap() .entry(platform) .or_default() - .push(package.to_string()), - ReportResult::Error(output) => write_err(package, platform, output), + .push((item.to_string(), location.to_string())), + ReportResult::Error(output) => write_err(item, location, platform, &output), } } -/// Writes buffered successes to `collect/{task}.{platform}.ok`, one package per -/// line. +/// Writes buffered successes to `collect/{task}.{platform}.ok`, one `item` (or +/// `item = location`) per line. /// /// # Panics /// @@ -126,11 +131,21 @@ pub fn flush() { return; } - for (platform, packages) in buffered { - let content = if packages.is_empty() { + for (platform, items) in buffered { + let lines: Vec<String> = items + .iter() + .map(|(item, location)| { + if location.is_empty() { + item.clone() + } else { + format!("{item} = {location}") + } + }) + .collect(); + let content = if lines.is_empty() { String::new() } else { - packages.join("\n") + "\n" + lines.join("\n") + "\n" }; let platform_name = platform.dir_name(); let path = Path::new(COLLECT_DIR).join(format!("{task}.{platform_name}.ok")); @@ -140,8 +155,9 @@ pub fn flush() { } } -/// Writes a failure entry to `collect/{task}.{platform}.{package}.err`. -fn write_err(package: &str, platform: ReportPlatform, output: String) { +/// Writes a failure entry to `collect/{task}.{platform}.{item}.err`, with the +/// location as the first line (empty when unknown). +fn write_err(item: &str, location: &str, platform: ReportPlatform, output: &str) { let Some(task) = CURRENT_TASK.lock().unwrap().clone() else { eprintln!("reporter: no current task; call reporter::set_task first"); return; @@ -153,8 +169,8 @@ fn write_err(package: &str, platform: ReportPlatform, output: String) { } let platform_name = platform.dir_name(); - let path = Path::new(COLLECT_DIR).join(format!("{task}.{platform_name}.{package}.err")); - if let Err(e) = fs::write(&path, output) { + let path = Path::new(COLLECT_DIR).join(format!("{task}.{platform_name}.{item}.err")); + if let Err(e) = fs::write(&path, format!("{location}\n{output}")) { eprintln!("reporter: failed to write {}: {e}", path.display()); } } @@ -173,14 +189,18 @@ mod tests { fs::remove_file(&ok_path).ok(); fs::remove_file(&err_path).ok(); - export("pkg-a", ReportResult::Ok); - export("pkg-b", ReportResult::Error("boom".to_string())); + export("pkg-a", "./pkg-a", ReportResult::Ok); + export("pkg-b", "./pkg-b", ReportResult::Error("boom".to_string())); + export("pkg-c", "", ReportResult::Ok); // no location flush(); assert!(ok_path.is_file()); - assert_eq!(fs::read_to_string(&ok_path).unwrap(), "pkg-a\n"); + assert_eq!( + fs::read_to_string(&ok_path).unwrap(), + "pkg-a = ./pkg-a\npkg-c\n" + ); assert!(err_path.is_file()); - assert_eq!(fs::read_to_string(&err_path).unwrap(), "boom"); + assert_eq!(fs::read_to_string(&err_path).unwrap(), "./pkg-b\nboom"); fs::remove_file(ok_path).ok(); fs::remove_file(err_path).ok(); diff --git a/mingling_ci/src/res/collect_logs.rs b/mingling_ci/src/res/collect_logs.rs index 7c5b375..6017168 100644 --- a/mingling_ci/src/res/collect_logs.rs +++ b/mingling_ci/src/res/collect_logs.rs @@ -18,17 +18,20 @@ pub struct GitInfo { /// Parsed contents of the collect directory. #[derive(Default, Clone)] pub struct ResCollectLogs { - /// `(task, package) -> os -> ok` + /// `(task, item) -> os -> ok` pub statuses: BTreeMap<(String, String), BTreeMap<String, bool>>, - /// `(task, os, package) -> stripped error output` + /// `(task, item) -> location` + pub locations: BTreeMap<(String, String), String>, + /// `(task, os, item) -> stripped error output (location line removed)` pub err_outputs: BTreeMap<(String, String, String), String>, pub git: GitInfo, } impl ResCollectLogs { /// Reads the flat `collect/` directory — aggregate `{task}.{os}.ok` files - /// (one package per line) and per-package `{task}.{os}.{package}.err` - /// files — plus the git info. + /// (`item` or `item = location` per line) and per-item + /// `{task}.{os}.{item}.err` files (first line is the location) — plus the + /// git info. #[must_use] pub fn read() -> Self { let mut logs = Self::default(); @@ -37,23 +40,33 @@ impl ResCollectLogs { for entry in entries.flatten() { let file_name = entry.file_name().to_string_lossy().into_owned(); if let Some((task, os)) = parse_ok_name(&file_name) { - // Aggregate success file: one package name per line. + // Aggregate success file: `item` or `item = location` per line. if let Ok(content) = std::fs::read_to_string(entry.path()) { - for package in content.lines().filter(|l| !l.is_empty()) { + for line in content.lines().filter(|l| !l.is_empty()) { + let (item, location) = line + .split_once('=') + .map_or((line, ""), |(name, loc)| (name.trim(), loc.trim())); logs.statuses - .entry((task.clone(), package.to_string())) + .entry((task.clone(), item.to_string())) .or_default() .insert(os.clone(), true); + logs.locations + .insert((task.clone(), item.to_string()), location.to_string()); } } - } else if let Some((task, os, package)) = parse_err_name(&file_name) { + } else if let Some((task, os, item)) = parse_err_name(&file_name) { + let content = std::fs::read_to_string(entry.path()).unwrap_or_default(); + let mut lines = content.splitn(2, '\n'); + let location = lines.next().unwrap_or_default().to_string(); + let output = lines.next().unwrap_or_default().to_string(); logs.statuses - .entry((task.clone(), package.clone())) + .entry((task.clone(), item.clone())) .or_default() .insert(os.clone(), false); - let err = std::fs::read_to_string(entry.path()).unwrap_or_default(); + logs.locations + .insert((task.clone(), item.clone()), location); logs.err_outputs - .insert((task, os, package), strip_ansi(&err)); + .insert((task, os, item), strip_ansi(&output)); } } } diff --git a/mingling_ci/src/task/cmd_build.rs b/mingling_ci/src/task/cmd_build.rs index a74c323..f37699c 100644 --- a/mingling_ci/src/task/cmd_build.rs +++ b/mingling_ci/src/task/cmd_build.rs @@ -9,14 +9,14 @@ use mingling::{ use crate::Next; use crate::res::Manifests; -use crate::task::run::run_parallel_checks; +use crate::task::run::{location, run_parallel_checks}; #[command(node = "build-all")] pub async fn build_all(manifests: &Manifests) -> Next { let tasks = manifests .package_dirs .iter() - .map(|(name, path)| (name.clone(), build_args(path))) + .map(|(name, path)| (name.clone(), location(path), build_args(path))) .collect(); let fail_count = run_parallel_checks("Build-All", "Building", tasks).await; ResultBuildAll { fail_count }.to_chain() diff --git a/mingling_ci/src/task/cmd_clippy.rs b/mingling_ci/src/task/cmd_clippy.rs index 0a4282b..7256bef 100644 --- a/mingling_ci/src/task/cmd_clippy.rs +++ b/mingling_ci/src/task/cmd_clippy.rs @@ -9,14 +9,14 @@ use mingling::{ use crate::Next; use crate::res::Manifests; -use crate::task::run::run_parallel_checks; +use crate::task::run::{location, run_parallel_checks}; #[command(node = "clippy-all")] pub async fn clippy_all(manifests: &Manifests) -> Next { let tasks = manifests .package_dirs .iter() - .map(|(name, path)| (name.clone(), clippy_args(path))) + .map(|(name, path)| (name.clone(), location(path), clippy_args(path))) .collect(); let fail_count = run_parallel_checks("Clippy-All", "Clippy", tasks).await; ResultClippyAll { fail_count }.to_chain() diff --git a/mingling_ci/src/task/cmd_test.rs b/mingling_ci/src/task/cmd_test.rs index 3ed193c..5b9f55a 100644 --- a/mingling_ci/src/task/cmd_test.rs +++ b/mingling_ci/src/task/cmd_test.rs @@ -9,7 +9,7 @@ use mingling::{ use crate::Next; use crate::res::{Manifests, ResCrateConfig}; -use crate::task::run::run_parallel_checks; +use crate::task::run::{location, run_parallel_checks}; #[command(node = "test-all")] pub async fn test_all(manifests: &Manifests, config: &ResCrateConfig) -> Next { @@ -21,7 +21,7 @@ pub async fn test_all(manifests: &Manifests, config: &ResCrateConfig) -> Next { || test_args(path), |cmd| cmd.iter().map(|s| OsString::from(s.as_str())).collect(), ); - (name.clone(), args) + (name.clone(), location(path), args) }) .collect(); let fail_count = run_parallel_checks("Test-All", "Testing", tasks).await; diff --git a/mingling_ci/src/task/run.rs b/mingling_ci/src/task/run.rs index d00334d..bf83d3b 100644 --- a/mingling_ci/src/task/run.rs +++ b/mingling_ci/src/task/run.rs @@ -1,10 +1,18 @@ use std::ffi::OsString; +use std::path::Path; use colored::Colorize; use indicatif::{ProgressBar, ProgressStyle}; use crate::reporter::{self, ReportResult}; +/// The manifest's parent directory, e.g. `./mingling` — the report location +/// for a crate-based item. +pub(crate) fn location(path: &Path) -> String { + path.parent() + .map_or_else(|| ".".to_string(), |d| d.to_string_lossy().into_owned()) +} + /// Outcome of a `cargo` subcommand. struct CargoResult { ok: bool, @@ -14,13 +22,13 @@ struct CargoResult { /// Runs the given cargo task list in parallel. /// -/// 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. +/// Each task is an `(item, location, argv)` triple; 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, - tasks: Vec<(String, Vec<OsString>)>, + tasks: Vec<(String, String, Vec<OsString>)>, ) -> usize { reporter::set_task(task); @@ -39,20 +47,20 @@ pub(crate) async fn run_parallel_checks( // Run each task in parallel. let mut set = tokio::task::JoinSet::new(); - for (name, args) in tasks { - set.spawn(async move { (name, run_cargo(args).await) }); + for (item, location, args) in tasks { + set.spawn(async move { (item, location, run_cargo(args).await) }); } let mut fail_count = 0; while let Some(joined) = set.join_next().await { - let Ok((name, result)) = joined else { + let Ok((item, location, result)) = joined else { continue; }; pb.inc(1); - pb.set_message(name.clone()); + pb.set_message(item.clone()); if result.ok { - reporter::export(&name, ReportResult::Ok); + reporter::export(&item, &location, ReportResult::Ok); } else { fail_count += 1; // Failures print to stderr immediately (bar suspended to avoid @@ -61,7 +69,7 @@ pub(crate) async fn run_parallel_checks( eprintln!( "{}: {} failed{}", phase.bold().bright_cyan(), - name, + item, result .exit_code .map_or_else(String::new, |c| format!(" (exit code {c})")) @@ -70,7 +78,7 @@ pub(crate) async fn run_parallel_checks( eprintln!(" {line}"); } }); - reporter::export(&name, ReportResult::Error(result.output)); + reporter::export(&item, &location, ReportResult::Error(result.output)); } } diff --git a/mingling_ci/tmpls/task_section.md b/mingling_ci/tmpls/task_section.md index 9c6daad..78c9801 100644 --- a/mingling_ci/tmpls/task_section.md +++ b/mingling_ci/tmpls/task_section.md @@ -1,15 +1,15 @@ ## Task: <<<task_name>>> -| Package-Name | Package-Directory | PASS (Windows) | PASS (Linux) | PASS (Mac OS) | -| -------------- | ----------------- | -------------- | ------------ | ------------- | +| Item-Name | Location | PASS (Windows) | PASS (Linux) | PASS (Mac OS) | +| ----------- | -------- | -------------- | ------------ | ------------- | >>>>>>>>>> rows @@@ >>> rows -| <<<package_name>>> | <<<package_dir>>> | <<<pass_win>>> | <<<pass_linux>>> | <<<pass_mac>>> | +| <<<item_name>>> | <<<location>>> | <<<pass_win>>> | <<<pass_linux>>> | <<<pass_mac>>> | @@@ <<< >>>>>>>>>> fails @@@ >>> fails -### Fail: #<<<package_name>>> +### Fail: <<<item_name>>> ```stdout <<<stdout>>> |
