diff options
| author | Weicao-CatilGrass <1992414357@qq.com> | 2026-06-12 14:41:46 +0800 |
|---|---|---|
| committer | Weicao-CatilGrass <1992414357@qq.com> | 2026-06-12 14:41:46 +0800 |
| commit | 93cc9d549c63f65b4fc424c53a7be9d66f00d117 (patch) | |
| tree | 49bce79fe24e76b8f964f4fe65623d3d3ae1ea68 /dev_tools/src | |
| parent | d8b87ec524b0d80ac7815a4675f7bd71a4951410 (diff) | |
Test markdown code blocks with dependency caching
Diffstat (limited to 'dev_tools/src')
| -rw-r--r-- | dev_tools/src/bin/test-all-markdown-code.rs | 68 | ||||
| -rw-r--r-- | dev_tools/src/verify.rs | 90 |
2 files changed, 120 insertions, 38 deletions
diff --git a/dev_tools/src/bin/test-all-markdown-code.rs b/dev_tools/src/bin/test-all-markdown-code.rs index c4e89be..500e66a 100644 --- a/dev_tools/src/bin/test-all-markdown-code.rs +++ b/dev_tools/src/bin/test-all-markdown-code.rs @@ -1,9 +1,10 @@ +use std::collections::HashMap; use std::path::{Path, PathBuf}; use colored::Colorize; use tools::verify::{ - build_block, generate_cargo_toml, generate_main_rs, is_block_testable, parse_code_blocks, - write_summary_report, + build_block, compute_block_hash, generate_cargo_toml, generate_main_rs, is_block_testable, + parse_code_blocks, write_summary_report, }; use tools::{eprintln_cargo_style, println_cargo_style}; @@ -78,11 +79,8 @@ fn main() { std::process::exit(1); } - // Ensure temp directory exists - let temp_dir = PathBuf::from(".temp/docs-test"); - let _ = std::fs::remove_dir_all(&temp_dir); - - let mut all_blocks: Vec<(String, Vec<tools::verify::CodeBlock>)> = Vec::new(); + // Parse all code blocks into a flat list with global indices + let mut flat_blocks: Vec<(usize, tools::verify::CodeBlock)> = Vec::new(); for (label, path) in &files { let content = std::fs::read_to_string(path).unwrap_or_else(|e| { @@ -92,12 +90,13 @@ fn main() { let source_file = format!("{label}/{}", path.file_name().unwrap().to_string_lossy()); let blocks = parse_code_blocks(&content, &source_file); let testable: Vec<_> = blocks.into_iter().filter(is_block_testable).collect(); - if !testable.is_empty() { - all_blocks.push((label.clone(), testable)); + for block in testable { + let idx = flat_blocks.len() + 1; // 1-based global index + flat_blocks.push((idx, block)); } } - let total_testable: usize = all_blocks.iter().map(|(_, b)| b.len()).sum(); + let total_testable = flat_blocks.len(); if total_testable == 0 { println_cargo_style!("No testable code blocks found"); @@ -106,32 +105,47 @@ fn main() { println_cargo_style!( "Test: found {total_testable} testable code blocks across {} files", - all_blocks.len() + files.len() ); - // Build each block - let mut block_index = 0usize; + // Group blocks by dependency hash + let mut groups: HashMap<String, Vec<(usize, tools::verify::CodeBlock)>> = HashMap::new(); + for (idx, block) in flat_blocks { + let hash = compute_block_hash(&block); + groups.entry(hash).or_default().push((idx, block)); + } + + println_cargo_style!( + "Cache: grouped into {} unique dependency configurations", + groups.len() + ); + + let temp_base = PathBuf::from(".temp/doc-test"); + + // Build groups — same hash reuses the same Cargo.toml + let mut results: Vec<(String, usize, bool, String)> = Vec::new(); let mut passed = 0usize; let mut failed = 0usize; - let mut results: Vec<(String, usize, bool, String)> = Vec::new(); - for (_label, blocks) in &all_blocks { - for block in blocks { - block_index += 1; + // Sort groups by hash for deterministic output order + let mut group_keys: Vec<&String> = groups.keys().collect(); + group_keys.sort(); - let block_label = format!( - "Block {} ({}:{})", - block_index, block.source_file, block.line - ); - print!(" Testing {block_label} ... "); + for hash in group_keys { + let blocks = &groups[hash]; + let crate_dir = temp_base.join(hash); + let src_dir = crate_dir.join("src"); + let manifest_path = crate_dir.join("Cargo.toml"); - let package_name = format!("test-block-{block_index}"); + // Generate a single Cargo.toml for the whole group (all blocks share same deps) + let first_block = &blocks[0].1; + let cargo_toml = generate_cargo_toml(first_block, "test-doc", &manifest_path); - let cargo_toml = generate_cargo_toml(block, &package_name); - let main_rs = generate_main_rs(block); - let src_dir = temp_dir.join("src"); - let manifest_path = temp_dir.join("Cargo.toml"); + for (block_idx, block) in blocks { + let block_label = format!("Block {block_idx} ({}:{})", block.source_file, block.line); + print!(" Testing {block_label} ... "); + let main_rs = generate_main_rs(block); let (ok, err) = build_block(&src_dir, &manifest_path, &cargo_toml, &main_rs); if ok { println!("{}", "passed".bold().bright_green()); diff --git a/dev_tools/src/verify.rs b/dev_tools/src/verify.rs index e94226c..23de19d 100644 --- a/dev_tools/src/verify.rs +++ b/dev_tools/src/verify.rs @@ -151,7 +151,10 @@ fn parse_single_block(lines: &[&str], start: usize, source_file: &str) -> Option } /// Generate a Cargo.toml for a block -pub fn generate_cargo_toml(block: &CodeBlock, package_name: &str) -> String { +/// +/// `manifest_path` is the full path to the Cargo.toml file being written; it is used to +/// compute the relative path to the `mingling` crate. +pub fn generate_cargo_toml(block: &CodeBlock, package_name: &str, manifest_path: &Path) -> String { let features_str = if !block.features.is_empty() { let feats: Vec<String> = block.features.iter().map(|f| format!("\"{f}\"")).collect(); format!("features = [{}]", feats.join(", ")) @@ -176,15 +179,13 @@ pub fn generate_cargo_toml(block: &CodeBlock, package_name: &str) -> String { } } + let mingling_path = find_mingling_relative_path(manifest_path); + let deps_section = if features_str.is_empty() { - format!( - "[dependencies]\nmingling = {{ path = \"{}\" }}\n{extra_deps}", - find_mingling_relative_path() - ) + format!("[dependencies]\nmingling = {{ path = \"{mingling_path}\" }}\n{extra_deps}",) } else { format!( - "[dependencies]\nmingling = {{ path = \"{}\", {features_str} }}\n{extra_deps}", - find_mingling_relative_path() + "[dependencies]\nmingling = {{ path = \"{mingling_path}\", {features_str} }}\n{extra_deps}", ) }; @@ -200,10 +201,26 @@ edition = "2024" ) } -/// Find the relative path from the temp test directory to mingling crate -fn find_mingling_relative_path() -> &'static str { - // Tests run from project root, temp is under .temp/ - "../../mingling" +/// Compute the relative path from a Cargo.toml's parent directory to the `mingling` crate. +/// +/// The process current directory is expected to be the project root (where `mingling/` lives). +/// Returns a forward-slash path safe for embedding in TOML strings. +fn find_mingling_relative_path(manifest_path: &Path) -> String { + let manifest_dir = manifest_path + .parent() + .expect("manifest_path has no parent directory"); + let cwd = std::env::current_dir().expect("failed to get current directory"); + + // Strip cwd prefix to get the relative components of the manifest directory + let relative_to_root = manifest_dir.strip_prefix(&cwd).unwrap_or(manifest_dir); + let depth = relative_to_root.components().count(); + + let mut result = String::new(); + for _ in 0..depth { + result.push_str("../"); + } + result.push_str("mingling"); + result } /// Generate main.rs for a block @@ -261,6 +278,57 @@ pub fn build_block( } } +/// Compute a stable hash for a code block based on its dependency configuration. +/// +/// Blocks with the same features and external dependencies produce the same hash, +/// allowing them to share a compiled crate and avoid redundant recompilation. +/// +/// Hash input (all sorted for stability): +/// - Sorted mingling feature strings +/// - Sorted external dependency names +/// - Sorted external dependency versions +/// - Sorted external deps as `name=version` pairs +pub fn compute_block_hash(block: &CodeBlock) -> String { + let mut features: Vec<&str> = block.features.iter().map(|s| s.as_str()).collect(); + features.sort(); + let features_str = features.join(","); + + let mut dep_names: Vec<&str> = block + .external_deps + .iter() + .map(|(n, _)| n.as_str()) + .collect(); + dep_names.sort(); + let dep_names_str = dep_names.join(","); + + let mut dep_versions: Vec<&str> = block + .external_deps + .iter() + .map(|(_, v)| v.as_str()) + .collect(); + dep_versions.sort(); + let dep_versions_str = dep_versions.join(","); + + let mut deps: Vec<String> = block + .external_deps + .iter() + .map(|(n, v)| format!("{n}={v}")) + .collect(); + deps.sort(); + let deps_str = deps.join(","); + + let canonical = format!("{features_str}\n{dep_names_str}\n{dep_versions_str}\n{deps_str}"); + + // FNV-1a 64-bit hash — stable across runs (no random seed) + let mut hash: u64 = 0xcbf29ce484222325; + for &byte in canonical.as_bytes() { + hash ^= byte as u64; + hash = hash.wrapping_mul(0x100000001b3); + } + + format!("{:016x}", hash) +} + /// Determine if a block should be treated as a test candidate. /// A block is NOT testable only if it has `// NOT VERIFIED` marker. pub fn is_block_testable(block: &CodeBlock) -> bool { |
