|
| 1 | +use std::collections::{BTreeMap, BTreeSet}; |
1 | 2 | use std::path::PathBuf; |
2 | 3 |
|
3 | | -use apollo_node_config::node_config::SequencerNodeConfig; |
| 4 | +use apollo_config::dumping::SerializeConfig; |
| 5 | +use apollo_config::{FIELD_SEPARATOR, IS_NONE_MARK}; |
| 6 | +use apollo_node_config::config_utils::{config_to_preset, private_parameters}; |
| 7 | +use apollo_node_config::node_config::{SequencerNodeConfig, CONFIG_POINTERS}; |
4 | 8 | use jrsonnet_evaluator::trace::PathResolver; |
5 | 9 | use jrsonnet_evaluator::{FileImportResolver, State}; |
6 | 10 | use serde_json::Value; |
7 | 11 | use strum::IntoEnumIterator; |
8 | 12 |
|
9 | | -use crate::service::{GetComponentConfigs, NodeService, NodeType}; |
| 13 | +use crate::deployment_definitions::BASE_APP_CONFIGS_DIR_PATH; |
| 14 | +use crate::service::{GetComponentConfigs, NodeService, NodeType, KEYS_TO_BE_REPLACED}; |
| 15 | +use crate::test_utils::is_path_prefix; |
10 | 16 |
|
11 | 17 | const JSONNET_DIR: &str = "crates/apollo_deployments/jsonnet"; |
12 | 18 | const TESTING_OVERRIDES_PATH: &str = "testing/overrides.libsonnet"; |
@@ -102,6 +108,96 @@ where |
102 | 108 | } |
103 | 109 | } |
104 | 110 |
|
| 111 | +/// Asserts the applicative config emitted by jsonnet reproduces the committed `app_configs/*.json` |
| 112 | +/// for every keys, except keys that are overridable, secret, or under `components.*`. |
| 113 | +pub fn test_applicative_matches_app_configs() { |
| 114 | + // Applicative side: the single consolidated `node` service carries every component's business |
| 115 | + // config; round-trip through the config struct and render it in the app_configs preset format. |
| 116 | + let built = eval_build("consolidated", TESTING_OVERRIDES_PATH); |
| 117 | + let node = built.get("node").expect("consolidated has a `node` service").clone(); |
| 118 | + let parsed: SequencerNodeConfig = serde_json::from_value(node).unwrap(); |
| 119 | + let build_preset = config_to_preset(&serde_json::json!(parsed.dump())); |
| 120 | + let build_map = build_preset.as_object().unwrap(); |
| 121 | + |
| 122 | + let excluded = non_default_paths(); |
| 123 | + let is_excluded = |path: &str| { |
| 124 | + is_path_prefix("components", path) || excluded.iter().any(|key| is_path_prefix(key, path)) |
| 125 | + }; |
| 126 | + |
| 127 | + let app_config_map = merged_app_configs(); |
| 128 | + |
| 129 | + let mut mismatches = Vec::new(); |
| 130 | + for (key, app_config_value) in &app_config_map { |
| 131 | + if is_excluded(key) { |
| 132 | + continue; |
| 133 | + } |
| 134 | + match build_map.get(key) { |
| 135 | + Some(build_value) => { |
| 136 | + if build_value != app_config_value { |
| 137 | + mismatches.push(format!( |
| 138 | + "{key}: applicative={build_value} app_config={app_config_value}" |
| 139 | + )); |
| 140 | + } |
| 141 | + } |
| 142 | + None => mismatches |
| 143 | + .push(format!("{key}: missing in applicative (app_config={app_config_value})")), |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + assert!( |
| 148 | + mismatches.is_empty(), |
| 149 | + "applicative config diverges from app_configs/*.json at {} non-overridable, non-secret \ |
| 150 | + keys:\n {}", |
| 151 | + mismatches.len(), |
| 152 | + mismatches.join("\n ") |
| 153 | + ); |
| 154 | +} |
| 155 | + |
| 156 | +/// Merges every base `app_configs/<component>.json` (skipping the derived `replacer_*` files) into |
| 157 | +/// a single flat dotted-key map. |
| 158 | +fn merged_app_configs() -> BTreeMap<String, Value> { |
| 159 | + let mut app_config_map: BTreeMap<String, Value> = BTreeMap::new(); |
| 160 | + for entry in std::fs::read_dir(BASE_APP_CONFIGS_DIR_PATH).expect("app_configs dir exists") { |
| 161 | + let path = entry.expect("readable dir entry").path(); |
| 162 | + let is_json = path.extension().is_some_and(|extension| extension == "json"); |
| 163 | + let is_replacer = path.file_name().unwrap().to_string_lossy().starts_with("replacer_"); |
| 164 | + if !is_json || is_replacer { |
| 165 | + continue; |
| 166 | + } |
| 167 | + let contents = std::fs::read_to_string(&path).expect("app_config file is readable"); |
| 168 | + let object: serde_json::Map<String, Value> = |
| 169 | + serde_json::from_str(&contents).expect("app_config is a JSON object"); |
| 170 | + app_config_map.extend(object); |
| 171 | + } |
| 172 | + app_config_map |
| 173 | +} |
| 174 | + |
| 175 | +/// The config paths that are overridable or secrets or passed as pointers. |
| 176 | +fn non_default_paths() -> BTreeSet<String> { |
| 177 | + // An optional config is marked overridable/secret as `<path>.#is_none`; the override replaces |
| 178 | + // the whole option, so exclude the `<path>` subtree (not just the marker). |
| 179 | + let is_none_suffix = format!("{FIELD_SEPARATOR}{IS_NONE_MARK}"); |
| 180 | + let insert_with_option_root = |paths: &mut BTreeSet<String>, key: &str| { |
| 181 | + paths.insert(key.to_string()); |
| 182 | + if let Some(option_root) = key.strip_suffix(&is_none_suffix) { |
| 183 | + paths.insert(option_root.to_string()); |
| 184 | + } |
| 185 | + }; |
| 186 | + |
| 187 | + let mut paths = BTreeSet::new(); |
| 188 | + for key in KEYS_TO_BE_REPLACED.iter() { |
| 189 | + insert_with_option_root(&mut paths, key); |
| 190 | + } |
| 191 | + for ((target_path, _param), pointing_paths) in CONFIG_POINTERS.iter() { |
| 192 | + paths.insert(target_path.clone()); |
| 193 | + paths.extend(pointing_paths.iter().cloned()); |
| 194 | + } |
| 195 | + for key in private_parameters() { |
| 196 | + insert_with_option_root(&mut paths, &key); |
| 197 | + } |
| 198 | + paths |
| 199 | +} |
| 200 | + |
105 | 201 | /// Clones a `components` map with `url` and `port` removed from each component object — the two |
106 | 202 | /// fields the Rust config leaves as deploy-time placeholders, so they can't be compared against the |
107 | 203 | /// jsonnet's baked-in real values. |
|
0 commit comments