arti config: Check that example config is exhaustive
This is the final piece of #457.
This commit is contained in:
parent
0a324f843f
commit
fe9fb6b6ee
|
@ -82,6 +82,7 @@ dependencies = [
|
|||
"derive_builder_fork_arti",
|
||||
"fs-mistrust",
|
||||
"futures",
|
||||
"itertools",
|
||||
"libc",
|
||||
"notify",
|
||||
"once_cell",
|
||||
|
@ -89,7 +90,9 @@ dependencies = [
|
|||
"rlimit",
|
||||
"safelog",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tor-config",
|
||||
"tor-error",
|
||||
"tor-rtcompat",
|
||||
|
|
|
@ -32,6 +32,7 @@ config = { version = "0.13", default-features = false, features = ["toml"] }
|
|||
derive_builder = { version = "0.11", package = "derive_builder_fork_arti" }
|
||||
fs-mistrust = { path = "../fs-mistrust", version = "0.2.0" }
|
||||
futures = "0.3.14"
|
||||
itertools = "0.10.1"
|
||||
notify = "4.0"
|
||||
once_cell = { version = "1", optional = true }
|
||||
rlimit = "0.8.3"
|
||||
|
@ -50,6 +51,8 @@ trust-dns-proto = "0.21.1"
|
|||
|
||||
[dev-dependencies]
|
||||
regex = { version = "1", default-features = false, features = ["std"] }
|
||||
serde_json = "1.0.50"
|
||||
toml = "0.5"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = { version = "0.2", default-features = false }
|
||||
|
|
|
@ -273,4 +273,118 @@ mod test {
|
|||
let proxy = config.proxy();
|
||||
assert_eq!(&config.proxy, proxy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exhaustive() {
|
||||
use itertools::Itertools;
|
||||
use serde_json::Value as JsValue;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
let example = uncomment_example_settings(ARTI_EXAMPLE_CONFIG);
|
||||
let example: toml::Value = toml::from_str(&example).unwrap();
|
||||
// dbg!(&example);
|
||||
let example = serde_json::to_value(&example).unwrap();
|
||||
// dbg!(&example);
|
||||
|
||||
// "Exhaustive" taxonomy of the recognised configuration keys
|
||||
//
|
||||
// We use the JSON serialization of the default builders, because Rust's toml
|
||||
// implementation likes to omit more things, that we want to see.
|
||||
//
|
||||
// I'm not sure this is quite perfect but it is pretty good,
|
||||
// and has found a number of un-exampled config keys.
|
||||
let exhausts = [
|
||||
serde_json::to_value(&TorClientConfig::builder()).unwrap(),
|
||||
serde_json::to_value(&ArtiConfig::builder()).unwrap(),
|
||||
];
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct Walk {
|
||||
current_path: Vec<String>,
|
||||
problems: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl Walk {
|
||||
/// Records a problem
|
||||
fn bad(&mut self, m: &str) {
|
||||
self.problems
|
||||
.push((self.current_path.join("."), m.to_string()));
|
||||
}
|
||||
|
||||
/// Recurses, looking for problems
|
||||
///
|
||||
/// Visited for every node in either or both of the starting `exhausts`.
|
||||
///
|
||||
/// `E` is the number of elements in `exhausts`, ie the number of different
|
||||
/// top-level config types that Arti uses. Ie, 2.
|
||||
fn walk<const E: usize>(
|
||||
&mut self,
|
||||
example: Option<&JsValue>,
|
||||
exhausts: [Option<&JsValue>; E],
|
||||
) {
|
||||
assert! { exhausts.into_iter().any(|e| e.is_some()) }
|
||||
|
||||
let example = if let Some(e) = example {
|
||||
e
|
||||
} else {
|
||||
self.bad("missing from example");
|
||||
return;
|
||||
};
|
||||
|
||||
let tables = exhausts.map(|e| e?.as_object());
|
||||
|
||||
// Union of the keys of both exhausts' tables (insofar as they *are* tables)
|
||||
let table_keys = tables
|
||||
.iter()
|
||||
.flat_map(|t| t.map(|t| t.keys().cloned()).into_iter().flatten())
|
||||
.collect::<BTreeSet<String>>();
|
||||
|
||||
for key in table_keys {
|
||||
let example = if let Some(e) = example.as_object() {
|
||||
e
|
||||
} else {
|
||||
// At least one of the exhausts was a nonempty table,
|
||||
// but the corresponding example node isn't a table.
|
||||
self.bad("expected table in example");
|
||||
continue;
|
||||
};
|
||||
|
||||
// Descend the same key in all the places.
|
||||
self.current_path.push(key.clone());
|
||||
self.walk(example.get(&key), tables.map(|t| t?.get(&key)));
|
||||
self.current_path.pop().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let exhausts = exhausts.iter().map(Some).collect_vec().try_into().unwrap();
|
||||
|
||||
let mut walk = Walk::default();
|
||||
walk.walk::<2>(Some(&example), exhausts);
|
||||
let mut problems = walk.problems;
|
||||
|
||||
// When adding things here, check that `arti-example-config.toml`
|
||||
// actually has something about these particular config keys.
|
||||
let expect_missing = ["tor_network.authorities", "tor_network.fallback_caches"];
|
||||
|
||||
for exp in expect_missing {
|
||||
let was = problems.len();
|
||||
problems.retain(|(path, _)| path != exp);
|
||||
if problems.len() == was {
|
||||
problems.push((
|
||||
exp.into(),
|
||||
"expected to be missing but found in default".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let problems = problems
|
||||
.into_iter()
|
||||
.map(|(path, m)| format!(" config key {:?}: {}", path, m))
|
||||
.collect_vec();
|
||||
|
||||
assert! { problems.is_empty(),
|
||||
"example config exhaustiveness check failed:\n{}\n",
|
||||
problems.join("\n")}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue