diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-08-18 10:36:53 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-08-18 10:36:53 +0800 |
| commit | b5941040586c3707620bbef6cf8ace4e565f7693 (patch) | |
| tree | b09ff500bb187328941372b1a79381afb0123e4f | |
| parent | 721536eae99d468299c6d7f5ecd6b7dbeac4c34a (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.lock | 1 | ||||
| -rw-r--r-- | mingling_ci/Cargo.toml | 1 | ||||
| -rw-r--r-- | mingling_ci/src/cmd/cmd_markdown_check.rs | 76 | ||||
| -rw-r--r-- | mingling_ci/src/markdown/test.rs | 34 |
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`. |
