|
1 | 1 | //! Tests for custom preprocessors.
|
2 | 2 |
|
| 3 | +use crate::book_test::list_all_files; |
3 | 4 | use crate::prelude::*;
|
4 | 5 | use anyhow::Result;
|
5 |
| -use mdbook_core::book::Book; |
| 6 | +use mdbook_core::book::{Book, BookItem, Chapter}; |
6 | 7 | use mdbook_driver::builtin_preprocessors::CmdPreprocessor;
|
7 | 8 | use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
|
| 9 | +use snapbox::IntoData; |
8 | 10 | use std::sync::{Arc, Mutex};
|
9 | 11 |
|
10 | 12 | struct Spy(Arc<Mutex<Inner>>);
|
@@ -199,3 +201,278 @@ fn with_preprocessor_same_name() {
|
199 | 201 | assert_eq!(inner.run_count, 1);
|
200 | 202 | assert_eq!(inner.rendered_with, ["html"]);
|
201 | 203 | }
|
| 204 | + |
| 205 | +// Checks that the interface stays backwards compatible. The interface here |
| 206 | +// should not be changed to fix a compatibility issue unless there is a |
| 207 | +// major-semver version update to mdbook. |
| 208 | +// |
| 209 | +// Note that this tests both preprocessors and renderers. It's in this module |
| 210 | +// for lack of a better location. |
| 211 | +#[test] |
| 212 | +fn extension_compatibility() { |
| 213 | + // This is here to force you to look at this test if you alter any of |
| 214 | + // these types such as adding new fields/variants. This test should be |
| 215 | + // updated accordingly. For example, new `BookItem` variants should be |
| 216 | + // added to the extension_compatibility book, or new fields should be |
| 217 | + // added to the expected input/output. This is also a check that these |
| 218 | + // should only be changed in a semver-breaking release |
| 219 | + let chapter = Chapter { |
| 220 | + name: "example".to_string(), |
| 221 | + content: "content".to_string(), |
| 222 | + number: None, |
| 223 | + sub_items: Vec::new(), |
| 224 | + path: None, |
| 225 | + source_path: None, |
| 226 | + parent_names: Vec::new(), |
| 227 | + }; |
| 228 | + let item = BookItem::Chapter(chapter); |
| 229 | + match &item { |
| 230 | + BookItem::Chapter(_) => {} |
| 231 | + BookItem::Separator => {} |
| 232 | + BookItem::PartTitle(_) => {} |
| 233 | + } |
| 234 | + let items = vec![item]; |
| 235 | + let _book = Book { items }; |
| 236 | + |
| 237 | + let mut test = BookTest::from_dir("preprocessor/extension_compatibility"); |
| 238 | + // Run it once with the preprocessor disabled so that we can verify |
| 239 | + // that the built book is identical with the preprocessor enabled. |
| 240 | + test.run("build", |cmd| { |
| 241 | + cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" |
| 242 | +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started |
| 243 | +[TIMESTAMP] [WARN] (mdbook_driver): The command `./my-preprocessor` for preprocessor `my-preprocessor` was not found, but is marked as optional. |
| 244 | +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend |
| 245 | +[TIMESTAMP] [INFO] (mdbook_html::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book/html` |
| 246 | +[TIMESTAMP] [WARN] (mdbook_driver): The command `./my-preprocessor` for preprocessor `my-preprocessor` was not found, but is marked as optional. |
| 247 | +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the my-renderer backend |
| 248 | +[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "my-renderer" renderer |
| 249 | +[TIMESTAMP] [WARN] (mdbook_driver): The command `./my-renderer` for backend `my-renderer` was not found, but is marked as optional. |
| 250 | +
|
| 251 | +"#]]); |
| 252 | + }); |
| 253 | + let orig_dir = test.dir.join("book.orig"); |
| 254 | + let pre_dir = test.dir.join("book"); |
| 255 | + std::fs::rename(&pre_dir, &orig_dir).unwrap(); |
| 256 | + |
| 257 | + // **CAUTION** DO NOT modify this value unless this is a major-semver change. |
| 258 | + let book_output = serde_json::json!({ |
| 259 | + "items": [ |
| 260 | + { |
| 261 | + "Chapter": { |
| 262 | + "content": "# Prefix chapter\n", |
| 263 | + "name": "Prefix chapter", |
| 264 | + "number": null, |
| 265 | + "parent_names": [], |
| 266 | + "path": "prefix.md", |
| 267 | + "source_path": "prefix.md", |
| 268 | + "sub_items": [] |
| 269 | + } |
| 270 | + }, |
| 271 | + { |
| 272 | + "Chapter": { |
| 273 | + "content": "# Chapter 1\n", |
| 274 | + "name": "Chapter 1", |
| 275 | + "number": [ |
| 276 | + 1 |
| 277 | + ], |
| 278 | + "parent_names": [], |
| 279 | + "path": "chapter_1.md", |
| 280 | + "source_path": "chapter_1.md", |
| 281 | + "sub_items": [] |
| 282 | + } |
| 283 | + }, |
| 284 | + { |
| 285 | + "Chapter": { |
| 286 | + "content": "", |
| 287 | + "name": "Draft chapter", |
| 288 | + "number": [ |
| 289 | + 2 |
| 290 | + ], |
| 291 | + "parent_names": [], |
| 292 | + "path": null, |
| 293 | + "source_path": null, |
| 294 | + "sub_items": [] |
| 295 | + } |
| 296 | + }, |
| 297 | + { |
| 298 | + "PartTitle": "Part title" |
| 299 | + }, |
| 300 | + { |
| 301 | + "Chapter": { |
| 302 | + "content": "# Part chapter\n", |
| 303 | + "name": "Part chapter", |
| 304 | + "number": [ |
| 305 | + 3 |
| 306 | + ], |
| 307 | + "parent_names": [], |
| 308 | + "path": "part/chapter.md", |
| 309 | + "source_path": "part/chapter.md", |
| 310 | + "sub_items": [ |
| 311 | + { |
| 312 | + "Chapter": { |
| 313 | + "content": "# Part sub chapter\n", |
| 314 | + "name": "Part sub chapter", |
| 315 | + "number": [ |
| 316 | + 3, |
| 317 | + 1 |
| 318 | + ], |
| 319 | + "parent_names": [ |
| 320 | + "Part chapter" |
| 321 | + ], |
| 322 | + "path": "part/sub-chapter.md", |
| 323 | + "source_path": "part/sub-chapter.md", |
| 324 | + "sub_items": [] |
| 325 | + } |
| 326 | + } |
| 327 | + ] |
| 328 | + } |
| 329 | + }, |
| 330 | + "Separator", |
| 331 | + { |
| 332 | + "Chapter": { |
| 333 | + "content": "# Suffix chapter\n", |
| 334 | + "name": "Suffix chapter", |
| 335 | + "number": null, |
| 336 | + "parent_names": [], |
| 337 | + "path": "suffix.md", |
| 338 | + "source_path": "suffix.md", |
| 339 | + "sub_items": [] |
| 340 | + } |
| 341 | + } |
| 342 | + ] |
| 343 | + }); |
| 344 | + let output_str = serde_json::to_string(&book_output).unwrap(); |
| 345 | + // **CAUTION** The only updates allowed here in a semver-compatible |
| 346 | + // release is to add new fields. |
| 347 | + let expected_config = serde_json::json!({ |
| 348 | + "book": { |
| 349 | + "authors": [], |
| 350 | + "description": null, |
| 351 | + "language": "en", |
| 352 | + "text-direction": null, |
| 353 | + "title": "extension_compatibility" |
| 354 | + }, |
| 355 | + "output": { |
| 356 | + "html": {}, |
| 357 | + "my-renderer": { |
| 358 | + "command": "./my-renderer", |
| 359 | + "custom-config": "renderer settings", |
| 360 | + "custom-table": { |
| 361 | + "extra": "xyz" |
| 362 | + }, |
| 363 | + "optional": true |
| 364 | + } |
| 365 | + }, |
| 366 | + "preprocessor": { |
| 367 | + "my-preprocessor": { |
| 368 | + "command": "./my-preprocessor", |
| 369 | + "custom-config": true, |
| 370 | + "custom-table": { |
| 371 | + "extra": "abc" |
| 372 | + }, |
| 373 | + "optional": true |
| 374 | + } |
| 375 | + } |
| 376 | + }); |
| 377 | + |
| 378 | + // **CAUTION** The only updates allowed here in a semver-compatible |
| 379 | + // release is to add new fields. The output should not change. |
| 380 | + let expected_preprocessor_input = serde_json::json!([ |
| 381 | + { |
| 382 | + "config": expected_config, |
| 383 | + "mdbook_version": "[VERSION]", |
| 384 | + "renderer": "html", |
| 385 | + "root": "[ROOT]" |
| 386 | + }, |
| 387 | + book_output |
| 388 | + ]); |
| 389 | + let expected_renderer_input = serde_json::json!( |
| 390 | + { |
| 391 | + "version": "[VERSION]", |
| 392 | + "root": "[ROOT]", |
| 393 | + "book": book_output, |
| 394 | + "config": expected_config, |
| 395 | + "destination": "[ROOT]/book/my-renderer", |
| 396 | + } |
| 397 | + ); |
| 398 | + |
| 399 | + // This preprocessor writes its input to some files, and writes the |
| 400 | + // hard-coded output specified above. |
| 401 | + test.rust_program( |
| 402 | + "my-preprocessor", |
| 403 | + &r###" |
| 404 | + use std::fs::OpenOptions; |
| 405 | + use std::io::{Read, Write}; |
| 406 | + fn main() { |
| 407 | + let mut args = std::env::args().skip(1); |
| 408 | + if args.next().as_deref() == Some("supports") { |
| 409 | + let mut file = OpenOptions::new() |
| 410 | + .create(true) |
| 411 | + .append(true) |
| 412 | + .open("support-check") |
| 413 | + .unwrap(); |
| 414 | + let renderer = args.next().unwrap(); |
| 415 | + writeln!(file, "{renderer}").unwrap(); |
| 416 | + if renderer != "html" { |
| 417 | + std::process::exit(1); |
| 418 | + } |
| 419 | + return; |
| 420 | + } |
| 421 | + let mut s = String::new(); |
| 422 | + std::io::stdin().read_to_string(&mut s).unwrap(); |
| 423 | + std::fs::write("preprocessor-input", &s).unwrap(); |
| 424 | + let output = r##"OUTPUT_REPLACE"##; |
| 425 | + println!("{output}"); |
| 426 | + } |
| 427 | + "### |
| 428 | + .replace("OUTPUT_REPLACE", &output_str), |
| 429 | + ) |
| 430 | + // This renderer writes its input to a file. |
| 431 | + .rust_program( |
| 432 | + "my-renderer", |
| 433 | + &r#" |
| 434 | + fn main() { |
| 435 | + use std::io::Read; |
| 436 | + let mut s = String::new(); |
| 437 | + std::io::stdin().read_to_string(&mut s).unwrap(); |
| 438 | + std::fs::write("renderer-input", &s).unwrap(); |
| 439 | + } |
| 440 | + "#, |
| 441 | + ) |
| 442 | + .run("build", |cmd| { |
| 443 | + cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" |
| 444 | +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started |
| 445 | +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend |
| 446 | +[TIMESTAMP] [INFO] (mdbook_html::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book/html` |
| 447 | +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the my-renderer backend |
| 448 | +[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "my-renderer" renderer |
| 449 | +
|
| 450 | +"#]]); |
| 451 | + }) |
| 452 | + .check_file("support-check", "html\nmy-renderer\n") |
| 453 | + .check_file( |
| 454 | + "preprocessor-input", |
| 455 | + serde_json::to_string(&expected_preprocessor_input) |
| 456 | + .unwrap() |
| 457 | + .is_json(), |
| 458 | + ) |
| 459 | + .check_file( |
| 460 | + "book/my-renderer/renderer-input", |
| 461 | + serde_json::to_string(&expected_renderer_input) |
| 462 | + .unwrap() |
| 463 | + .is_json(), |
| 464 | + ); |
| 465 | + // Verify both directories have the exact same output. |
| 466 | + test.rm_r("book/my-renderer/renderer-input"); |
| 467 | + let orig_files = list_all_files(&orig_dir); |
| 468 | + let pre_files = list_all_files(&pre_dir); |
| 469 | + assert_eq!(orig_files, pre_files); |
| 470 | + for file in &orig_files { |
| 471 | + let orig_path = orig_dir.join(file); |
| 472 | + if orig_path.is_file() { |
| 473 | + let orig = std::fs::read(&orig_path).unwrap(); |
| 474 | + let pre = std::fs::read(&pre_dir.join(file)).unwrap(); |
| 475 | + test.assert.eq(pre, orig); |
| 476 | + } |
| 477 | + } |
| 478 | +} |
0 commit comments