aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-18 10:36:53 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-18 10:36:53 +0800
commitb5941040586c3707620bbef6cf8ace4e565f7693 (patch)
treeb09ff500bb187328941372b1a79381afb0123e4f
parent721536eae99d468299c6d7f5ecd6b7dbeac4c34a (diff)
feat(ci-new): report per-file results
Add per-file report generation for markdown checks using the reporter module, grouping outcomes by source file and exporting ok or error results with snake_case item names.
-rw-r--r--mingling_ci/Cargo.lock1
-rw-r--r--mingling_ci/Cargo.toml1
-rw-r--r--mingling_ci/src/cmd/cmd_markdown_check.rs76
-rw-r--r--mingling_ci/src/markdown/test.rs34
4 files changed, 97 insertions, 15 deletions
diff --git a/mingling_ci/Cargo.lock b/mingling_ci/Cargo.lock
index 538b06c..1a86bbf 100644
--- a/mingling_ci/Cargo.lock
+++ b/mingling_ci/Cargo.lock
@@ -322,6 +322,7 @@ version = "0.1.0"
dependencies = [
"colored",
"indicatif",
+ "just_fmt 0.2.1",
"just_progress",
"just_template",
"mingling",
diff --git a/mingling_ci/Cargo.toml b/mingling_ci/Cargo.toml
index 5de752a..7c747db 100644
--- a/mingling_ci/Cargo.toml
+++ b/mingling_ci/Cargo.toml
@@ -35,6 +35,7 @@ tokio = { version = "1.53.1", features = [
prettytable-rs = "0.10.0"
toml = "0.8"
just_template = "0.2.1"
+just_fmt = "0.2.1"
[build-dependencies]
mingling = { version = "0.4.0", features = [
diff --git a/mingling_ci/src/cmd/cmd_markdown_check.rs b/mingling_ci/src/cmd/cmd_markdown_check.rs
index c153832..6044846 100644
--- a/mingling_ci/src/cmd/cmd_markdown_check.rs
+++ b/mingling_ci/src/cmd/cmd_markdown_check.rs
@@ -1,5 +1,7 @@
+use std::collections::HashMap;
use std::path::{Path, PathBuf};
+use just_fmt::snake_case;
use mingling::{
Grouped, RenderResult, Routable,
macros::{buffer, command, renderer},
@@ -8,7 +10,8 @@ use mingling::{
use crate::Next;
use crate::markdown::project::parse_markdown;
-use crate::markdown::test::try_test_markdown_project;
+use crate::markdown::test::{MarkdownBlockOutcome, try_test_markdown_project};
+use crate::reporter::{self, ReportResult};
use crate::res::{CargoError, MessagePrinter};
const VERIFIED_DOCS: &str = ".config/verified-docs.toml";
@@ -23,12 +26,19 @@ pub async fn markdown_check(args: Vec<String>) -> Next {
if !path.is_file() {
return ErrorMarkdownArgs(format!("{} is not a file", path.display())).to_chain();
}
-
let Ok(content) = std::fs::read_to_string(&path) else {
return ErrorMarkdownArgs(format!("failed to read {}", path.display())).to_chain();
};
- let projects = parse_markdown(&content, &path.to_string_lossy());
- let fail_count = try_test_markdown_project(projects).await;
+
+ let location = path.to_string_lossy().into_owned();
+ let item = format!("doc-{}", snake_case!(&stem_of(&path)));
+ reporter::set_task("Markdown-Check");
+
+ let projects = parse_markdown(&content, &location);
+ let outcomes = try_test_markdown_project(projects).await;
+ let file_info = HashMap::from([(location.clone(), (item, location))]);
+ let fail_count = report_files(&outcomes, &file_info);
+ reporter::flush();
ResultMarkdownCheck { fail_count }.to_chain()
}
@@ -38,20 +48,74 @@ pub async fn markdown_check_all() -> Next {
let Some(files) = verified_md_files() else {
return ErrorMarkdownConfig.to_chain();
};
+ reporter::set_task("Markdown-Check-All");
+ // Collect all projects; remember each file's report identity
+ // (`{key}-{snake_case(file_stem)}` -> location).
let mut projects = Vec::new();
+ let mut file_info: HashMap<String, (String, String)> = HashMap::new();
for (label, path) in files {
let Ok(content) = std::fs::read_to_string(&path) else {
continue;
};
- let source_file = format!("{label}/{}", path.file_name().unwrap().to_string_lossy());
+ let file_name = path.file_name().unwrap().to_string_lossy();
+ let source_file = format!("{label}/{file_name}");
+ let item = format!("{label}-{}", snake_case!(&stem_of(&path)));
+ let location = path.to_string_lossy().into_owned();
+ file_info.insert(source_file.clone(), (item, location));
projects.extend(parse_markdown(&content, &source_file));
}
- let fail_count = try_test_markdown_project(projects).await;
+
+ let outcomes = try_test_markdown_project(projects).await;
+ let fail_count = report_files(&outcomes, &file_info);
+ reporter::flush();
ResultMarkdownCheck { fail_count }.to_chain()
}
+/// The file name without extension, e.g. `README.md` → `README`.
+fn stem_of(path: &Path) -> String {
+ path.file_stem()
+ .unwrap_or_default()
+ .to_string_lossy()
+ .into_owned()
+}
+
+/// Exports one report entry per source file: `ok` when every block passed,
+/// otherwise an error carrying the failed blocks' details.
+fn report_files(
+ outcomes: &[MarkdownBlockOutcome],
+ file_info: &HashMap<String, (String, String)>,
+) -> usize {
+ let mut by_file: HashMap<&str, (bool, Vec<String>)> = HashMap::new();
+ for outcome in outcomes {
+ let (ok, outputs) = by_file
+ .entry(outcome.source_file.as_str())
+ .or_insert((true, Vec::new()));
+ if !outcome.ok {
+ *ok = false;
+ outputs.push(format!(
+ "{}:{}:\n{}",
+ outcome.source_file, outcome.line, outcome.output
+ ));
+ }
+ }
+
+ let mut fail_count = 0;
+ for (source_file, (ok, outputs)) in by_file {
+ let Some((item, location)) = file_info.get(source_file) else {
+ continue;
+ };
+ if ok {
+ reporter::export(item, location, ReportResult::Ok);
+ } else {
+ fail_count += outputs.len();
+ reporter::export(item, location, ReportResult::Error(outputs.join("\n\n")));
+ }
+ }
+ fail_count
+}
+
/// Reads `verified-docs.toml` and collects all `.md` files: single files,
/// directories, or `**` globs (walked from the base directory).
fn verified_md_files() -> Option<Vec<(String, PathBuf)>> {
diff --git a/mingling_ci/src/markdown/test.rs b/mingling_ci/src/markdown/test.rs
index c010e8c..f8bd1f7 100644
--- a/mingling_ci/src/markdown/test.rs
+++ b/mingling_ci/src/markdown/test.rs
@@ -13,12 +13,23 @@ use super::project::{
/// Temporary root for the generated test crates.
const TEMP_BASE: &str = ".temp/doc-test";
+/// Outcome of testing one code block.
+pub(crate) struct MarkdownBlockOutcome {
+ pub source_file: String,
+ pub line: usize,
+ pub ok: bool,
+ /// Failure detail; empty when `ok`.
+ pub output: String,
+}
+
/// Runs the given projects in parallel.
///
/// Projects sharing a dependency hash share one temporary crate (written
/// serially within the group); groups run in parallel. Progress is shown on
-/// stderr; failures print immediately. Returns the number of failed blocks.
-pub(crate) async fn try_test_markdown_project(projs: Vec<MarkdownTestProject>) -> usize {
+/// stderr; failures print there too. Returns one outcome per block.
+pub(crate) async fn try_test_markdown_project(
+ projs: Vec<MarkdownTestProject>,
+) -> Vec<MarkdownBlockOutcome> {
// Group by dependency hash for crate sharing.
let mut groups: BTreeMap<String, Vec<MarkdownTestProject>> = BTreeMap::new();
for proj in projs {
@@ -49,7 +60,7 @@ pub(crate) async fn try_test_markdown_project(projs: Vec<MarkdownTestProject>) -
let manifest_path = crate_dir.join("Cargo.toml");
let cargo_toml = generate_cargo_toml(&blocks[0], &manifest_path);
- let mut failed = 0;
+ let mut group_outcomes = Vec::new();
for proj in &blocks {
let label = format!("{}:{}", proj.source_file, proj.line);
pb.set_message(label.clone());
@@ -69,25 +80,30 @@ pub(crate) async fn try_test_markdown_project(projs: Vec<MarkdownTestProject>) -
pb.inc(1);
if !ok {
- failed += 1;
// Plain stderr: `pb.println` is swallowed on non-TTY (CI).
eprintln!(" {} {label}", "failed".bold().bright_red());
eprintln!(" {label} FAILED:\n{err}");
}
+ group_outcomes.push(MarkdownBlockOutcome {
+ source_file: proj.source_file.clone(),
+ line: proj.line,
+ ok,
+ output: err,
+ });
}
- failed
+ group_outcomes
}));
}
- let mut fail_count = 0;
+ let mut all_outcomes = Vec::new();
for handle in handles {
- if let Ok(failed) = handle.await {
- fail_count += failed;
+ if let Ok(group_outcomes) = handle.await {
+ all_outcomes.extend(group_outcomes);
}
}
pb.finish_and_clear();
- fail_count
+ all_outcomes
}
/// Writes the temporary crate files and runs `cargo check`.