diff options
Diffstat (limited to 'mingling_ci/src')
| -rw-r--r-- | mingling_ci/src/cmd/cmd_markdown_check.rs | 76 | ||||
| -rw-r--r-- | mingling_ci/src/markdown/test.rs | 34 |
2 files changed, 95 insertions, 15 deletions
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`. |
