aboutsummaryrefslogtreecommitdiff
path: root/dev/ci/src/reporter.rs
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-19 05:54:00 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-19 06:15:20 +0800
commitec9edc294fd5e7e29977fc7b0e6fb953422bc0e2 (patch)
treebbf89ef4457b8de8a8a9bca5668045d612d27572 /dev/ci/src/reporter.rs
parent06dfc27194c11e1d1033c292c759a1c5d82e780b (diff)
chore: reorganize dev tools into dev/ directory and consolidate configsHEADmain
Move CI, dev tools, and configs from root-level scattered locations into a unified `dev/` directory structure. Update all references across build scripts, documentation, and editor configurations. Also consolidate editor config generation into build.rs for automated synchronization of rust-analyzer settings across VS Code and Zed.
Diffstat (limited to 'dev/ci/src/reporter.rs')
-rw-r--r--dev/ci/src/reporter.rs208
1 files changed, 208 insertions, 0 deletions
diff --git a/dev/ci/src/reporter.rs b/dev/ci/src/reporter.rs
new file mode 100644
index 0000000..1a1ac08
--- /dev/null
+++ b/dev/ci/src/reporter.rs
@@ -0,0 +1,208 @@
+//! Minimal log exporter for CI reports.
+//!
+//! Writes per-package results into `collect/{task}/{platform}/{package}.{ok|err}`
+//! so that the [`crate::cmd::collect_results`] command can assemble the final
+//! report. The task name is set once per CI phase via [`set_task`].
+
+use std::collections::HashMap;
+use std::fs;
+use std::path::Path;
+use std::sync::{LazyLock, Mutex};
+
+/// Root of the collected CI logs (relative to the repo root).
+pub const COLLECT_DIR: &str = "./.temp/reports/collect";
+
+/// Generated report output (relative to the repo root).
+pub const REPORT_PATH: &str = "./.temp/reports/result.md";
+
+/// The platform a package check ran on.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
+pub enum ReportPlatform {
+ Windows,
+ Linux,
+ MacOS,
+}
+
+impl ReportPlatform {
+ /// Directory name used under the task folder.
+ const fn dir_name(self) -> &'static str {
+ match self {
+ Self::Windows => "Windows",
+ Self::Linux => "Linux",
+ Self::MacOS => "MacOS",
+ }
+ }
+}
+
+/// The outcome of a package check.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ReportResult {
+ /// Check passed.
+ Ok,
+ /// Check failed, with the captured output.
+ Error(String),
+}
+
+/// Current task name (e.g. `Build-All`); set via [`set_task`].
+static CURRENT_TASK: Mutex<Option<String>> = Mutex::new(None);
+
+/// 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.
+///
+/// # Panics
+///
+/// Panics if the internal mutex is poisoned.
+pub fn set_task(task: &str) {
+ *CURRENT_TASK.lock().unwrap() = Some(task.to_string());
+}
+
+/// 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 `{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(item: &str, location: &str, result: ReportResult) {
+ export_on(item, location, current_platform(), result);
+}
+
+/// The `ReportPlatform` for the currently compiling target.
+fn current_platform() -> ReportPlatform {
+ if cfg!(target_os = "windows") {
+ ReportPlatform::Windows
+ } else if cfg!(target_os = "macos") {
+ ReportPlatform::MacOS
+ } else {
+ ReportPlatform::Linux
+ }
+}
+
+/// 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 `{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(item: &str, location: &str, platform: ReportPlatform, result: ReportResult) {
+ match result {
+ ReportResult::Ok => OK_BUFFER
+ .lock()
+ .unwrap()
+ .entry(platform)
+ .or_default()
+ .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 `item` (or
+/// `item = location`) per line.
+///
+/// # Panics
+///
+/// Panics if the internal task mutex is poisoned.
+pub fn flush() {
+ let Some(task) = CURRENT_TASK.lock().unwrap().clone() else {
+ eprintln!("reporter: no current task; call reporter::set_task first");
+ return;
+ };
+
+ let buffered = std::mem::take(&mut *OK_BUFFER.lock().unwrap());
+ if buffered.is_empty() {
+ return;
+ }
+
+ if let Err(e) = fs::create_dir_all(COLLECT_DIR) {
+ eprintln!("reporter: failed to create {COLLECT_DIR}: {e}");
+ return;
+ }
+
+ 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 {
+ lines.join("\n") + "\n"
+ };
+ let platform_name = platform.dir_name();
+ let path = Path::new(COLLECT_DIR).join(format!("{task}.{platform_name}.ok"));
+ if let Err(e) = fs::write(&path, content) {
+ eprintln!("reporter: failed to write {}: {e}", path.display());
+ }
+ }
+}
+
+/// 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;
+ };
+
+ if let Err(e) = fs::create_dir_all(COLLECT_DIR) {
+ eprintln!("reporter: failed to create {COLLECT_DIR}: {e}");
+ return;
+ }
+
+ let platform_name = platform.dir_name();
+ 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());
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn export_writes_ok_and_err_files() {
+ set_task("reporter-test");
+ let platform_name = current_platform().dir_name();
+ let ok_path = Path::new(COLLECT_DIR).join(format!("reporter-test.{platform_name}.ok"));
+ let err_path =
+ Path::new(COLLECT_DIR).join(format!("reporter-test.{platform_name}.pkg-b.err"));
+ fs::remove_file(&ok_path).ok();
+ fs::remove_file(&err_path).ok();
+
+ 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 = ./pkg-a\npkg-c\n"
+ );
+ assert!(err_path.is_file());
+ assert_eq!(fs::read_to_string(&err_path).unwrap(), "./pkg-b\nboom");
+
+ fs::remove_file(ok_path).ok();
+ fs::remove_file(err_path).ok();
+ }
+}