aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--dev_tools/src/bin/test-all-markdown-code.rs68
-rw-r--r--dev_tools/src/verify.rs90
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 {