hyperactor_config/global.rs
1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Global layered configuration for Hyperactor.
10//!
11//! This module provides the process-wide configuration store and APIs
12//! to access it. Configuration values are resolved via a **layered
13//! model**: `TestOverride → Env → Runtime → File → ClientOverride →
14//! Default`.
15//!
16//! - Reads (`get`, `get_cloned`) consult layers in that order, falling
17//! back to defaults if no explicit value is set.
18//! - `attrs()` returns a complete snapshot of all CONFIG-marked keys at
19//! call time: it materializes defaults for keys not set in any layer.
20//! Keys without @meta(CONFIG = …) are excluded.
21//! - In tests, `lock()` and `override_key` allow temporary overrides
22//! that are removed automatically when the guard drops.
23//! - In normal operation, a parent process can capture its effective
24//! config via `attrs()` and pass that snapshot to a child during
25//! bootstrap. The child installs it as a `ClientOverride` layer.
26//! Note that Env and Runtime layers will take precedence over this
27//! inherited configuration.
28//!
29//! This design provides flexibility (easy test overrides, runtime
30//! updates, YAML/Env baselines) while ensuring type safety and
31//! predictable resolution order.
32//!
33//! # Testing
34//!
35//! Tests can override global configuration using [`lock`]. This
36//! ensures such tests are serialized (and cannot clobber each other's
37//! overrides).
38//!
39//! ```ignore
40//! #[test]
41//! fn test_my_feature() {
42//! let config = hyperactor::config::global::lock();
43//! let _guard = config.override_key(SOME_CONFIG_KEY, test_value);
44//! // ... test logic here ...
45//! }
46//! ```
47use std::collections::HashMap;
48use std::marker::PhantomData;
49use std::path::Path;
50use std::sync::Arc;
51use std::sync::LazyLock;
52use std::sync::RwLock;
53use std::sync::atomic::AtomicU64;
54use std::sync::atomic::Ordering;
55
56use arc_swap::ArcSwap;
57use serde::Deserialize;
58use serde::Serialize;
59
60use crate::CONFIG;
61use crate::attrs::AttrKeyInfo;
62use crate::attrs::AttrValue;
63use crate::attrs::Attrs;
64use crate::attrs::Key;
65use crate::from_env;
66use crate::from_yaml;
67
68/// Configuration source layers in priority order.
69///
70/// Resolution order is always: **TestOverride -> Env -> Runtime
71/// -> File -> ClientOverride -> Default**.
72///
73/// Smaller `priority()` number = higher precedence.
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
75pub enum Source {
76 /// Values set by the config snapshot sent from the client
77 /// during proc bootstrap.
78 ClientOverride,
79 /// Values loaded from configuration files (e.g., YAML).
80 File,
81 /// Values read from environment variables at process startup.
82 /// Higher priority than Runtime, File, and ClientOverride, but
83 /// lower than TestOverride.
84 Env,
85 /// Values set programmatically at runtime. High-priority layer
86 /// but overridden by Env and TestOverride.
87 Runtime,
88 /// Ephemeral values inserted by tests via
89 /// `ConfigLock::override_key`. Always wins over all other
90 /// sources; removed when the guard drops.
91 TestOverride,
92 /// The key's declared default value. Not stored as a layer —
93 /// used only in [`ConfigEntry`] to report that no explicit layer
94 /// provided a value.
95 Default,
96}
97
98/// Return the numeric priority for a source.
99///
100/// Smaller number = higher precedence. Matches the documented order:
101/// TestOverride (0) -> Env (1) -> Runtime (2) -> File (3) ->
102/// ClientOverride (4).
103fn priority(s: Source) -> u8 {
104 match s {
105 Source::TestOverride => 0,
106 Source::Env => 1,
107 Source::Runtime => 2,
108 Source::File => 3,
109 Source::ClientOverride => 4,
110 Source::Default => 5,
111 }
112}
113
114/// The full set of configuration layers in priority order.
115///
116/// `Layers` wraps a vector of [`Layer`]s, always kept sorted by
117/// [`priority`] (lowest number = highest precedence).
118///
119/// Resolution (`get`, `get_cloned`, `attrs`) consults `ordered` from
120/// front to back, returning the first value found for each key and
121/// falling back to defaults if none are set in any layer.
122struct Layers {
123 /// Kept sorted by `priority` (lowest number first = highest
124 /// priority).
125 ordered: Vec<Layer>,
126}
127
128/// A single configuration layer in the global configuration model.
129///
130/// Layers are consulted in priority order (`TestOverride → Env →
131/// Runtime → File → ClientOverride → Default`) when resolving
132/// configuration values. Each variant holds an [`Attrs`] map of
133/// key/value pairs.
134///
135/// The `TestOverride` variant additionally maintains per-key override
136/// stacks to support nested and out-of-order test overrides.
137///
138/// Variants:
139/// - [`Layer::ClientOverride`] - Values set by the config snapshot
140/// sent from the client during proc bootstrap.
141/// - [`Layer::File`] — Values loaded from configuration files.
142/// - [`Layer::Env`] — Values sourced from process environment
143/// variables.
144/// - [`Layer::Runtime`] — Programmatically set runtime overrides.
145/// - [`Layer::TestOverride`] — Temporary in-test overrides applied
146/// under [`ConfigLock`].
147///
148/// Layers are stored in [`Layers::ordered`], kept sorted by their
149/// effective [`Source`] priority (`TestOverride` first, `Default`
150/// last).
151enum Layer {
152 /// Values set by the config snapshot sent from the client
153 /// during proc bootstrap.
154 ClientOverride(Attrs),
155
156 /// Values loaded from configuration files.
157 File(Attrs),
158
159 /// Values read from process environment variables. Typically
160 /// installed at startup via [`init_from_env`].
161 Env(Attrs),
162
163 /// Values set programmatically at runtime. High-priority layer
164 /// used for dynamic updates (e.g., Python `configure()` API), but
165 /// overridden by Env and TestOverride layers.
166 Runtime(Attrs),
167
168 /// Ephemeral values inserted during tests via
169 /// [`ConfigLock::override_key`]. Always takes precedence over all
170 /// other layers. Holds both the active `attrs` map and a per-key
171 /// `stacks` table to support nested and out-of-order test
172 /// overrides.
173 TestOverride {
174 attrs: Attrs,
175 stacks: HashMap<&'static str, OverrideStack>,
176 },
177}
178
179/// A per-key stack of test overrides used by the
180/// [`Layer::TestOverride`] layer.
181///
182/// Each stack tracks the sequence of active overrides applied to a
183/// single configuration key. The topmost frame represents the
184/// currently effective override; earlier frames represent older
185/// (still live) guards that may drop out of order.
186///
187/// Fields:
188/// - `env_var`: The associated process environment variable name, if
189/// any.
190/// - `saved_env`: The original environment variable value before the
191/// first override was applied (or `None` if it did not exist).
192/// - `frames`: The stack of active override frames, with the top
193/// being the last element in the vector.
194struct OverrideStack {
195 /// The name of the process environment variable associated with
196 /// this configuration key, if any.
197 ///
198 /// Used to mirror changes to the environment when overrides are
199 /// applied or removed. `None` if the key has no
200 /// `CONFIG.env_name`.
201 env_var: Option<String>,
202
203 /// The original value of the environment variable before the
204 /// first override was applied.
205 ///
206 /// Stored so it can be restored once the last frame is dropped.
207 /// `None` means the variable did not exist prior to overriding.
208 saved_env: Option<String>,
209
210 /// The sequence of active override frames for this key.
211 ///
212 /// Each frame represents one active test override; the last
213 /// element (`frames.last()`) is the current top-of-stack and
214 /// defines the effective value seen in the configuration and
215 /// environment.
216 frames: Vec<OverrideFrame>,
217}
218
219/// A single entry in a per-key override stack.
220///
221/// Each `OverrideFrame` represents one active test override applied
222/// via [`ConfigLock::override_key`]. Frames are uniquely identified
223/// by a monotonically increasing token and record both the value
224/// being overridden and its string form for environment mirroring.
225///
226/// When a guard drops, its frame is removed from the stack; if it was
227/// the top, the next frame (if any) becomes active, or the original
228/// environment is restored when the stack becomes empty.
229struct OverrideFrame {
230 /// A unique, monotonically increasing identifier for this
231 /// override frame.
232 ///
233 /// Used to associate a dropping [`ConfigValueGuard`] with its
234 /// corresponding entry in the stack, even if drops occur out of
235 /// order.
236 token: u64,
237
238 /// The serialized configuration value active while this frame is
239 /// on top of its stack.
240 ///
241 /// Stored as a boxed [`SerializableValue`] to match how values
242 /// are kept within [`Attrs`].
243 value: Box<dyn crate::attrs::SerializableValue>,
244
245 /// Pre-rendered string form of the value, used for environment
246 /// variable updates when this frame becomes active.
247 ///
248 /// Avoids recomputing `value.display()` on every push or pop.
249 env_str: String,
250}
251
252/// Return the [`Source`] corresponding to a given [`Layer`].
253///
254/// This provides a uniform way to retrieve a layer's logical source
255/// (File, Env, Runtime, TestOverride, or ClientOverride) regardless
256/// of its internal representation. Used for sorting layers by
257/// priority and for source-based lookups or removals.
258fn layer_source(l: &Layer) -> Source {
259 match l {
260 Layer::File(_) => Source::File,
261 Layer::Env(_) => Source::Env,
262 Layer::Runtime(_) => Source::Runtime,
263 Layer::TestOverride { .. } => Source::TestOverride,
264 Layer::ClientOverride(_) => Source::ClientOverride,
265 }
266}
267
268/// Return an immutable reference to the [`Attrs`] contained in a
269/// [`Layer`].
270///
271/// This abstracts over the specific layer variant so callers can read
272/// configuration values uniformly without needing to pattern-match on
273/// the layer type. For `TestOverride`, this returns the current
274/// top-level attributes reflecting the active overrides.
275fn layer_attrs(l: &Layer) -> &Attrs {
276 match l {
277 Layer::File(a) | Layer::Env(a) | Layer::Runtime(a) | Layer::ClientOverride(a) => a,
278 Layer::TestOverride { attrs, .. } => attrs,
279 }
280}
281
282/// Return a mutable reference to the [`Attrs`] contained in a
283/// [`Layer`].
284///
285/// This allows callers to modify configuration values within any
286/// layer without needing to pattern-match on its variant. For
287/// `TestOverride`, the returned [`Attrs`] always reflect the current
288/// top-of-stack overrides for each key.
289fn layer_attrs_mut(l: &mut Layer) -> &mut Attrs {
290 match l {
291 Layer::File(a) | Layer::Env(a) | Layer::Runtime(a) | Layer::ClientOverride(a) => a,
292 Layer::TestOverride { attrs, .. } => attrs,
293 }
294}
295
296impl Layers {
297 // Mutation methods:
298
299 /// Insert or replace a configuration layer for the given source.
300 ///
301 /// If a layer with the same [`Source`] already exists, its contents
302 /// are replaced with the provided `attrs`. Otherwise a new layer is
303 /// added. After insertion, layers are re-sorted so that
304 /// higher-priority sources (e.g. [`Source::TestOverride`],
305 /// [`Source::Env`]) appear before lower-priority ones
306 /// ([`Source::Runtime`], [`Source::File`]).
307 fn set(&mut self, source: Source, attrs: Attrs) {
308 if let Some(l) = self.ordered.iter_mut().find(|l| layer_source(l) == source) {
309 *layer_attrs_mut(l) = attrs;
310 } else {
311 self.ordered.push(make_layer(source, attrs));
312 }
313 self.ordered.sort_by_key(|l| priority(layer_source(l)));
314 }
315
316 /// Insert or update a configuration layer for the given [`Source`].
317 ///
318 /// If a layer with the same [`Source`] already exists, its attributes
319 /// are **updated in place**: all keys present in `attrs` are absorbed
320 /// into the existing layer, overwriting any previous values for those
321 /// keys while leaving all other keys in that layer unchanged.
322 ///
323 /// If no layer for `source` exists yet, this behaves like [`set`]: a
324 /// new layer is created with the provided `attrs`.
325 fn merge(&mut self, source: Source, attrs: Attrs) {
326 if let Some(layer) = self.ordered.iter_mut().find(|l| layer_source(l) == source) {
327 layer_attrs_mut(layer).merge(attrs);
328 } else {
329 self.ordered.push(make_layer(source, attrs));
330 }
331 self.ordered.sort_by_key(|l| priority(layer_source(l)));
332 }
333
334 /// Remove the configuration layer for the given [`Source`], if
335 /// present.
336 ///
337 /// After this call, values from that source will no longer contribute
338 /// to resolution in [`get`], [`get_cloned`], or [`attrs`]. Defaults
339 /// and any remaining layers continue to apply in their normal
340 /// priority order.
341 fn clear(&mut self, source: Source) {
342 self.ordered.retain(|l| layer_source(l) != source);
343 }
344
345 /// Reset the global configuration to only Defaults (for testing).
346 ///
347 /// This clears all explicit layers (`File`, `Env`, `Runtime`,
348 /// `ClientOverride`, and `TestOverride`). Subsequent lookups will
349 /// resolve keys entirely from their declared defaults.
350 fn reset(&mut self) {
351 self.ordered.clear();
352 }
353
354 // Read methods:
355
356 /// Return a complete, merged snapshot of the effective configuration
357 /// **(only keys marked with `@meta(CONFIG = ...)`)**.
358 ///
359 /// Resolution per key:
360 /// 1) First explicit value found in layers (TestOverride → Env →
361 /// Runtime → File → ClientOverride).
362 /// 2) Otherwise, the key's default (if any).
363 fn materialize(&self) -> Attrs {
364 let mut merged = Attrs::new();
365 for info in inventory::iter::<AttrKeyInfo>() {
366 if info.meta.get(CONFIG).is_none() {
367 continue;
368 }
369 let name = info.name;
370 let mut chosen: Option<Box<dyn crate::attrs::SerializableValue>> = None;
371 for layer in &self.ordered {
372 if let Some(v) = layer_attrs(layer).get_value_by_name(name) {
373 chosen = Some(v.cloned());
374 break;
375 }
376 }
377 let boxed = match chosen {
378 Some(b) => b,
379 None => {
380 if let Some(default) = info.default {
381 default.cloned()
382 } else {
383 continue;
384 }
385 }
386 };
387 merged.insert_value_by_name_unchecked(name, boxed);
388 }
389 merged
390 }
391
392 /// Return a complete, merged snapshot of the effective configuration
393 /// **(only keys marked with `@meta(CONFIG = ...)` and `propagate: true`)**.
394 ///
395 /// This is similar to [`materialize`] but excludes keys with
396 /// `propagate: false`. Use this when sending config to child processes
397 /// via `BootstrapProcManager`.
398 fn materialize_propagatable(&self) -> Attrs {
399 let mut merged = Attrs::new();
400 for info in inventory::iter::<AttrKeyInfo>() {
401 let Some(cfg_meta) = info.meta.get(CONFIG) else {
402 continue;
403 };
404 if !cfg_meta.propagate {
405 continue;
406 }
407 let name = info.name;
408 let mut chosen: Option<Box<dyn crate::attrs::SerializableValue>> = None;
409 for layer in &self.ordered {
410 if let Some(v) = layer_attrs(layer).get_value_by_name(name) {
411 chosen = Some(v.cloned());
412 break;
413 }
414 }
415 let boxed = match chosen {
416 Some(b) => b,
417 None => {
418 if let Some(default) = info.default {
419 default.cloned()
420 } else {
421 continue;
422 }
423 }
424 };
425 merged.insert_value_by_name_unchecked(name, boxed);
426 }
427 merged
428 }
429
430 /// Return a snapshot of the attributes for a specific configuration
431 /// source.
432 ///
433 /// If a layer with the given [`Source`] exists, this clones and
434 /// returns its [`Attrs`]. Otherwise an empty [`Attrs`] is returned.
435 fn layer_attrs_for(&self, source: Source) -> Attrs {
436 if let Some(layer) = self.ordered.iter().find(|l| layer_source(l) == source) {
437 layer_attrs(layer).clone()
438 } else {
439 Attrs::new()
440 }
441 }
442
443 // Test override support:
444
445 /// Ensure TestOverride layer exists, return mutable access to its
446 /// attrs and stacks.
447 fn ensure_test_override(&mut self) -> (&mut Attrs, &mut HashMap<&'static str, OverrideStack>) {
448 let idx = if let Some(i) = self
449 .ordered
450 .iter()
451 .position(|l| matches!(l, Layer::TestOverride { .. }))
452 {
453 i
454 } else {
455 self.ordered.push(Layer::TestOverride {
456 attrs: Attrs::new(),
457 stacks: HashMap::new(),
458 });
459 self.ordered.sort_by_key(|l| priority(layer_source(l)));
460 self.ordered
461 .iter()
462 .position(|l| matches!(l, Layer::TestOverride { .. }))
463 .expect("just inserted TestOverride layer")
464 };
465 match &mut self.ordered[idx] {
466 Layer::TestOverride { attrs, stacks } => (attrs, stacks),
467 _ => unreachable!(),
468 }
469 }
470
471 /// Get mutable access to TestOverride layer if it exists.
472 fn test_override_mut(
473 &mut self,
474 ) -> Option<(&mut Attrs, &mut HashMap<&'static str, OverrideStack>)> {
475 let idx = self
476 .ordered
477 .iter()
478 .position(|l| matches!(l, Layer::TestOverride { .. }))?;
479 match &mut self.ordered[idx] {
480 Layer::TestOverride { attrs, stacks } => Some((attrs, stacks)),
481 _ => None,
482 }
483 }
484
485 /// Remove the TestOverride layer entirely.
486 fn remove_test_override(&mut self) {
487 self.ordered
488 .retain(|l| !matches!(l, Layer::TestOverride { .. }));
489 }
490}
491
492/// Global configuration state combining layers and materialized
493/// snapshot.
494///
495/// This struct holds both the mutable layer stack (protected by
496/// RwLock) and the pre-materialized snapshot (in ArcSwap) together to
497/// avoid initialization order dependencies.
498struct GlobalConfig {
499 /// The layered configuration store.
500 layers: RwLock<Layers>,
501 /// Pre-materialized snapshot for lock-free reads.
502 materialized: ArcSwap<Attrs>,
503}
504
505/// Global layered configuration store.
506///
507/// This is the single authoritative store for configuration in the
508/// process. It is always present, protected by an `RwLock`, and holds
509/// a [`Layers`] struct containing all active sources.
510///
511/// On startup it is seeded with a single [`Source::Env`] layer
512/// (values loaded from process environment variables). Additional
513/// layers can be installed later via [`set`] or cleared with
514/// [`clear`]. Reads (`get`, `get_cloned`, `attrs`) consult the layers
515/// in priority order.
516///
517/// In tests, a [`Source::TestOverride`] layer is pushed on demand by
518/// [`ConfigLock::override_key`]. This layer always takes precedence
519/// and is automatically removed when the guard drops.
520///
521/// In normal operation, a parent process may capture its config with
522/// [`attrs`] and pass it to a child during bootstrap. The child
523/// installs this snapshot as its [`Source::ClientOverride`] layer,
524/// which has the lowest precedence among explicit layers.
525static GLOBAL: LazyLock<GlobalConfig> = LazyLock::new(|| {
526 let env = from_env();
527 let layers = Layers {
528 ordered: vec![Layer::Env(env)],
529 };
530 let materialized = ArcSwap::new(Arc::new(layers.materialize()));
531 GlobalConfig {
532 layers: RwLock::new(layers),
533 materialized,
534 }
535});
536
537/// Update the materialized snapshot from the current layers.
538///
539/// Must be called while holding `GLOBAL.layers.write()` to ensure the
540/// snapshot is consistent with the layers.
541fn rematerialize(layers: &Layers) {
542 GLOBAL.materialized.store(Arc::new(layers.materialize()));
543}
544
545/// Monotonically increasing sequence used to assign unique tokens to
546/// each test override frame.
547///
548/// Tokens identify individual [`ConfigValueGuard`] instances within a
549/// key's override stack, allowing frames to be removed safely even
550/// when guards are dropped out of order. The counter starts at 1 and
551/// uses relaxed atomic ordering since exact sequencing across threads
552/// is not required—only uniqueness.
553static OVERRIDE_TOKEN_SEQ: AtomicU64 = AtomicU64::new(1);
554
555/// Acquire the global configuration lock.
556///
557/// This lock serializes all mutations of the global configuration,
558/// ensuring they cannot clobber each other. It returns a
559/// [`ConfigLock`] guard, which must be held for the duration of any
560/// mutation (e.g. inserting or overriding values).
561///
562/// Most commonly used in tests, where it provides exclusive access to
563/// push a [`Source::TestOverride`] layer via
564/// [`ConfigLock::override_key`]. The override layer is automatically
565/// removed when the guard drops, restoring the original state.
566///
567/// # Example
568/// ```rust,ignore
569/// let lock = hyperactor::config::global::lock();
570/// let _guard = lock.override_key(CONFIG_KEY, "test_value");
571/// // Code under test sees the overridden config.
572/// // On drop, the key is restored.
573/// ```
574pub fn lock() -> ConfigLock {
575 static MUTEX: LazyLock<std::sync::Mutex<()>> = LazyLock::new(|| std::sync::Mutex::new(()));
576 ConfigLock {
577 _guard: MUTEX.lock().unwrap_or_else(|e| e.into_inner()),
578 }
579}
580
581/// Initialize the global configuration from environment variables.
582///
583/// Reads values from process environment variables, using each key's
584/// `CONFIG.env_name` (from `@meta(CONFIG = ConfigAttr { … })`) to
585/// determine its mapping. The resulting values are installed as the
586/// [`Source::Env`] layer. Keys without a corresponding environment
587/// variable fall back to lower-priority sources or defaults.
588///
589/// Typically invoked once at process startup to overlay config values
590/// from the environment. Repeated calls replace the existing Env
591/// layer.
592pub fn init_from_env() {
593 set(Source::Env, from_env());
594}
595
596/// Initialize the global configuration from a YAML file.
597///
598/// Loads values from the specified YAML file and installs them as the
599/// [`Source::File`] layer. During resolution, File is consulted after
600/// TestOverride, Env, and Runtime layers, and before ClientOverride
601/// and defaults.
602///
603/// Typically invoked once at process startup to provide a baseline
604/// configuration. Repeated calls replace the existing File layer.
605pub fn init_from_yaml<P: AsRef<Path>>(path: P) -> Result<(), anyhow::Error> {
606 let file = from_yaml(path)?;
607 set(Source::File, file);
608 Ok(())
609}
610
611/// Get a key from the global configuration (Copy types).
612///
613/// Resolution order: TestOverride -> Env -> Runtime -> File ->
614/// ClientOverride -> Default. Panics if the key has no default and is
615/// not set in any layer.
616///
617/// This function reads from a pre-materialized snapshot for lock-free
618/// performance. The snapshot is updated atomically whenever layers
619/// change.
620pub fn get<T: AttrValue + Copy>(key: Key<T>) -> T {
621 let snapshot = GLOBAL.materialized.load();
622 *snapshot.get(key).expect("key must have a default")
623}
624
625/// Return the override value for `key` if it is explicitly present in
626/// `overrides`, otherwise fall back to the global value for that key.
627pub fn override_or_global<T: AttrValue + Copy>(overrides: &Attrs, key: Key<T>) -> T {
628 if overrides.contains_key(key) {
629 *overrides.get(key).unwrap()
630 } else {
631 get(key)
632 }
633}
634
635/// Get a key by cloning the value.
636///
637/// Resolution order: TestOverride -> Env -> Runtime -> File ->
638/// ClientOverride -> Default. Panics if the key has no default and
639/// is not set in any layer.
640pub fn get_cloned<T: AttrValue>(key: Key<T>) -> T {
641 try_get_cloned(key)
642 .expect("key must have a default")
643 .clone()
644}
645
646/// Try to get a key by cloning the value.
647///
648/// Resolution order: TestOverride -> Env -> Runtime -> File ->
649/// ClientOverride -> Default. Returns None if the key has no default
650/// and is not set in any layer.
651///
652/// This function reads from a pre-materialized snapshot for lock-free
653/// performance.
654pub fn try_get_cloned<T: AttrValue>(key: Key<T>) -> Option<T> {
655 let snapshot = GLOBAL.materialized.load();
656 snapshot.get(key).cloned()
657}
658
659/// Construct a [`Layer`] for the given [`Source`] using the provided
660/// `attrs`.
661///
662/// Used by [`set`] and [`create_or_merge`] when installing a new
663/// configuration layer.
664fn make_layer(source: Source, attrs: Attrs) -> Layer {
665 match source {
666 Source::File => Layer::File(attrs),
667 Source::Env => Layer::Env(attrs),
668 Source::Runtime => Layer::Runtime(attrs),
669 Source::TestOverride => Layer::TestOverride {
670 attrs,
671 stacks: HashMap::new(),
672 },
673 Source::ClientOverride => Layer::ClientOverride(attrs),
674 Source::Default => {
675 // Default is a reporting-only source used in ConfigEntry;
676 // it does not correspond to a real layer.
677 unreachable!("Source::Default is not a real layer — it cannot be used in make_layer")
678 }
679 }
680}
681
682/// Insert or replace a configuration layer for the given source.
683///
684/// If a layer with the same [`Source`] already exists, its contents
685/// are replaced with the provided `attrs`. Otherwise a new layer is
686/// added. After insertion, layers are re-sorted so that
687/// higher-priority sources (e.g. [`Source::TestOverride`],
688/// [`Source::Env`]) appear before lower-priority ones
689/// ([`Source::Runtime`], [`Source::File`]).
690///
691/// This function is used by initialization routines (e.g.
692/// `init_from_env`, `init_from_yaml`) and by tests when overriding
693/// configuration values.
694pub fn set(source: Source, attrs: Attrs) {
695 let mut g = GLOBAL.layers.write().unwrap();
696 g.set(source, attrs);
697 rematerialize(&g);
698}
699
700/// Insert or update a configuration layer for the given [`Source`].
701///
702/// If a layer with the same [`Source`] already exists, its attributes
703/// are **updated in place**: all keys present in `attrs` are absorbed
704/// into the existing layer, overwriting any previous values for those
705/// keys while leaving all other keys in that layer unchanged.
706///
707/// If no layer for `source` exists yet, this behaves like [`set`]: a
708/// new layer is created with the provided `attrs`.
709///
710/// This is useful for incremental / additive updates (for example,
711/// runtime configuration driven by a Python API), where callers want
712/// to change a subset of keys without discarding previously installed
713/// values in the same layer.
714///
715/// By contrast, [`set`] replaces the entire layer for `source` with
716/// `attrs`, discarding any existing values in that layer.
717pub fn create_or_merge(source: Source, attrs: Attrs) {
718 let mut g = GLOBAL.layers.write().unwrap();
719 g.merge(source, attrs);
720 rematerialize(&g);
721}
722
723/// Remove the configuration layer for the given [`Source`], if
724/// present.
725///
726/// After this call, values from that source will no longer contribute
727/// to resolution in [`get`], [`get_cloned`], or [`attrs`]. Defaults
728/// and any remaining layers continue to apply in their normal
729/// priority order.
730pub fn clear(source: Source) {
731 let mut g = GLOBAL.layers.write().unwrap();
732 g.clear(source);
733 rematerialize(&g);
734}
735
736/// Return a complete, merged snapshot of the effective configuration
737/// **(only keys marked with `@meta(CONFIG = ...)`)**.
738///
739/// Resolution per key:
740/// 1) First explicit value found in layers (TestOverride → Env →
741/// Runtime → File → ClientOverride).
742/// 2) Otherwise, the key's default (if any).
743///
744/// Notes:
745/// - This materializes defaults into the returned Attrs for all
746/// CONFIG-marked keys, so it's self-contained.
747/// - Keys without `CONFIG` meta are excluded.
748pub fn attrs() -> Attrs {
749 GLOBAL.layers.read().unwrap().materialize()
750}
751
752/// Return a complete, merged snapshot of the effective configuration
753/// **(only keys marked with `@meta(CONFIG = ...)` and `propagate: true`)**.
754///
755/// Resolution per key:
756/// 1) First explicit value found in layers (TestOverride → Env →
757/// Runtime → File → ClientOverride).
758/// 2) Otherwise, the key's default (if any).
759///
760/// Notes:
761/// - This materializes defaults into the returned Attrs for all
762/// CONFIG-marked keys with `propagate: true`.
763/// - Keys without `CONFIG` meta are excluded.
764/// - Keys with `propagate: false` are excluded.
765///
766/// Use this when sending config to child processes via
767/// `BootstrapProcManager`. Process-local configs (like TLS cert paths)
768/// should have `propagate: false` and will not be included.
769pub fn propagatable_attrs() -> Attrs {
770 GLOBAL.layers.read().unwrap().materialize_propagatable()
771}
772
773/// A single config entry with its resolved value and provenance.
774///
775/// Used by the admin config-inspection endpoint to report per-proc
776/// configuration state including which layer the effective value
777/// came from.
778#[derive(Debug, Clone, Serialize, Deserialize)]
779pub struct ConfigEntry {
780 /// Fully qualified key name (e.g. `hyperactor::config::codec_max_frame_length`).
781 pub name: String,
782 /// Display representation of the resolved value.
783 pub value: String,
784 /// Display representation of the declared default, if any.
785 pub default_value: Option<String>,
786 /// Which layer the resolved value came from.
787 pub source: Source,
788 /// True when the resolved display value differs from the declared default.
789 pub changed_from_default: bool,
790 /// Environment variable name, if the key declares one.
791 pub env_var: Option<String>,
792}
793
794/// Snapshot all CONFIG-marked keys with their resolved values and
795/// source layers.
796///
797/// Iterates the global key registry, filters to `@meta(CONFIG = ...)`
798/// keys, and for each key walks layers top-to-bottom to find which
799/// layer provides the effective value. Results are sorted by name.
800pub fn config_entries() -> Vec<ConfigEntry> {
801 let g = GLOBAL.layers.read().unwrap();
802 let mut entries = Vec::new();
803
804 for info in inventory::iter::<AttrKeyInfo>() {
805 let Some(cfg_meta) = info.meta.get(CONFIG) else {
806 continue;
807 };
808
809 let name = info.name;
810
811 // Walk layers to find the effective value and its source.
812 let mut chosen_value: Option<Box<dyn crate::attrs::SerializableValue>> = None;
813 let mut chosen_source: Option<Source> = None;
814 for layer in &g.ordered {
815 if let Some(v) = layer_attrs(layer).get_value_by_name(name) {
816 chosen_value = Some(v.cloned());
817 chosen_source = Some(layer_source(layer));
818 break;
819 }
820 }
821
822 // Fall back to declared default if no layer provides a value.
823 let (value_str, source) = match chosen_value {
824 Some(v) => ((info.display)(&*v), chosen_source.unwrap()),
825 None => {
826 if let Some(default) = info.default {
827 ((info.display)(default), Source::Default)
828 } else {
829 continue;
830 }
831 }
832 };
833
834 // Compute default display string for changed_from_default comparison.
835 let default_str = info.default.map(|d| (info.display)(d));
836 let changed_from_default = match &default_str {
837 Some(ds) => value_str != *ds,
838 None => true, // no declared default → always "changed"
839 };
840
841 entries.push(ConfigEntry {
842 name: name.to_string(),
843 value: value_str,
844 default_value: default_str,
845 source,
846 changed_from_default,
847 env_var: cfg_meta.env_name.clone(),
848 });
849 }
850
851 entries.sort_by(|a, b| a.name.cmp(&b.name));
852 entries
853}
854
855/// Return a snapshot of the attributes for a specific configuration
856/// source.
857///
858/// If a layer with the given [`Source`] exists, this clones and
859/// returns its [`Attrs`]. Otherwise an empty [`Attrs`] is returned.
860/// The returned map is detached from the global store – mutating it
861/// does **not** affect the underlying layer; use [`set`] or
862/// [`create_or_merge`] to modify layers.
863fn layer_attrs_for(source: Source) -> Attrs {
864 GLOBAL.layers.read().unwrap().layer_attrs_for(source)
865}
866
867/// Snapshot the current attributes in the **Runtime** configuration
868/// layer.
869///
870/// This returns a cloned [`Attrs`] containing only values explicitly
871/// set in the [`Source::Runtime`] layer (no merging with
872/// Env/File/Defaults). If no Runtime layer is present, an empty
873/// [`Attrs`] is returned.
874pub fn runtime_attrs() -> Attrs {
875 layer_attrs_for(Source::Runtime)
876}
877
878/// Reset the global configuration to only Defaults (for testing).
879///
880/// This clears all explicit layers (`File`, `Env`, `Runtime`,
881/// `ClientOverride`, and `TestOverride`). Subsequent lookups will
882/// resolve keys entirely from their declared defaults.
883///
884/// Note: Should be called while holding [`global::lock`] in tests, to
885/// ensure no concurrent modifications happen.
886pub fn reset_to_defaults() {
887 let mut g = GLOBAL.layers.write().unwrap();
888 g.reset();
889 rematerialize(&g);
890}
891
892/// A guard that holds the global configuration lock and provides
893/// override functionality.
894///
895/// This struct acts as both a lock guard (preventing other tests from
896/// modifying global config) and as the only way to create
897/// configuration overrides. Override guards cannot outlive this
898/// ConfigLock, ensuring proper synchronization.
899pub struct ConfigLock {
900 _guard: std::sync::MutexGuard<'static, ()>,
901}
902
903impl ConfigLock {
904 /// Create a configuration override that is active until the
905 /// returned guard is dropped.
906 ///
907 /// Each call pushes a new frame onto a per-key override stack
908 /// within the [`Source::TestOverride`] layer. The topmost frame
909 /// defines the effective value seen by `get()` and in the
910 /// mirrored environment variable (if any). When a guard is
911 /// dropped, its frame is removed: if it was the top, the previous
912 /// frame (if any) becomes active or the key and env var are
913 /// restored to their prior state.
914 ///
915 /// The returned guard must not outlive this [`ConfigLock`].
916 pub fn override_key<'a, T: AttrValue>(
917 &'a self,
918 key: crate::attrs::Key<T>,
919 value: T,
920 ) -> ConfigValueGuard<'a, T> {
921 let token = OVERRIDE_TOKEN_SEQ.fetch_add(1, Ordering::Relaxed);
922
923 let mut g = GLOBAL.layers.write().unwrap();
924
925 {
926 // Ensure TestOverride layer exists and get mutable access.
927 let (attrs, stacks) = g.ensure_test_override();
928
929 // Compute env var (if any) for this key once.
930 let (env_var, env_str) = if let Some(cfg) = key.attrs().get(crate::CONFIG) {
931 if let Some(name) = &cfg.env_name {
932 (Some(name.clone()), value.display())
933 } else {
934 (None, String::new())
935 }
936 } else {
937 (None, String::new())
938 };
939
940 // Get per-key stack (by declared name).
941 let key_name = key.name();
942 let stack = stacks.entry(key_name).or_insert_with(|| OverrideStack {
943 env_var: env_var.clone(),
944 saved_env: env_var.as_ref().and_then(|n| std::env::var(n).ok()),
945 frames: Vec::new(),
946 });
947
948 // Push the new frame.
949 let boxed: Box<dyn crate::attrs::SerializableValue> = Box::new(value.clone());
950 stack.frames.push(OverrideFrame {
951 token,
952 value: boxed,
953 env_str,
954 });
955
956 // Make this frame the active value in TestOverride attrs.
957 attrs.set(key, value.clone());
958
959 // Update process env to reflect new top-of-stack.
960 if let (Some(var), Some(top)) = (stack.env_var.as_ref(), stack.frames.last()) {
961 // SAFETY: Under global ConfigLock during tests.
962 unsafe { std::env::set_var(var, &top.env_str) }
963 }
964 }
965
966 rematerialize(&g);
967
968 ConfigValueGuard {
969 key,
970 token,
971 _phantom: PhantomData,
972 }
973 }
974}
975
976/// When a [`ConfigLock`] is dropped, the special
977/// [`Source::TestOverride`] layer (if present) is removed entirely.
978/// This discards all temporary overrides created under the lock,
979/// ensuring they cannot leak into subsequent tests or callers. Other
980/// layers (`Runtime`, `Env`, `File`, `ClientOverride`, and defaults)
981/// are left untouched.
982///
983/// Note: individual values within the TestOverride layer may already
984/// have been restored by [`ConfigValueGuard`]s as they drop. This
985/// final removal guarantees no residual layer remains once the lock
986/// itself is released.
987impl Drop for ConfigLock {
988 fn drop(&mut self) {
989 let mut g = GLOBAL.layers.write().unwrap();
990 g.remove_test_override();
991 rematerialize(&g);
992 }
993}
994
995/// A guard that restores a single configuration value when dropped.
996pub struct ConfigValueGuard<'a, T: 'static> {
997 key: crate::attrs::Key<T>,
998 token: u64,
999 // This is here so we can hold onto a 'a lifetime.
1000 _phantom: PhantomData<&'a ()>,
1001}
1002
1003/// When a [`ConfigValueGuard`] is dropped, it restores configuration
1004/// state for the key it was guarding.
1005///
1006/// Behavior:
1007/// - Each key maintains a stack of override frames. The most recent
1008/// frame (top of stack) defines the effective value in
1009/// [`Source::TestOverride`].
1010/// - Dropping a guard removes its frame. If it was the top frame, the
1011/// next frame (if any) becomes active and both the config and
1012/// mirrored env var are updated accordingly.
1013/// - If the dropped frame was not on top, no changes occur until the
1014/// active frame is dropped.
1015/// - When the last frame for a key is removed, the key is deleted
1016/// from the TestOverride layer and its associated environment
1017/// variable (if any) is restored to its original value or removed
1018/// if it did not exist.
1019///
1020/// This guarantees that nested or out-of-order test overrides are
1021/// restored deterministically and without leaking state into
1022/// subsequent tests.
1023impl<T: 'static> Drop for ConfigValueGuard<'_, T> {
1024 fn drop(&mut self) {
1025 let mut g = GLOBAL.layers.write().unwrap();
1026
1027 // Track whether the config actually changed (for rematerialization).
1028 let mut config_changed = false;
1029
1030 // Env var restoration info (captured inside the block, applied
1031 // outside).
1032 let mut restore_env_var: Option<String> = None;
1033 let mut restore_env_to: Option<String> = None;
1034
1035 if let Some((attrs, stacks)) = g.test_override_mut() {
1036 let key_name = self.key.name();
1037
1038 // We need a tiny scope for the &mut borrow of the stack so
1039 // we can call `stacks.remove(key_name)` afterward if it
1040 // becomes empty.
1041 let mut remove_empty_stack = false;
1042
1043 if let Some(stack) = stacks.get_mut(key_name) {
1044 // Find this guard's frame by token.
1045 if let Some(pos) = stack.frames.iter().position(|f| f.token == self.token) {
1046 let is_top = pos + 1 == stack.frames.len();
1047
1048 if is_top {
1049 // Pop the active frame
1050 stack.frames.pop();
1051 config_changed = true;
1052
1053 if let Some(new_top) = stack.frames.last() {
1054 // New top becomes active: update attrs and env.
1055 attrs.insert_value(self.key, (*new_top.value).cloned());
1056 if let Some(var) = stack.env_var.as_ref() {
1057 // SAFETY: Under global ConfigLock during tests.
1058 unsafe { std::env::set_var(var, &new_top.env_str) }
1059 }
1060 } else {
1061 // Stack empty: remove the key now, then after
1062 // releasing the &mut borrow of the stack,
1063 // restore the env var and remove the stack
1064 // entry.
1065 let _ = attrs.remove_value(self.key);
1066
1067 // Capture restoration details while we still
1068 // have access to the stack.
1069 if let Some(var) = stack.env_var.as_ref() {
1070 restore_env_var = Some(var.clone());
1071 restore_env_to = stack.saved_env.clone(); // None => unset
1072 }
1073 remove_empty_stack = true
1074 }
1075 } else {
1076 // Out-of-order drop: remove only that frame:
1077 // active top stays
1078 stack.frames.remove(pos);
1079 // No changes to attrs or env here (and no
1080 // rematerialization needed).
1081 }
1082 } // else: token already handled; nothing to do
1083 } // &mut stack borrow ends here
1084
1085 // If we emptied the stack for this key, remove the stack
1086 // entry.
1087 if remove_empty_stack {
1088 let _ = stacks.remove(key_name);
1089 }
1090 }
1091
1092 // Restore env var outside the borrow scope.
1093 if let Some(var) = restore_env_var.as_ref() {
1094 // SAFETY: Under global ConfigLock during tests.
1095 unsafe {
1096 if let Some(val) = restore_env_to.as_ref() {
1097 std::env::set_var(var, val);
1098 } else {
1099 std::env::remove_var(var);
1100 }
1101 }
1102 }
1103
1104 // Rematerialize if the config actually changed.
1105 if config_changed {
1106 rematerialize(&g);
1107 }
1108 }
1109}
1110
1111#[cfg(test)]
1112mod tests {
1113 use std::time::Duration;
1114
1115 use super::*;
1116 use crate::ConfigAttr;
1117 use crate::attrs::declare_attrs;
1118
1119 // Test configuration keys used to exercise the layered config
1120 // infrastructure. These mirror hyperactor's config keys but are
1121 // declared locally to keep hyperactor_config independent.
1122
1123 declare_attrs! {
1124 /// Maximum frame length for codec
1125 @meta(CONFIG = ConfigAttr::new(
1126 Some("HYPERACTOR_CODEC_MAX_FRAME_LENGTH".to_string()),
1127 None,
1128 ))
1129 pub attr CODEC_MAX_FRAME_LENGTH: usize = 10 * 1024 * 1024 * 1024; // 10 GiB
1130
1131 /// Message delivery timeout
1132 @meta(CONFIG = ConfigAttr::new(
1133 Some("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT".to_string()),
1134 None,
1135 ))
1136 pub attr MESSAGE_DELIVERY_TIMEOUT: Duration = Duration::from_secs(30);
1137
1138 /// Number of messages after which to send an acknowledgment
1139 @meta(CONFIG = ConfigAttr::new(
1140 Some("HYPERACTOR_MESSAGE_ACK_EVERY_N_MESSAGES".to_string()),
1141 None,
1142 ))
1143 pub attr MESSAGE_ACK_EVERY_N_MESSAGES: u64 = 1000;
1144
1145 /// Maximum buffer size for split port messages
1146 @meta(CONFIG = ConfigAttr::new(
1147 Some("HYPERACTOR_SPLIT_MAX_BUFFER_SIZE".to_string()),
1148 None,
1149 ))
1150 pub attr SPLIT_MAX_BUFFER_SIZE: usize = 5;
1151
1152 /// Whether to use multipart encoding for network channel communications
1153 @meta(CONFIG = ConfigAttr::new(
1154 Some("HYPERACTOR_CHANNEL_MULTIPART".to_string()),
1155 None,
1156 ))
1157 pub attr CHANNEL_MULTIPART: bool = true;
1158
1159 /// Default hop Time-To-Live for message envelopes
1160 @meta(CONFIG = ConfigAttr::new(
1161 Some("HYPERACTOR_MESSAGE_TTL_DEFAULT".to_string()),
1162 None,
1163 ))
1164 pub attr MESSAGE_TTL_DEFAULT: u8 = 64;
1165
1166 /// A test key with no environment variable mapping
1167 @meta(CONFIG = ConfigAttr::new(
1168 None,
1169 None,
1170 ))
1171 pub attr CONFIG_KEY_NO_ENV: u32 = 100;
1172 }
1173
1174 #[test]
1175 fn test_global_config() {
1176 let config = lock();
1177
1178 // Reset global config to defaults to avoid interference from
1179 // other tests
1180 reset_to_defaults();
1181
1182 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
1183 {
1184 let _guard = config.override_key(CODEC_MAX_FRAME_LENGTH, 1024);
1185 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 1024);
1186 // The configuration will be automatically restored when
1187 // _guard goes out of scope
1188 }
1189
1190 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
1191 }
1192
1193 #[test]
1194 fn test_overrides() {
1195 let config = lock();
1196
1197 // Reset global config to defaults to avoid interference from
1198 // other tests
1199 reset_to_defaults();
1200
1201 // Test the new lock/override API for individual config values
1202 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
1203 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1204
1205 // Test single value override
1206 {
1207 let _guard = config.override_key(CODEC_MAX_FRAME_LENGTH, 2048);
1208 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 2048);
1209 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30)); // Unchanged
1210 }
1211
1212 // Values should be restored after guard is dropped
1213 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
1214
1215 // Test multiple overrides
1216 let orig_value = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").ok();
1217 {
1218 let _guard1 = config.override_key(CODEC_MAX_FRAME_LENGTH, 4096);
1219 let _guard2 = config.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_mins(1));
1220
1221 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 4096);
1222 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_mins(1));
1223 // This was overridden:
1224 assert_eq!(
1225 std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap(),
1226 "1m"
1227 );
1228 }
1229 assert_eq!(
1230 std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").ok(),
1231 orig_value
1232 );
1233
1234 // All values should be restored
1235 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
1236 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1237 }
1238
1239 #[test]
1240 fn test_layer_precedence_env_over_file_and_replacement() {
1241 let _lock = lock();
1242 reset_to_defaults();
1243
1244 // File sets a value.
1245 let mut file = Attrs::new();
1246 file[CODEC_MAX_FRAME_LENGTH] = 1111;
1247 set(Source::File, file);
1248
1249 // Env sets a different value.
1250 let mut env = Attrs::new();
1251 env[CODEC_MAX_FRAME_LENGTH] = 2222;
1252 set(Source::Env, env);
1253
1254 // Env should win over File.
1255 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 2222);
1256
1257 // Replace Env layer with a new value.
1258 let mut env2 = Attrs::new();
1259 env2[CODEC_MAX_FRAME_LENGTH] = 3333;
1260 set(Source::Env, env2);
1261
1262 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 3333);
1263 }
1264
1265 #[test]
1266 fn test_layer_precedence_read_file_if_not_found_in_env() {
1267 let _lock = lock();
1268 reset_to_defaults();
1269
1270 // Read the default value because no layers have been set.
1271 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 10737418240);
1272
1273 // File sets a value.
1274 let mut file = Attrs::new();
1275 file[CODEC_MAX_FRAME_LENGTH] = 1111;
1276 set(Source::File, file);
1277
1278 // Env does not have any attribute.
1279 let env = Attrs::new();
1280 set(Source::Env, env);
1281
1282 // Should read from File.
1283 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 1111);
1284
1285 // Replace Env layer with a new value.
1286 let mut env2 = Attrs::new();
1287 env2[CODEC_MAX_FRAME_LENGTH] = 2222;
1288 set(Source::Env, env2);
1289
1290 // Env should win over File.
1291 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 2222);
1292 }
1293
1294 #[test]
1295 fn test_runtime_overrides_and_clear_restores_lower_layers() {
1296 let _lock = lock();
1297 reset_to_defaults();
1298
1299 // File baseline.
1300 let mut file = Attrs::new();
1301 file[MESSAGE_DELIVERY_TIMEOUT] = Duration::from_secs(30);
1302 set(Source::File, file);
1303
1304 // Env override.
1305 let mut env = Attrs::new();
1306 env[MESSAGE_DELIVERY_TIMEOUT] = Duration::from_secs(40);
1307 set(Source::Env, env);
1308
1309 // Runtime layer (but Env beats it).
1310 let mut rt = Attrs::new();
1311 rt[MESSAGE_DELIVERY_TIMEOUT] = Duration::from_secs(50);
1312 set(Source::Runtime, rt);
1313
1314 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(40));
1315
1316 // Clearing Env should reveal Runtime.
1317 clear(Source::Env);
1318
1319 // With the Env layer gone, Runtime wins over File.
1320 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(50));
1321 }
1322
1323 #[test]
1324 fn test_attrs_snapshot_materializes_defaults_and_omits_meta() {
1325 let _lock = lock();
1326 reset_to_defaults();
1327
1328 // No explicit layers: values should come from Defaults.
1329 let snap = attrs();
1330
1331 // A few representative defaults are materialized:
1332 assert_eq!(snap[CODEC_MAX_FRAME_LENGTH], 10 * 1024 * 1024 * 1024);
1333 assert_eq!(snap[MESSAGE_DELIVERY_TIMEOUT], Duration::from_secs(30));
1334
1335 // CONFIG has no default and wasn't explicitly set: should be
1336 // omitted.
1337 let json = serde_json::to_string(&snap).unwrap();
1338 assert!(
1339 !json.contains("hyperactor::config::config"),
1340 "CONFIG must not appear in snapshot unless explicitly set"
1341 );
1342 }
1343
1344 #[test]
1345 fn test_parent_child_snapshot_as_clientoverride_layer() {
1346 let _lock = lock();
1347 reset_to_defaults();
1348
1349 // Parent effective config (pretend it's a parent process).
1350 let mut parent_env = Attrs::new();
1351 parent_env[MESSAGE_ACK_EVERY_N_MESSAGES] = 12345;
1352 set(Source::Env, parent_env);
1353
1354 let parent_snap = attrs();
1355
1356 // "Child" process: start clean, install parent snapshot as
1357 // ClientOverride.
1358 reset_to_defaults();
1359 set(Source::ClientOverride, parent_snap);
1360
1361 // Child should observe parent's effective value from the
1362 // ClientOverride layer (since child has no Env/Runtime/File
1363 // layers set).
1364 assert_eq!(get(MESSAGE_ACK_EVERY_N_MESSAGES), 12345);
1365 }
1366
1367 #[test]
1368 fn test_testoverride_layer_override_and_env_restore() {
1369 let lock = lock();
1370 reset_to_defaults();
1371
1372 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1373
1374 // SAFETY: single-threaded test.
1375 unsafe {
1376 std::env::remove_var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT");
1377 }
1378
1379 {
1380 let _guard = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(99));
1381 // Override wins:
1382 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(99));
1383
1384 // Env should be mirrored to the same duration (string may
1385 // be "1m 39s")
1386 let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1387 let parsed = humantime::parse_duration(&s).unwrap();
1388 assert_eq!(parsed, Duration::from_secs(99));
1389 }
1390
1391 // After drop, value and env restored:
1392 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1393 assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err());
1394 }
1395
1396 #[test]
1397 fn test_reset_to_defaults_clears_all_layers() {
1398 let _lock = lock();
1399 reset_to_defaults();
1400
1401 // Seed multiple layers.
1402 let mut file = Attrs::new();
1403 file[SPLIT_MAX_BUFFER_SIZE] = 7;
1404 set(Source::File, file);
1405
1406 let mut env = Attrs::new();
1407 env[SPLIT_MAX_BUFFER_SIZE] = 8;
1408 set(Source::Env, env);
1409
1410 let mut rt = Attrs::new();
1411 rt[SPLIT_MAX_BUFFER_SIZE] = 9;
1412 set(Source::Runtime, rt);
1413
1414 // Sanity: Env wins over Runtime and File.
1415 assert_eq!(get(SPLIT_MAX_BUFFER_SIZE), 8);
1416
1417 // Reset clears all explicit layers; defaults apply.
1418 reset_to_defaults();
1419 assert_eq!(get(SPLIT_MAX_BUFFER_SIZE), 5); // default
1420 }
1421
1422 #[test]
1423 fn test_get_cloned_resolution_matches_get() {
1424 let _lock = lock();
1425 reset_to_defaults();
1426
1427 let mut env = Attrs::new();
1428 env[MESSAGE_DELIVERY_TIMEOUT] = Duration::from_mins(2);
1429 set(Source::Env, env);
1430
1431 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_mins(2));
1432 let v = get_cloned(MESSAGE_DELIVERY_TIMEOUT);
1433 assert_eq!(v, Duration::from_mins(2));
1434 }
1435
1436 #[test]
1437 fn test_attrs_snapshot_respects_layer_precedence_per_key() {
1438 let _lock = lock();
1439 reset_to_defaults();
1440
1441 let mut file = Attrs::new();
1442 file[MESSAGE_TTL_DEFAULT] = 10;
1443 set(Source::File, file);
1444
1445 let mut env = Attrs::new();
1446 env[MESSAGE_TTL_DEFAULT] = 20;
1447 set(Source::Env, env);
1448
1449 let snap = attrs();
1450 assert_eq!(snap[MESSAGE_TTL_DEFAULT], 20); // Env beats File
1451 }
1452
1453 declare_attrs! {
1454 @meta(CONFIG = ConfigAttr::new(
1455 None,
1456 None,
1457 ))
1458 pub attr CONFIG_KEY: bool = true;
1459
1460 pub attr NON_CONFIG_KEY: bool = true;
1461
1462 @meta(CONFIG = ConfigAttr::new(
1463 None,
1464 None,
1465 ).process_local())
1466 pub attr NON_PROPAGATE_KEY: bool = true;
1467 }
1468
1469 #[test]
1470 fn test_attrs_excludes_non_config_keys() {
1471 let _lock = lock();
1472 reset_to_defaults();
1473
1474 let snap = attrs();
1475 let json = serde_json::to_string(&snap).unwrap();
1476
1477 // Expect our CONFIG_KEY to be present.
1478 assert!(
1479 json.contains("hyperactor_config::global::tests::config_key"),
1480 "attrs() should include keys with @meta(CONFIG = ...)"
1481 );
1482 // Expect our NON_CONFIG_KEY to be omitted.
1483 assert!(
1484 !json.contains("hyperactor_config::global::tests::non_config_key"),
1485 "attrs() should exclude keys without @meta(CONFIG = ...)"
1486 );
1487 }
1488
1489 #[test]
1490 fn test_propagatable_attrs_excludes_non_propagate_keys() {
1491 let _lock = lock();
1492 reset_to_defaults();
1493
1494 // attrs() should include NON_PROPAGATE_KEY (it has CONFIG meta)
1495 let snap = attrs();
1496 let json = serde_json::to_string(&snap).unwrap();
1497 assert!(
1498 json.contains("hyperactor_config::global::tests::non_propagate_key"),
1499 "attrs() should include keys with propagate: false"
1500 );
1501
1502 // propagatable_attrs() should exclude NON_PROPAGATE_KEY
1503 let propagatable = propagatable_attrs();
1504 let json_propagatable = serde_json::to_string(&propagatable).unwrap();
1505 assert!(
1506 !json_propagatable.contains("hyperactor_config::global::tests::non_propagate_key"),
1507 "propagatable_attrs() should exclude keys with propagate: false"
1508 );
1509
1510 // propagatable_attrs() should still include CONFIG_KEY (propagate: true)
1511 assert!(
1512 json_propagatable.contains("hyperactor_config::global::tests::config_key"),
1513 "propagatable_attrs() should include keys with propagate: true"
1514 );
1515 }
1516
1517 #[test]
1518 fn test_testoverride_multiple_stacked_overrides_lifo() {
1519 let lock = lock();
1520 reset_to_defaults();
1521
1522 // Baseline sanity.
1523 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1524
1525 // Start from a clean env so we can assert restoration to "unset".
1526 // SAFETY: single-threaded tests.
1527 unsafe {
1528 std::env::remove_var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT");
1529 }
1530 assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err());
1531
1532 // Stack A: 40s (becomes top)
1533 let guard_a = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(40));
1534 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(40));
1535 {
1536 let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1537 assert_eq!(
1538 humantime::parse_duration(&s).unwrap(),
1539 Duration::from_secs(40)
1540 );
1541 }
1542
1543 // Stack B: 50s (new top)
1544 let guard_b = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(50));
1545 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(50));
1546 {
1547 let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1548 assert_eq!(
1549 humantime::parse_duration(&s).unwrap(),
1550 Duration::from_secs(50)
1551 );
1552 }
1553
1554 // Drop B first → should reveal A (LIFO)
1555 std::mem::drop(guard_b);
1556 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(40));
1557 {
1558 let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1559 assert_eq!(
1560 humantime::parse_duration(&s).unwrap(),
1561 Duration::from_secs(40)
1562 );
1563 }
1564
1565 // Drop A → should restore default and unset env.
1566 std::mem::drop(guard_a);
1567 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1568 assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err());
1569 }
1570
1571 #[test]
1572 fn test_testoverride_out_of_order_drop_keeps_top_stable() {
1573 let lock = lock();
1574 reset_to_defaults();
1575
1576 // Clean env baseline.
1577 // SAFETY: single-threaded tests.
1578 unsafe {
1579 std::env::remove_var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT");
1580 }
1581 assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err());
1582
1583 // Push three frames in order: A=40s, B=50s, C=70s (C is top).
1584 let guard_a = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(40));
1585 let guard_b = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(50));
1586 let guard_c = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(70));
1587
1588 // Top is C.
1589 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(70));
1590 {
1591 let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1592 assert_eq!(
1593 humantime::parse_duration(&s).unwrap(),
1594 Duration::from_secs(70)
1595 );
1596 }
1597
1598 // Drop the *middle* frame (B) first → top must remain C, env unchanged.
1599 std::mem::drop(guard_b);
1600 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(70));
1601 {
1602 let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1603 assert_eq!(
1604 humantime::parse_duration(&s).unwrap(),
1605 Duration::from_secs(70)
1606 );
1607 }
1608
1609 // Now drop C → A becomes top, env follows A.
1610 std::mem::drop(guard_c);
1611 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(40));
1612 {
1613 let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1614 assert_eq!(
1615 humantime::parse_duration(&s).unwrap(),
1616 Duration::from_secs(40)
1617 );
1618 }
1619
1620 // Drop A → restore default and clear env.
1621 std::mem::drop(guard_a);
1622 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1623 assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err());
1624 }
1625
1626 #[test]
1627 fn test_priority_order() {
1628 use Source::*;
1629 assert!(priority(TestOverride) < priority(Env));
1630 assert!(priority(Env) < priority(Runtime));
1631 assert!(priority(Runtime) < priority(File));
1632 assert!(priority(File) < priority(ClientOverride));
1633 }
1634
1635 #[test]
1636 fn test_create_or_merge_runtime_merges_keys() {
1637 let _lock = lock();
1638 reset_to_defaults();
1639
1640 // Seed Runtime with one key.
1641 let mut rt = Attrs::new();
1642 rt[MESSAGE_TTL_DEFAULT] = 10;
1643 set(Source::Runtime, rt);
1644
1645 // Now update Runtime with a different key via
1646 // `create_or_merge`.
1647 let mut update = Attrs::new();
1648 update[MESSAGE_ACK_EVERY_N_MESSAGES] = 123;
1649 create_or_merge(Source::Runtime, update);
1650
1651 // Both keys should now be visible from Runtime.
1652 assert_eq!(get(MESSAGE_TTL_DEFAULT), 10);
1653 assert_eq!(get(MESSAGE_ACK_EVERY_N_MESSAGES), 123);
1654 }
1655
1656 #[test]
1657 fn test_create_or_merge_runtime_creates_layer_if_missing() {
1658 let _lock = lock();
1659 reset_to_defaults();
1660
1661 let mut rt = Attrs::new();
1662 rt[MESSAGE_TTL_DEFAULT] = 42;
1663 create_or_merge(Source::Runtime, rt);
1664
1665 assert_eq!(get(MESSAGE_TTL_DEFAULT), 42);
1666 }
1667
1668 #[test]
1669 fn test_clientoverride_precedence_loses_to_all_other_layers() {
1670 let _lock = lock();
1671 reset_to_defaults();
1672
1673 // ClientOverride sets a baseline value.
1674 let mut client = Attrs::new();
1675 client[MESSAGE_TTL_DEFAULT] = 10;
1676 set(Source::ClientOverride, client);
1677 assert_eq!(get(MESSAGE_TTL_DEFAULT), 10);
1678
1679 // File should beat ClientOverride.
1680 let mut file = Attrs::new();
1681 file[MESSAGE_TTL_DEFAULT] = 20;
1682 set(Source::File, file);
1683 assert_eq!(get(MESSAGE_TTL_DEFAULT), 20);
1684
1685 // Runtime should beat both File and ClientOverride.
1686 let mut runtime = Attrs::new();
1687 runtime[MESSAGE_TTL_DEFAULT] = 30;
1688 set(Source::Runtime, runtime);
1689 assert_eq!(get(MESSAGE_TTL_DEFAULT), 30);
1690
1691 // Env should beat Runtime, File, and ClientOverride.
1692 let mut env = Attrs::new();
1693 env[MESSAGE_TTL_DEFAULT] = 40;
1694 set(Source::Env, env);
1695 assert_eq!(get(MESSAGE_TTL_DEFAULT), 40);
1696
1697 // Clear higher layers one by one to verify fallback.
1698 clear(Source::Env);
1699 assert_eq!(get(MESSAGE_TTL_DEFAULT), 30); // Runtime
1700
1701 clear(Source::Runtime);
1702 assert_eq!(get(MESSAGE_TTL_DEFAULT), 20); // File
1703
1704 clear(Source::File);
1705 assert_eq!(get(MESSAGE_TTL_DEFAULT), 10); // ClientOverride
1706 }
1707
1708 #[test]
1709 fn test_create_or_merge_clientoverride() {
1710 let _lock = lock();
1711 reset_to_defaults();
1712
1713 // Seed ClientOverride with one key.
1714 let mut client = Attrs::new();
1715 client[MESSAGE_TTL_DEFAULT] = 10;
1716 set(Source::ClientOverride, client);
1717
1718 // Merge in a different key.
1719 let mut update = Attrs::new();
1720 update[MESSAGE_ACK_EVERY_N_MESSAGES] = 123;
1721 create_or_merge(Source::ClientOverride, update);
1722
1723 // Both keys should now be visible.
1724 assert_eq!(get(MESSAGE_TTL_DEFAULT), 10);
1725 assert_eq!(get(MESSAGE_ACK_EVERY_N_MESSAGES), 123);
1726 }
1727
1728 #[test]
1729 fn test_override_or_global_returns_override_when_present() {
1730 let _lock = lock();
1731 reset_to_defaults();
1732
1733 // Set a global value via Env.
1734 let mut env = Attrs::new();
1735 env[MESSAGE_TTL_DEFAULT] = 99;
1736 set(Source::Env, env);
1737
1738 // Create an override Attrs with a different value.
1739 let mut overrides = Attrs::new();
1740 overrides[MESSAGE_TTL_DEFAULT] = 42;
1741
1742 // Should return the override value, not global.
1743 assert_eq!(override_or_global(&overrides, MESSAGE_TTL_DEFAULT), 42);
1744 }
1745
1746 #[test]
1747 fn test_override_or_global_returns_global_when_not_present() {
1748 let _lock = lock();
1749 reset_to_defaults();
1750
1751 // Set a global value via Env.
1752 let mut env = Attrs::new();
1753 env[MESSAGE_TTL_DEFAULT] = 99;
1754 set(Source::Env, env);
1755
1756 // Empty overrides.
1757 let overrides = Attrs::new();
1758
1759 // Should return the global value.
1760 assert_eq!(override_or_global(&overrides, MESSAGE_TTL_DEFAULT), 99);
1761 }
1762
1763 #[test]
1764 fn test_runtime_attrs_returns_only_runtime_layer() {
1765 let _lock = lock();
1766 reset_to_defaults();
1767
1768 // Set values in multiple layers.
1769 let mut file = Attrs::new();
1770 file[MESSAGE_TTL_DEFAULT] = 10;
1771 set(Source::File, file);
1772
1773 let mut env = Attrs::new();
1774 env[SPLIT_MAX_BUFFER_SIZE] = 20;
1775 set(Source::Env, env);
1776
1777 let mut runtime = Attrs::new();
1778 runtime[MESSAGE_ACK_EVERY_N_MESSAGES] = 123;
1779 set(Source::Runtime, runtime);
1780
1781 // runtime_attrs() should return only Runtime layer contents.
1782 let rt = runtime_attrs();
1783
1784 // Should have the Runtime key.
1785 assert_eq!(rt[MESSAGE_ACK_EVERY_N_MESSAGES], 123);
1786
1787 // Should NOT have File or Env keys.
1788 assert!(!rt.contains_key(MESSAGE_TTL_DEFAULT));
1789 assert!(!rt.contains_key(SPLIT_MAX_BUFFER_SIZE));
1790 }
1791
1792 #[test]
1793 fn test_override_key_without_env_name_does_not_mirror_to_env() {
1794 let lock = lock();
1795 reset_to_defaults();
1796
1797 // Verify default value.
1798 assert_eq!(get(CONFIG_KEY_NO_ENV), 100);
1799
1800 // Override the key (which has no env_name).
1801 let _guard = lock.override_key(CONFIG_KEY_NO_ENV, 999);
1802
1803 // Should see the override value.
1804 assert_eq!(get(CONFIG_KEY_NO_ENV), 999);
1805
1806 // No env var should have been set (test doesn't crash,
1807 // behavior is clean). This test mainly ensures no panic
1808 // occurs during override/restore.
1809
1810 drop(_guard);
1811
1812 // Should restore to default.
1813 assert_eq!(get(CONFIG_KEY_NO_ENV), 100);
1814 }
1815
1816 #[test]
1817 fn test_multiple_different_keys_overridden_simultaneously() {
1818 let lock = lock();
1819 reset_to_defaults();
1820
1821 // SAFETY: single-threaded test.
1822 unsafe {
1823 std::env::remove_var("HYPERACTOR_CODEC_MAX_FRAME_LENGTH");
1824 std::env::remove_var("HYPERACTOR_MESSAGE_TTL_DEFAULT");
1825 }
1826
1827 // Override multiple different keys at once.
1828 let guard1 = lock.override_key(CODEC_MAX_FRAME_LENGTH, 1111);
1829 let guard2 = lock.override_key(MESSAGE_TTL_DEFAULT, 42);
1830 let guard3 = lock.override_key(CHANNEL_MULTIPART, false);
1831
1832 // All should reflect their override values.
1833 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 1111);
1834 assert_eq!(get(MESSAGE_TTL_DEFAULT), 42);
1835 assert!(!get(CHANNEL_MULTIPART));
1836
1837 // Env vars should be mirrored.
1838 assert_eq!(
1839 std::env::var("HYPERACTOR_CODEC_MAX_FRAME_LENGTH").unwrap(),
1840 "1111"
1841 );
1842 assert_eq!(
1843 std::env::var("HYPERACTOR_MESSAGE_TTL_DEFAULT").unwrap(),
1844 "42"
1845 );
1846
1847 // Drop guards in arbitrary order.
1848 drop(guard2); // Drop MESSAGE_TTL_DEFAULT first
1849
1850 // MESSAGE_TTL_DEFAULT should restore, others should remain.
1851 assert_eq!(get(MESSAGE_TTL_DEFAULT), MESSAGE_TTL_DEFAULT_DEFAULT);
1852 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 1111);
1853 assert!(!get(CHANNEL_MULTIPART));
1854
1855 // Env for MESSAGE_TTL_DEFAULT should be cleared.
1856 assert!(std::env::var("HYPERACTOR_MESSAGE_TTL_DEFAULT").is_err());
1857
1858 drop(guard1);
1859 drop(guard3);
1860
1861 // All should be restored.
1862 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
1863 assert_eq!(get(CHANNEL_MULTIPART), CHANNEL_MULTIPART_DEFAULT);
1864 }
1865
1866 #[test]
1867 fn test_lock_recovers_after_panic() {
1868 let handle = std::thread::spawn(|| {
1869 let _lock = lock();
1870 panic!("intentional panic while holding ConfigLock");
1871 });
1872
1873 let result = handle.join();
1874 assert!(result.is_err(), "thread should have panicked");
1875
1876 let lock = lock();
1877 reset_to_defaults();
1878
1879 let _guard = lock.override_key(CODEC_MAX_FRAME_LENGTH, 9999);
1880 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 9999);
1881 }
1882
1883 // ── config_entries() tests ─────────────────────────────────────────────
1884
1885 #[test]
1886 fn test_config_entries_excludes_non_config_keys() {
1887 let _lock = lock();
1888 reset_to_defaults();
1889
1890 let entries = config_entries();
1891 assert!(
1892 entries
1893 .iter()
1894 .any(|e| e.name.contains("config_key") && !e.name.contains("non_config")),
1895 "CONFIG_KEY should appear in config_entries()"
1896 );
1897 assert!(
1898 !entries.iter().any(|e| e.name.contains("non_config_key")),
1899 "NON_CONFIG_KEY should not appear in config_entries()"
1900 );
1901 }
1902
1903 #[test]
1904 fn test_config_entries_layer_precedence() {
1905 let lock = lock();
1906 reset_to_defaults();
1907
1908 // Defaults-only: source should be Default.
1909 let entries = config_entries();
1910 let codec = entries
1911 .iter()
1912 .find(|e| e.name.contains("codec_max_frame_length"))
1913 .expect("CODEC_MAX_FRAME_LENGTH should appear");
1914 assert_eq!(codec.source, Source::Default);
1915
1916 // Override at TestOverride → source should be TestOverride.
1917 let _guard = lock.override_key(CODEC_MAX_FRAME_LENGTH, 1024);
1918 let entries = config_entries();
1919 let codec = entries
1920 .iter()
1921 .find(|e| e.name.contains("codec_max_frame_length"))
1922 .expect("CODEC_MAX_FRAME_LENGTH should appear");
1923 assert_eq!(codec.source, Source::TestOverride);
1924 assert_eq!(codec.value, "1024");
1925 }
1926
1927 #[test]
1928 fn test_config_entries_changed_from_default_different_value() {
1929 let lock = lock();
1930 reset_to_defaults();
1931
1932 let _guard = lock.override_key(CODEC_MAX_FRAME_LENGTH, 1024);
1933 let entries = config_entries();
1934 let codec = entries
1935 .iter()
1936 .find(|e| e.name.contains("codec_max_frame_length"))
1937 .expect("CODEC_MAX_FRAME_LENGTH should appear");
1938 assert!(
1939 codec.changed_from_default,
1940 "value differs from default → changed_from_default should be true"
1941 );
1942 }
1943
1944 #[test]
1945 fn test_config_entries_changed_from_default_same_value() {
1946 let lock = lock();
1947 reset_to_defaults();
1948
1949 // Override to the same value as the declared default.
1950 let _guard = lock.override_key(CODEC_MAX_FRAME_LENGTH, CODEC_MAX_FRAME_LENGTH_DEFAULT);
1951 let entries = config_entries();
1952 let codec = entries
1953 .iter()
1954 .find(|e| e.name.contains("codec_max_frame_length"))
1955 .expect("CODEC_MAX_FRAME_LENGTH should appear");
1956 assert!(
1957 !codec.changed_from_default,
1958 "value matches default → changed_from_default should be false"
1959 );
1960 }
1961
1962 #[test]
1963 fn test_config_entries_sorted_by_name() {
1964 let _lock = lock();
1965 reset_to_defaults();
1966
1967 let entries = config_entries();
1968 for w in entries.windows(2) {
1969 assert!(
1970 w[0].name <= w[1].name,
1971 "entries not sorted: {:?} > {:?}",
1972 w[0].name,
1973 w[1].name,
1974 );
1975 }
1976 }
1977
1978 #[test]
1979 fn test_config_entries_env_var_extraction() {
1980 let _lock = lock();
1981 reset_to_defaults();
1982
1983 let entries = config_entries();
1984 let codec = entries
1985 .iter()
1986 .find(|e| e.name.contains("codec_max_frame_length"))
1987 .expect("CODEC_MAX_FRAME_LENGTH should appear");
1988 assert_eq!(
1989 codec.env_var.as_deref(),
1990 Some("HYPERACTOR_CODEC_MAX_FRAME_LENGTH")
1991 );
1992
1993 // CONFIG_KEY_NO_ENV has no env_name.
1994 let no_env = entries
1995 .iter()
1996 .find(|e| e.name.contains("config_key_no_env"))
1997 .expect("CONFIG_KEY_NO_ENV should appear");
1998 assert_eq!(no_env.env_var, None);
1999 }
2000}