aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-18 10:13:39 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-18 10:13:39 +0800
commit8ea8e13f1a6b2a2942b78127e23d6783c5188ca5 (patch)
tree01cc8dde8cb8c90529c8d5f66dc57a08ed7cdc26
parent570b2bc1710c0ab8a68ad69a6121144e4f5e3aca (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.rs47
-rw-r--r--mingling_ci/src/reporter.rs72
-rw-r--r--mingling_ci/src/res/collect_logs.rs35
-rw-r--r--mingling_ci/src/task/cmd_build.rs4
-rw-r--r--mingling_ci/src/task/cmd_clippy.rs4
-rw-r--r--mingling_ci/src/task/cmd_test.rs4
-rw-r--r--mingling_ci/src/task/run.rs30
-rw-r--r--mingling_ci/tmpls/task_section.md8
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>>>