use mical_cli_config::{Config, Value};
use proptest::{prelude::*, property_test};
fn qp<'a>(config: &'a Config, prefix: &str) -> Vec<(&'a str, Value<'a>)> {
config.query_prefix(prefix).collect()
}
#[test]
fn empty_config_returns_nothing() {
let config = Config::from_kv_entries(std::iter::empty());
assert!(qp(&config, "any").is_empty());
assert!(qp(&config, "").is_empty());
}
#[test]
fn single_entry_prefix_match() {
let config = Config::from_kv_entries([("key", Value::String("v"))]);
assert_eq!(qp(&config, "k"), [("key", Value::String("v"))]);
}
#[test]
fn single_entry_exact_key_as_prefix() {
let config = Config::from_kv_entries([("key", Value::String("v"))]);
assert_eq!(qp(&config, "key"), [("key", Value::String("v"))]);
}
#[test]
fn no_match() {
let config =
Config::from_kv_entries([("apple", Value::String("a")), ("banana", Value::String("b"))]);
assert!(qp(&config, "cherry").is_empty());
assert!(qp(&config, "c").is_empty());
assert!(qp(&config, "apples").is_empty());
}
#[test]
fn empty_prefix_returns_all_in_first_occurrence_order() {
let config = Config::from_kv_entries([
("b", Value::String("1")),
("a", Value::String("2")),
("c", Value::String("3")),
]);
assert_eq!(
qp(&config, ""),
[("b", Value::String("1")), ("a", Value::String("2")), ("c", Value::String("3")),]
);
}
#[test]
fn groups_ordered_by_first_occurrence() {
let config = Config::from_kv_entries([
("x", Value::String("x1")),
("y", Value::String("y1")),
("x", Value::String("x2")),
]);
assert_eq!(
qp(&config, ""),
[("x", Value::String("x1")), ("x", Value::String("x2")), ("y", Value::String("y1")),]
);
}
#[test]
fn hierarchical_keys_with_shared_prefix() {
let config = Config::from_kv_entries([
("app.name", Value::String("MyApp")),
("app.version", Value::String("1.0")),
("db.host", Value::String("localhost")),
("app.name", Value::String("Renamed")),
]);
assert_eq!(
qp(&config, "app."),
[
("app.name", Value::String("MyApp")),
("app.name", Value::String("Renamed")),
("app.version", Value::String("1.0")),
]
);
assert_eq!(qp(&config, "db."), [("db.host", Value::String("localhost"))]);
}
#[test]
fn prefix_longer_than_any_key() {
let config = Config::from_kv_entries([("a", Value::String("1")), ("ab", Value::String("2"))]);
assert!(qp(&config, "abc").is_empty());
}
#[test]
fn mixed_value_types() {
let config = Config::from_kv_entries([
("s.flag", Value::Bool(true)),
("s.count", Value::Integer("42")),
("s.name", Value::String("test")),
]);
assert_eq!(
qp(&config, "s."),
[
("s.flag", Value::Bool(true)),
("s.count", Value::Integer("42")),
("s.name", Value::String("test")),
]
);
}
#[test]
fn complex_interleaving_with_prefix_filter() {
let config = Config::from_kv_entries([
("b.x", Value::String("b.x-1")),
("a.x", Value::String("a.x-1")),
("b.y", Value::String("b.y-1")),
("a.y", Value::String("a.y-1")),
("b.x", Value::String("b.x-2")),
]);
assert_eq!(
qp(&config, "a."),
[("a.x", Value::String("a.x-1")), ("a.y", Value::String("a.y-1")),]
);
assert_eq!(
qp(&config, "b."),
[
("b.x", Value::String("b.x-1")),
("b.x", Value::String("b.x-2")),
("b.y", Value::String("b.y-1")),
]
);
assert_eq!(
qp(&config, ""),
[
("b.x", Value::String("b.x-1")),
("b.x", Value::String("b.x-2")),
("a.x", Value::String("a.x-1")),
("b.y", Value::String("b.y-1")),
("a.y", Value::String("a.y-1")),
]
);
}
#[test]
fn prefix_distinguishes_similar_keys() {
let config = Config::from_kv_entries([
("a", Value::String("1")),
("ab", Value::String("2")),
("abc", Value::String("3")),
("b", Value::String("4")),
]);
assert_eq!(qp(&config, "ab"), [("ab", Value::String("2")), ("abc", Value::String("3")),]);
}
#[test]
fn many_keys_interleaved() {
let keys = ["ui.theme", "ui.font", "net.host", "net.port"];
let entries: Vec<(&str, Value)> = (0..100)
.map(|i| {
let k = keys[i % keys.len()];
let v = Value::Integer("1");
(k, v)
})
.collect();
let config = Config::from_kv_entries(entries);
let ui = qp(&config, "ui.");
let ui_keys: Vec<&str> = ui.iter().map(|(k, _)| *k).collect();
let first_font = ui_keys.iter().position(|k| *k == "ui.font").unwrap();
assert!(ui_keys[..first_font].iter().all(|k| *k == "ui.theme"));
assert_eq!(ui.iter().filter(|(k, _)| *k == "ui.theme").count(), 25);
assert_eq!(ui.iter().filter(|(k, _)| *k == "ui.font").count(), 25);
let all = qp(&config, "");
let mut seen_order: Vec<&str> = Vec::new();
for (k, _) in &all {
if !seen_order.contains(k) {
seen_order.push(k);
}
}
assert_eq!(seen_order, vec!["ui.theme", "ui.font", "net.host", "net.port"]);
assert_eq!(all.len(), 100);
}
#[test]
fn many_overrides_of_single_key_among_others() {
let mut entries: Vec<(&str, Value)> = Vec::new();
entries.push(("server.port", Value::Integer("8080")));
for i in 0..50 {
entries.push(("server.log", Value::String(if i % 2 == 0 { "info" } else { "debug" })));
entries.push(("server.port", Value::Integer("9090")));
}
let config = Config::from_kv_entries(entries);
let result = qp(&config, "server.");
let mut seen_order: Vec<&str> = Vec::new();
for (k, _) in &result {
if !seen_order.contains(k) {
seen_order.push(k);
}
}
assert_eq!(seen_order, vec!["server.port", "server.log"]);
assert_eq!(result.iter().filter(|(k, _)| *k == "server.port").count(), 51);
assert_eq!(result.iter().filter(|(k, _)| *k == "server.log").count(), 50);
}
fn reference_query_prefix<'a>(
entries: &'a [(String, String)],
prefix: &str,
) -> Vec<(&'a str, &'a str)> {
let mut key_order: Vec<&str> = Vec::new();
for (k, _) in entries {
if k.starts_with(prefix) && !key_order.contains(&k.as_str()) {
key_order.push(k.as_str());
}
}
let mut result = Vec::new();
for group_key in &key_order {
for (k, v) in entries {
if k.as_str() == *group_key {
result.push((k.as_str(), v.as_str()));
}
}
}
result
}
#[property_test]
fn matches_linear_reference(
#[strategy = prop::collection::vec(("[a-d]{1,4}", "[a-z]{0,8}"), 0..100)] entries: Vec<(
String,
String,
)>,
#[strategy = "[a-d]{0,3}"] prefix: String,
) {
let config = Config::from_kv_entries(
entries.iter().map(|(k, v)| (k.as_str(), Value::String(v.as_str()))),
);
let actual: Vec<(&str, &str)> = config
.query_prefix(&prefix)
.map(|(k, v)| match v {
Value::String(s) => (k, s),
_ => unreachable!(),
})
.collect();
let expected = reference_query_prefix(&entries, &prefix);
prop_assert_eq!(actual, expected);
}
#[property_test]
fn empty_prefix_equals_entries(
#[strategy = prop::collection::vec(("[a-z]{1,5}", "[a-z]{0,5}"), 0..50)] entries: Vec<(
String,
String,
)>,
) {
let config = Config::from_kv_entries(
entries.iter().map(|(k, v)| (k.as_str(), Value::String(v.as_str()))),
);
let from_prefix: Vec<_> = config
.query_prefix("")
.map(|(k, v)| {
(
k,
match v {
Value::String(s) => s,
_ => unreachable!(),
},
)
})
.collect();
let from_entries: Vec<_> = config
.entries()
.map(|(k, v)| {
(
k,
match v {
Value::String(s) => s,
_ => unreachable!(),
},
)
})
.collect();
prop_assert_eq!(from_prefix, from_entries);
}
#[property_test]
fn all_keys_start_with_prefix(
#[strategy = prop::collection::vec(("[a-d]{1,4}", "[a-z]{0,5}"), 0..20)] entries: Vec<(
String,
String,
)>,
#[strategy = "[a-d]{0,3}"] prefix: String,
) {
let config = Config::from_kv_entries(
entries.iter().map(|(k, v)| (k.as_str(), Value::String(v.as_str()))),
);
for (key, _) in config.query_prefix(&prefix) {
prop_assert!(
key.starts_with(&prefix),
"key {:?} does not start with prefix {:?}",
key,
prefix
);
}
}
#[property_test]
fn same_keys_are_consecutive(
#[strategy = prop::collection::vec(("[a-c]{1,3}", "[a-z]{0,5}"), 0..100)] entries: Vec<(
String,
String,
)>,
#[strategy = "[a-c]{0,2}"] prefix: String,
) {
let config = Config::from_kv_entries(
entries.iter().map(|(k, v)| (k.as_str(), Value::String(v.as_str()))),
);
let keys: Vec<&str> = config.query_prefix(&prefix).map(|(k, _)| k).collect();
let mut finished: Vec<&str> = Vec::new();
for window in keys.windows(2) {
if window[0] != window[1] {
finished.push(window[0]);
}
prop_assert!(
!finished.contains(&window[1]),
"key {:?} reappears after gap (finished groups: {:?})",
window[1],
finished
);
}
}
#[property_test]
fn values_consistent_with_query(
#[strategy = prop::collection::vec(("[a-c]{1,3}", "[a-z]{0,5}"), 0..15)] entries: Vec<(
String,
String,
)>,
#[strategy = "[a-c]{0,2}"] prefix: String,
) {
let config = Config::from_kv_entries(
entries.iter().map(|(k, v)| (k.as_str(), Value::String(v.as_str()))),
);
let prefix_results: Vec<(String, String)> = config
.query_prefix(&prefix)
.map(|(k, v)| {
(
k.to_owned(),
match v {
Value::String(s) => s.to_owned(),
_ => unreachable!(),
},
)
})
.collect();
let mut unique_keys: Vec<&str> = Vec::new();
for (k, _) in &prefix_results {
if !unique_keys.contains(&k.as_str()) {
unique_keys.push(k.as_str());
}
}
for key in unique_keys {
let from_prefix: Vec<&str> =
prefix_results.iter().filter(|(k, _)| k == key).map(|(_, v)| v.as_str()).collect();
let from_query: Vec<&str> = config
.query(key)
.map(|v| match v {
Value::String(s) => s,
_ => unreachable!(),
})
.collect();
prop_assert_eq!(from_prefix, from_query, "values mismatch for key {:?}", key);
}
}