use std::path::Path;
use super::ServerConfig;
type TestResult = Result<(), Box<dyn std::error::Error>>;
const SHIPPED_CONFIG_FILES: [&str; 4] = [
"dev-config.toml",
"examples/agent-dev/demo-config.toml",
"examples/incident-triage/demo-config.toml",
"docs/authoring/tutorial-examples/aion.toml",
];
type ConfigExtractor = fn(&str) -> Result<Vec<String>, String>;
const EMBEDDED_CONFIG_SOURCES: [(&str, ConfigExtractor); 5] = [
("ci/smoke-embed.sh", extract_smoke_embed_heredocs),
(
"conformance/aion-clients/README.md",
extract_server_toml_fences,
),
("docs/GETTING-STARTED.md", extract_server_toml_fences),
("docs/operations/operations.md", extract_server_toml_fences),
(
"docs/design/aion-operations/DESIGN.md",
extract_server_toml_fences,
),
];
#[test]
fn every_shipped_config_satisfies_the_boot_required_set() -> TestResult {
let scratch = crate::test_support::private_tempdir()?;
let mut failures = Vec::new();
for relative in SHIPPED_CONFIG_FILES {
match read_repo_file(relative) {
Ok(bytes) => {
if let Err(failure) = check_boot_required(relative, &bytes, scratch.path()) {
failures.push(failure);
}
}
Err(failure) => failures.push(failure),
}
}
for (relative, extract) in EMBEDDED_CONFIG_SOURCES {
let extracted = read_repo_file(relative)
.and_then(|bytes| {
String::from_utf8(bytes)
.map_err(|utf8_error| format!("{relative} is not UTF-8: {utf8_error}"))
})
.and_then(|source| {
extract(&source).map_err(|extract_error| format!("{relative}: {extract_error}"))
});
match extracted {
Ok(configs) => {
for (index, config) in configs.iter().enumerate() {
let name = format!("{relative} (embedded config {})", index + 1);
if let Err(failure) =
check_boot_required(&name, config.as_bytes(), scratch.path())
{
failures.push(failure);
}
}
}
Err(failure) => failures.push(failure),
}
}
assert!(
failures.is_empty(),
"shipped configs fail the boot-side required sweep:\n{}",
failures.join("\n")
);
Ok(())
}
#[test]
fn the_declared_upgrade_defaults_match_the_shipped_teaching_configs() -> TestResult {
let dev_config = String::from_utf8(read_repo_file("dev-config.toml")?)?;
let cli_template = String::from_utf8(read_repo_file(
"crates/aion-cli/templates/shared/aion.toml",
)?)?;
let sources: [(&str, &str); 3] = [
("dev-config.toml", dev_config.as_str()),
(
"crates/aion-server/templates/first-run-config.toml",
crate::config::FIRST_RUN_CONFIG,
),
(
"crates/aion-cli/templates/shared/aion.toml",
cli_template.as_str(),
),
];
for (name, text) in sources {
let document: toml::Value = toml::from_str(text)?;
for entry in &crate::state::BOOT_REQUIRED_FIELD_DEFAULTS {
let shipped = document
.get(entry.section)
.and_then(|section| section.get(entry.key))
.ok_or_else(|| format!("{name} does not carry {}", entry.path))?;
let declared: toml::Value =
toml::from_str::<toml::Value>(&format!("value = {}", entry.default_toml))?
.get("value")
.cloned()
.ok_or_else(|| format!("declared default for {} did not parse", entry.path))?;
assert_eq!(
shipped, &declared,
"{name}'s {} must equal the declared upgrade default `{}`",
entry.path, entry.default_toml
);
}
}
Ok(())
}
#[test]
fn the_first_run_scaffold_satisfies_the_boot_required_set() -> TestResult {
let scratch = crate::test_support::private_tempdir()?;
check_boot_required(
"crates/aion-server/templates/first-run-config.toml",
crate::config::FIRST_RUN_CONFIG.as_bytes(),
scratch.path(),
)?;
Ok(())
}
fn read_repo_file(relative: &str) -> Result<Vec<u8>, String> {
let path = format!("{}/../../{relative}", env!("CARGO_MANIFEST_DIR"));
std::fs::read(&path)
.map_err(|io_error| format!("cannot read shipped config `{path}`: {io_error}"))
}
fn extract_smoke_embed_heredocs(script: &str) -> Result<Vec<String>, String> {
const OPENER: &str = "<<EOF\n";
let mut configs = Vec::new();
let mut cursor = 0;
while let Some(found) = script[cursor..].find(OPENER) {
let start = cursor + found + OPENER.len();
let end = start
+ script[start..]
.find("\nEOF\n")
.ok_or_else(|| "a `<<EOF` heredoc never closes".to_owned())?;
let body = &script[start..end];
if body.contains("[server]") {
let substituted = body
.replace("${HTTP_ADDR}", "127.0.0.1:18099")
.replace("${GRPC_PORT}", "51099");
if let Some(token) = residual_dollar_token(&substituted) {
return Err(format!(
"a heredoc carries a shell-expansion token this sweep does \
not substitute: `{token}`"
));
}
configs.push(substituted);
}
cursor = end;
}
if configs.is_empty() {
return Err("no `<<EOF` heredoc containing a [server] section found".to_owned());
}
Ok(configs)
}
fn extract_server_toml_fences(markdown: &str) -> Result<Vec<String>, String> {
const OPENER: &str = "```toml\n";
let mut configs = Vec::new();
let mut cursor = 0;
while let Some(found) = markdown[cursor..].find(OPENER) {
let start = cursor + found + OPENER.len();
let end = start
+ markdown[start..]
.find("\n```")
.ok_or_else(|| "a ```toml fence never closes".to_owned())?;
let block = &markdown[start..end];
if block.contains("[server]") {
let substituted = block.replace("<repo>", ".");
if let Some(line) = substituted.lines().find(|line| line.contains("\"<")) {
return Err(format!(
"a config block carries a value placeholder this sweep does \
not substitute: `{line}`"
));
}
configs.push(substituted);
}
cursor = end;
}
if configs.is_empty() {
return Err("no ```toml fence containing a [server] section found".to_owned());
}
Ok(configs)
}
fn residual_dollar_token(text: &str) -> Option<String> {
for (start, _) in text.match_indices('$') {
let tail = &text[start + 1..];
let mut token = String::from("$");
let mut rest = tail.chars();
match rest.next() {
Some('{') => {
token.push('{');
for c in rest {
if c == '\n' {
break;
}
token.push(c);
if c == '}' {
break;
}
}
return Some(token);
}
Some(first) if first.is_ascii_alphabetic() || first == '_' => {
token.push(first);
for c in rest {
if c.is_ascii_alphanumeric() || c == '_' {
token.push(c);
} else {
break;
}
}
return Some(token);
}
_ => {}
}
}
None
}
fn check_boot_required(name: &str, bytes: &[u8], scratch: &Path) -> Result<(), String> {
let config = ServerConfig::from_slice_with_home_in(bytes, scratch, scratch)
.map_err(|error| format!("{name} does not parse: {error}"))?;
if config.store.lock_acquisition_patience_ms.is_some()
|| config.store.lock_acquisition_retry_cadence_ms.is_some()
{
return Err(format!(
"{name} teaches the retired lock-acquisition keys; remove them \
(the server warns about and ignores them)"
));
}
crate::state::boot_required_probe(&config)
.map_err(|error| format!("{name} fails the boot-side required sweep: {error}"))
}