From 9af35a623e4559d81fc69b39644fad0d55e0947e Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sun, 17 May 2026 15:10:48 +0800 Subject: [PATCH] fix(test): accept book-root paths for --chapter filter `mdbook test --chapter` now matches chapter titles, paths relative to SUMMARY.md, and paths prefixed with the book `src` directory (e.g. `src/chapter_1.md` from shell tab completion). Fixes #3009 Co-authored-by: Cursor --- crates/mdbook-driver/src/mdbook.rs | 51 ++++++++++++++++++++++-- crates/mdbook-driver/src/mdbook/tests.rs | 18 +++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/crates/mdbook-driver/src/mdbook.rs b/crates/mdbook-driver/src/mdbook.rs index 45bae41816..c21098b58f 100644 --- a/crates/mdbook-driver/src/mdbook.rs +++ b/crates/mdbook-driver/src/mdbook.rs @@ -6,7 +6,7 @@ use crate::init::BookBuilder; use crate::load::{load_book, load_book_from_disk}; use anyhow::{Context, Error, Result, bail}; use indexmap::IndexMap; -use mdbook_core::book::{Book, BookItem, BookItems}; +use mdbook_core::book::{Book, BookItem, BookItems, Chapter}; use mdbook_core::config::{Config, RustEdition}; use mdbook_core::utils::fs; use mdbook_html::HtmlHandlebars; @@ -274,9 +274,9 @@ impl MDBook { _ => continue, }; - if let Some(chapter) = chapter { - if ch.name != chapter && chapter_path.to_str() != Some(chapter) { - if chapter == "?" { + if let Some(filter) = chapter { + if !chapter_matches_filter(filter, ch, &self.config.book.src) { + if filter == "?" { info!("Skipping chapter '{}'...", ch.name); } continue; @@ -399,6 +399,49 @@ struct OutputConfig { } /// Look at the `Config` and try to figure out what renderers to use. +/// Whether `filter` from `mdbook test --chapter` matches `ch`. +fn chapter_matches_filter(filter: &str, ch: &Chapter, book_src: &Path) -> bool { + if ch.name == filter { + return true; + } + + let Some(path) = ch.path.as_ref().filter(|p| !p.as_os_str().is_empty()) else { + return false; + }; + + if path.to_string_lossy() == filter { + return true; + } + + if ch + .source_path + .as_ref() + .is_some_and(|source| source.to_string_lossy() == filter) + { + return true; + } + + let filter_norm = normalize_chapter_path(filter); + if filter_norm == normalize_chapter_path(path) { + return true; + } + + let via_src = book_src.join(path); + if filter_norm == normalize_chapter_path(&via_src) { + return true; + } + + false +} + +fn normalize_chapter_path(path: impl AsRef) -> String { + path.as_ref() + .to_string_lossy() + .replace('\\', "/") + .trim_start_matches("./") + .to_string() +} + fn determine_renderers(config: &Config) -> Result>> { let mut renderers = IndexMap::new(); diff --git a/crates/mdbook-driver/src/mdbook/tests.rs b/crates/mdbook-driver/src/mdbook/tests.rs index f4a37072ac..cef30e550d 100644 --- a/crates/mdbook-driver/src/mdbook/tests.rs +++ b/crates/mdbook-driver/src/mdbook/tests.rs @@ -1,7 +1,25 @@ use super::*; +use mdbook_core::book::Chapter; +use std::path::Path; use std::str::FromStr; use toml::value::{Table, Value}; +#[test] +fn chapter_matches_filter_accepts_src_prefixed_paths() { + let ch = Chapter::new( + "Intro", + String::new(), + Path::new("chapter_1.md"), + Vec::new(), + ); + let src = Path::new("src"); + + assert!(chapter_matches_filter("Intro", &ch, src)); + assert!(chapter_matches_filter("chapter_1.md", &ch, src)); + assert!(chapter_matches_filter("src/chapter_1.md", &ch, src)); + assert!(!chapter_matches_filter("other.md", &ch, src)); +} + #[test] fn config_defaults_to_html_renderer_if_empty() { let cfg = Config::default();