mdbook/tests/testsuite/config.rs
Eric Huss 2afad43bdd Add error handling to env config handling
This adds several changes to how environment variables are handled to
more closely align with how configs are handled, and to fix an issue
with replacing entire tables. The changes are:

- Top-level tables like `MDBOOK_BOOK` now *replace* the contents of the
  `book` table instead of merging it. This adds consistency with how all
  the other environment objects work.
- Fixed allowing top-level replacement of `MDBOOK_BOOK` and
  `MDBOOK_OUTPUT`. This was inadvertently recently broken.
- Added ability to replace top-level `MDBOOK_RUST`. I don't recall why
  that wasn't included.
- Reject invalid keys like `MDBOOK_FOO`.
- Reject unknown keys, like `MDBOOK_BOOK='{"xyz": 123}'`
- Reject invalid types, like `MDBOOK_BOOK='{"title": 123}'`
2025-11-17 14:38:58 -08:00

343 lines
9.6 KiB
Rust

//! Tests for book configuration loading.
use crate::prelude::*;
// Test that config can load from environment variable.
#[test]
fn config_from_env() {
BookTest::from_dir("config/empty")
.run("build", |cmd| {
cmd.env("MDBOOK_BOOK__TITLE", "Custom env title");
})
.check_file_contains(
"book/index.html",
"<title>Chapter 1 - Custom env title</title>",
);
// json for some subtable
//
}
// Test environment config with JSON.
#[test]
fn config_json_from_env() {
// build table
BookTest::from_dir("config/empty")
.run("build", |cmd| {
cmd.env(
"MDBOOK_BOOK",
r#"{"title": "My Awesome Book", "authors": ["Michael-F-Bryan"]}"#,
);
})
.check_file_contains(
"book/index.html",
"<title>Chapter 1 - My Awesome Book</title>",
);
// book table
BookTest::from_dir("config/empty")
.run("build", |cmd| {
cmd.env("MDBOOK_BUILD", r#"{"build-dir": "alt"}"#);
})
.check_file_contains("alt/index.html", "<title>Chapter 1</title>");
}
// Test that a preprocessor receives config set in the environment.
#[test]
fn preprocessor_cfg_from_env() {
let mut test = BookTest::from_dir("config/empty");
test.rust_program(
"cat-to-file",
r#"
fn main() {
use std::io::Read;
let mut s = String::new();
std::io::stdin().read_to_string(&mut s).unwrap();
std::fs::write("out.txt", s).unwrap();
println!("{{\"items\": []}}");
}
"#,
)
.run("build", |cmd| {
cmd.env(
"MDBOOK_PREPROCESSOR__CAT_TO_FILE",
r#"{"command":"./cat-to-file", "array": [1,2,3], "number": 123}"#,
);
});
let out = read_to_string(test.dir.join("out.txt"));
let (ctx, _book) = mdbook_preprocessor::parse_input(out.as_bytes()).unwrap();
let cfg: serde_json::Value = ctx.config.get("preprocessor.cat-to-file").unwrap().unwrap();
assert_eq!(
cfg,
serde_json::json!({
"command": "./cat-to-file",
"array": [1,2,3],
"number": 123,
})
);
}
// Test that a renderer receives config set in the environment.
#[test]
fn output_cfg_from_env() {
let mut test = BookTest::from_dir("config/empty");
test.rust_program(
"cat-to-file",
r#"
fn main() {
use std::io::Read;
let mut s = String::new();
std::io::stdin().read_to_string(&mut s).unwrap();
std::fs::write("out.txt", s).unwrap();
}
"#,
)
.run("build", |cmd| {
cmd.env(
"MDBOOK_OUTPUT__CAT_TO_FILE",
r#"{"command":"./cat-to-file", "array": [1,2,3], "number": 123}"#,
);
});
let out = read_to_string(test.dir.join("book/out.txt"));
let ctx = mdbook_renderer::RenderContext::from_json(out.as_bytes()).unwrap();
let cfg: serde_json::Value = ctx.config.get("output.cat-to-file").unwrap().unwrap();
assert_eq!(
cfg,
serde_json::json!({
"command": "./cat-to-file",
"array": [1,2,3],
"number": 123,
})
);
}
// An invalid key at the top level.
#[test]
fn bad_config_top_level() {
BookTest::init(|_| {})
.change_file("book.toml", "foo = 123")
.run("build", |cmd| {
cmd.expect_failure()
.expect_stdout(str![[""]])
.expect_stderr(str![[r#"
ERROR Invalid configuration file
[TAB]Caused by: TOML parse error at line 1, column 1
|
1 | foo = 123
| ^^^
unknown field `foo`, expected one of `book`, `build`, `rust`, `output`, `preprocessor`
"#]]);
});
}
// An invalid table at the top level.
#[test]
fn bad_config_top_level_table() {
BookTest::init(|_| {})
.change_file(
"book.toml",
"[other]\n\
foo = 123",
)
.run("build", |cmd| {
cmd.expect_failure()
.expect_stdout(str![[""]])
.expect_stderr(str![[r#"
ERROR Invalid configuration file
[TAB]Caused by: TOML parse error at line 1, column 2
|
1 | [other]
| ^^^^^
unknown field `other`, expected one of `book`, `build`, `rust`, `output`, `preprocessor`
"#]]);
});
}
// An invalid key in the main book table.
#[test]
fn bad_config_in_book_table() {
BookTest::init(|_| {})
.change_file(
"book.toml",
"[book]\n\
title = \"bad-config\"\n\
foo = 123"
)
.run("build", |cmd| {
cmd.expect_failure()
.expect_stdout(str![[""]])
.expect_stderr(str![[r#"
ERROR Invalid configuration file
[TAB]Caused by: TOML parse error at line 3, column 1
|
3 | foo = 123
| ^^^
unknown field `foo`, expected one of `title`, `authors`, `description`, `src`, `language`, `text-direction`
"#]]);
});
}
// An invalid key in the main rust table.
#[test]
fn bad_config_in_rust_table() {
BookTest::init(|_| {})
.change_file(
"book.toml",
"[rust]\n\
title = \"bad-config\"\n",
)
.run("build", |cmd| {
cmd.expect_failure()
.expect_stdout(str![[""]])
.expect_stderr(str![[r#"
ERROR Invalid configuration file
[TAB]Caused by: TOML parse error at line 2, column 1
|
2 | title = "bad-config"
| ^^^^^
unknown field `title`, expected `edition`
"#]]);
});
}
// An invalid top-level key in the environment.
#[test]
fn env_invalid_config_key() {
BookTest::from_dir("config/empty").run("build", |cmd| {
cmd.env("MDBOOK_FOO", "testing")
.expect_failure()
.expect_stdout(str![[""]])
.expect_stderr(str![[r#"
ERROR invalid key `foo`
"#]]);
});
}
// An invalid value in the environment.
#[test]
fn env_invalid_value() {
BookTest::from_dir("config/empty")
.run("build", |cmd| {
cmd.env("MDBOOK_BOOK", r#"{"titlez": "typo"}"#)
.expect_failure()
.expect_stdout(str![[""]])
.expect_stderr(str![[r#"
ERROR unknown field `titlez`, expected one of `title`, `authors`, `description`, `src`, `language`, `text-direction`
"#]]);
})
.run("build", |cmd| {
cmd.env("MDBOOK_BOOK__TITLE", r#"{"looks like obj": "abc"}"#)
.expect_failure()
.expect_stdout(str![[""]])
.expect_stderr(str![[r#"
ERROR invalid type: map, expected a string
in `title`
"#]]);
})
// This is not valid JSON, so falls back to be interpreted as a string.
.run("build", |cmd| {
cmd.env("MDBOOK_BOOK__TITLE", r#"{braces}"#)
.expect_stdout(str![[""]])
.expect_stderr(str![[r#"
INFO Book building has started
INFO Running the html backend
INFO HTML book written to `[ROOT]/book`
"#]]);
})
.check_file_contains("book/index.html", "<title>Chapter 1 - {braces}</title>");
}
// Replacing the entire book table from the environment.
#[test]
fn env_entire_book_table() {
BookTest::init(|_| {})
.change_file(
"book.toml",
"[book]\n\
title = \"config title\"\n\
",
)
.run("build", |cmd| {
cmd.env("MDBOOK_BOOK", r#"{"description": "custom description"}"#);
})
// The book.toml title is removed.
.check_file_contains("book/index.html", "<title>Chapter 1</title>")
.check_file_contains(
"book/index.html",
r#"<meta name="description" content="custom description">"#,
);
}
// Replacing the entire output or preprocessor table from the environment.
#[test]
fn env_entire_output_preprocessor_table() {
BookTest::from_dir("config/empty")
.rust_program(
"mdbook-my-preprocessor",
r#"
fn main() {
let mut args = std::env::args().skip(1);
if args.next().as_deref() == Some("supports") {
return;
}
use std::io::Read;
let mut s = String::new();
std::io::stdin().read_to_string(&mut s).unwrap();
assert!(s.contains("custom preprocessor config"));
println!("{{\"items\": []}}");
}
"#,
)
.rust_program(
"mdbook-my-output",
r#"
fn main() {
use std::io::Read;
let mut s = String::new();
std::io::stdin().read_to_string(&mut s).unwrap();
assert!(s.contains("custom output config"));
eprintln!("preprocessor saw custom config");
}
"#,
)
.run("build", |cmd| {
let mut paths: Vec<_> =
std::env::split_paths(&std::env::var_os("PATH").unwrap_or_default()).collect();
paths.push(cmd.dir.clone());
let path = std::env::join_paths(paths).unwrap().into_string().unwrap();
cmd.env(
"MDBOOK_OUTPUT",
r#"{"my-output": {"foo": "custom output config"}}"#,
)
.env(
"MDBOOK_PREPROCESSOR",
r#"{"my-preprocessor": {"foo": "custom preprocessor config"}}"#,
)
.env("PATH", path)
.expect_stdout(str![[""]])
.expect_stderr(str![[r#"
INFO Book building has started
INFO Running the my-output backend
INFO Invoking the "my-output" renderer
preprocessor saw custom config
"#]]);
})
// No HTML output
.check_file_list("book", str![[""]]);
}