#![allow(
missing_docs,
reason = "an integration test crate has no public API to document"
)]
#![cfg(all(feature = "input", feature = "fixture"))]
use ctl_core::fixture::Suite;
use ctl_core::indoc;
use ctl_core::input::{Input, Limits, Loosened, REPO_FILE, set_default_limits, use_repo_limits};
use serde::de::IgnoredAny;
fn nested(depth: usize) -> String {
(0..depth)
.map(|level| format!("{}a:", " ".repeat(level)))
.collect::<Vec<_>>()
.join("\n")
+ " 1\n"
}
fn depth(limit: usize) -> Limits {
Limits {
max_depth: Some(limit),
..Limits::NONE
}
}
fn refusal(input: &Input) -> Option<String> {
let error = input.parse::<IgnoredAny>().err()?;
assert_eq!(error.problems.len(), 1, "{error}");
Some(error.problems[0].message.clone())
}
#[test]
fn a_limit_error_names_the_limit_the_file_and_where_to_raise_it() {
let input = Input::new("deep.yaml", nested(6)).limits(depth(4));
let error = input.parse::<IgnoredAny>().unwrap_err();
let shown = error.to_string();
assert!(shown.starts_with("deep.yaml:"), "{shown}");
assert!(
shown.ends_with("exceeds max_depth (4); raise limits.max_depth in .ctl/input.yaml"),
"{shown}"
);
assert!(error.problems[0].line.is_some(), "{shown}");
}
#[test]
fn text_already_in_memory_is_held_to_the_input_byte_limit() {
let limits = Limits {
max_reader_input_bytes: Some(10),
..Limits::NONE
};
let input = Input::new("big.yaml", "a: 12345678901234\n").limits(limits);
let error = input.parse::<IgnoredAny>().unwrap_err();
assert_eq!(error.problems[0].line, None, "{error}");
assert_eq!(
error.problems[0].message,
"is 18 bytes, which exceeds max_reader_input_bytes (10); \
raise limits.max_reader_input_bytes in .ctl/input.yaml"
);
}
#[test]
fn the_call_then_the_repository_then_the_process_decide_each_field() {
let suite = Suite::new("input-limits");
let six = Input::new("six.yaml", nested(6));
assert_eq!(refusal(&six), None, "serde-saphyr's depth 64 admits six");
set_default_limits(depth(4));
assert_eq!(
refusal(&six).as_deref(),
Some("exceeds max_depth (4); raise limits.max_depth in .ctl/input.yaml"),
"the process default applies"
);
let repo = suite.scenario("raised");
repo.file(
REPO_FILE,
indoc! {"
limits:
max_depth: 8
"},
);
assert_eq!(
use_repo_limits(repo.path()).unwrap(),
[],
"a limit at or below the default is not loosened"
);
assert_eq!(refusal(&six), None, "the repository beats the process");
let five = Input::new("six.yaml", nested(6)).limits(depth(5));
assert!(
refusal(&five).is_some_and(|message| message.contains("max_depth (5)")),
"the call beats the repository"
);
set_default_limits(Limits {
max_depth: Some(4),
max_nodes: Some(20),
..Limits::NONE
});
let wide = Input::new("wide.yaml", format!("[{}]\n", vec!["1"; 30].join(", ")));
assert!(
refusal(&wide).is_some_and(|message| message.contains("max_nodes (20)")),
"a field the repository leaves unset falls through to the process"
);
assert_eq!(
refusal(&six),
None,
"max_depth still comes from the repository"
);
let empty = suite.scenario("no-file");
use_repo_limits(empty.path()).unwrap();
assert!(
refusal(&six).is_some_and(|message| message.contains("max_depth (4)")),
"a repository without the file overrides nothing"
);
a_repository_file_is_held_to_known_fields(&suite);
set_default_limits(Limits::NONE);
let tight = suite.scenario("tight");
tight.file(
REPO_FILE,
indoc! {"
limits:
max_depth: 1
"},
);
use_repo_limits(tight.path()).unwrap();
use_repo_limits(repo.path())
.expect("a repository file is read under the process default, not the last repository");
assert_eq!(
refusal(&six),
None,
"the second repository replaced the first"
);
use_repo_limits(empty.path()).unwrap();
set_default_limits(depth(1));
let error = use_repo_limits(tight.path()).unwrap_err().to_string();
assert!(
error.ends_with(
"exceeds max_depth (1); raise max_depth in the tool's process default, \
which reads .ctl/input.yaml"
),
"a limit hit reading the repository file points at the process default: {error}"
);
set_default_limits(Limits::NONE);
}
#[expect(
clippy::unwrap_used,
reason = "a test helper: a failure here is the calling test failing"
)]
fn a_repository_file_is_held_to_known_fields(suite: &Suite) {
let wrong = suite.scenario("unknown");
wrong.file(
REPO_FILE,
indoc! {"
limits:
max_dept: 8
"},
);
let error = use_repo_limits(wrong.path()).unwrap_err().to_string();
assert!(error.contains("input.yaml"), "{error}");
assert!(error.contains("max_dept"), "{error}");
let ratio = suite.scenario("ratio-off");
ratio.file(
REPO_FILE,
indoc! {"
limits:
enforce_alias_anchor_ratio: false
"},
);
let error = use_repo_limits(ratio.path()).unwrap_err().to_string();
assert!(
error.contains("enforce_alias_anchor_ratio"),
"the alias ratio cannot be turned off: {error}"
);
let loose = suite.scenario("loose");
loose.file(
REPO_FILE,
indoc! {"
limits:
max_depth: 128
max_nodes: 10
max_reader_input_bytes: 536870912
alias_anchor_ratio_multiplier: 1000
"},
);
let loosened = use_repo_limits(loose.path()).unwrap();
assert_eq!(
loosened,
[
Loosened {
field: "max_reader_input_bytes",
value: "536870912".to_owned(),
default: "268435456".to_owned(),
},
Loosened {
field: "max_depth",
value: "128".to_owned(),
default: "64".to_owned(),
},
Loosened {
field: "alias_anchor_ratio_multiplier",
value: "1000".to_owned(),
default: "10".to_owned(),
},
],
"each field above the default is reported, a tighter one is not"
);
assert_eq!(
loosened[1].to_string(),
".ctl/input.yaml raises max_depth to 128, above the default 64"
);
}