use super::*;
use crate::Config;
fn old_shape_json() -> String {
let mut document =
serde_json::to_value(Config::default()).expect("serialize the default config");
let storage = document
.get_mut("storage")
.and_then(serde_json::Value::as_object_mut)
.expect("the default config has a storage object");
storage.insert("memtable_size_threshold".into(), 33_554_432.into());
storage.insert("max_sstable_size".into(), 268_435_456.into());
storage.insert("block_size".into(), 65_536.into());
storage.insert("enable_bloom_filters".into(), true.into());
storage.insert("bloom_filter_fp_rate".into(), 0.01.into());
storage.insert("io_threads".into(), 8.into());
storage.insert("sync_mode".into(), "Normal".into());
storage.insert(
"compaction".into(),
serde_json::json!({ "auto_compaction": false }),
);
let query = document
.get_mut("query")
.and_then(serde_json::Value::as_object_mut)
.expect("the default config has a query object");
query.insert("plan_cache_size".into(), 1000.into());
query.insert("enable_optimization".into(), true.into());
query.insert(
"parallel".into(),
serde_json::json!({ "enabled": true, "max_threads": 4, "min_parallel_rows": 1000 }),
);
let root = document
.as_object_mut()
.expect("a serialized config is an object");
root.insert(
"performance".into(),
serde_json::json!({
"enable_metrics": true,
"metrics_interval": { "secs": 60, "nanos": 0 },
"enable_profiling": false,
"background_tasks": {
"enable_stats": true,
"stats_interval": { "secs": 300, "nanos": 0 },
"enable_cleanup": true,
"cleanup_interval": { "secs": 3600, "nanos": 0 }
}
}),
);
serde_json::to_string_pretty(&document).expect("re-serialize the old-shape document")
}
#[test]
fn an_old_shape_document_loads_and_warns_for_every_removed_key() {
let json = old_shape_json();
let (config, warning) = Config::from_json_str_reporting_removed(&json, "config dict")
.expect("an old-shape config must still LOAD: the posture is parse-and-ignore");
assert_eq!(config.storage.memtable_size_threshold, 33_554_432);
assert_eq!(
config.memory.max_memory,
Config::default().memory.max_memory
);
assert!(!config.storage.compaction.auto_compaction);
let warning = warning.expect("a document naming removed keys MUST warn, never be silent");
assert!(
warning.contains("config dict"),
"the warning must name the source: {warning}"
);
for removed in REMOVED_KEYS {
assert!(
warning.contains(removed.path),
"the warning must name {} so the user can find and delete it: {warning}",
removed.path
);
}
}
#[test]
fn the_warning_claims_nothing_about_whether_the_load_succeeds() {
let single: &[&'static Removed] = &[&REMOVED_KEYS[0]];
let all: Vec<&'static Removed> = REMOVED_KEYS.iter().collect();
for present in [single, all.as_slice()] {
let warning = deprecation_warning("config dict", present).expect("keys are present");
assert!(
warning.contains("REMOVED") && warning.contains("NO EFFECT"),
"the warning must state that the named keys do nothing: {warning}"
);
for forbidden in ["still loads", "IGNORED", "loads successfully"] {
assert!(
!warning.contains(forbidden),
"the warning must make NO claim about the fate of the load \
(found {forbidden:?}): a LATER stage can still reject the \
document, whatever placement the caller chooses: {warning}"
);
}
}
}
#[test]
fn a_current_shape_document_is_silent() {
let json = serde_json::to_string(&Config::default()).expect("serialize default config");
let (_, warning) = Config::from_json_str_reporting_removed(&json, "config dict")
.expect("a round-tripped default config must load");
assert!(
warning.is_none(),
"a config naming no removed key must warn about nothing: {warning:?}"
);
}
#[test]
fn matching_is_scoped_to_the_dotted_path() {
let document = serde_json::json!({
"block_size": 4096,
"query": { "block_size": 4096, "io_threads": 2 },
"storage": { "compaction": { "block_size": 4096 } }
});
assert!(
!json_has_path(&document, "storage.block_size"),
"a `block_size` elsewhere is not `storage.block_size`"
);
assert!(
!json_has_path(&document, "storage.io_threads"),
"a `query.io_threads` is not `storage.io_threads`"
);
assert!(json_has_path(&document, "query.block_size"));
}
#[test]
fn a_null_valued_removed_key_is_still_reported() {
let document = serde_json::json!({ "storage": { "sync_mode": serde_json::Value::Null } });
assert!(json_has_path(&document, "storage.sync_mode"));
}
#[test]
fn a_failed_load_produces_no_warning() {
let json = r#"{ "storage": { "block_size": 65536 } }"#;
assert!(
warning_for_json("config dict", json).is_some(),
"fixture must name a removed key, else this test proves nothing"
);
let outcome = Config::from_json_str_reporting_removed(json, "config dict");
assert!(
outcome.is_err(),
"an incomplete document must still fail the load"
);
assert!(
outcome
.map(|(_, warning)| warning)
.unwrap_or(None)
.is_none(),
"the reporting constructor must not return a warning alongside an Err"
);
}
#[test]
fn unparseable_content_is_not_this_scans_error() {
assert!(warning_for_json("config dict", "{ not json").is_none());
assert!(Config::from_json_str("{ not json").is_err());
}
#[test]
fn every_removed_key_documents_its_removal() {
for removed in REMOVED_KEYS {
assert!(
removed.note.contains("#1696"),
"{} must cite the issue that removed it",
removed.path
);
assert!(
removed.note.len() > 30,
"{} needs a real explanation, not a stub: {}",
removed.path,
removed.note
);
}
}
#[test]
fn direct_serde_deserialization_is_the_unreported_surface() {
let json = old_shape_json();
let (_, warning) = Config::from_json_str_reporting_removed(&json, "config dict")
.expect("the fixture is a loadable document");
let warning = warning.expect("the reporting constructor must name the removed keys");
assert!(warning.contains("storage.block_size"));
let direct: Config =
serde_json::from_str(&json).expect("serde accepts the removed keys silently");
assert_eq!(
direct.storage.memtable_size_threshold, 33_554_432,
"the surviving half of the document must still be honoured, else this \
test is about a failed parse instead of a silent discard"
);
}