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",
|
"derive_builder_fork_arti",
|
||||||
"fs-mistrust",
|
"fs-mistrust",
|
||||||
"futures",
|
"futures",
|
||||||
|
"itertools",
|
||||||
"libc",
|
"libc",
|
||||||
"notify",
|
"notify",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
@ -89,7 +90,9 @@ dependencies = [
|
||||||
"rlimit",
|
"rlimit",
|
||||||
"safelog",
|
"safelog",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml",
|
||||||
"tor-config",
|
"tor-config",
|
||||||
"tor-error",
|
"tor-error",
|
||||||
"tor-rtcompat",
|
"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" }
|
derive_builder = { version = "0.11", package = "derive_builder_fork_arti" }
|
||||||
fs-mistrust = { path = "../fs-mistrust", version = "0.2.0" }
|
fs-mistrust = { path = "../fs-mistrust", version = "0.2.0" }
|
||||||
futures = "0.3.14"
|
futures = "0.3.14"
|
||||||
|
itertools = "0.10.1"
|
||||||
notify = "4.0"
|
notify = "4.0"
|
||||||
once_cell = { version = "1", optional = true }
|
once_cell = { version = "1", optional = true }
|
||||||
rlimit = "0.8.3"
|
rlimit = "0.8.3"
|
||||||
|
@ -50,6 +51,8 @@ trust-dns-proto = "0.21.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
regex = { version = "1", default-features = false, features = ["std"] }
|
regex = { version = "1", default-features = false, features = ["std"] }
|
||||||
|
serde_json = "1.0.50"
|
||||||
|
toml = "0.5"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
libc = { version = "0.2", default-features = false }
|
libc = { version = "0.2", default-features = false }
|
||||||
|
|
|
@ -273,4 +273,118 @@ mod test {
|
||||||
let proxy = config.proxy();
|
let proxy = config.proxy();
|
||||||
assert_eq!(&config.proxy, 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