smart_config/
testing.rs

1//! Testing tools for configurations.
2//!
3//! # Overview
4//!
5//! On the most basic level, testing allows to ensure that a configuration can be parsed
6//! from the expected data format. Additionally, tests can help ensure that no breaking changes
7//! are made to the configuration code, and can serve as documentation.
8//!
9//! - [`test()`] function parses a [config source](ConfigSource), such as (parsed) YAML or mocked env vars.
10//! - [`test_complete()`] additionally requires that the source is complete, i.e. covers *all* config params,
11//!   recursively if there are any sub-configs. Naturally, for enum configs, a single variant is required
12//!   (but this variant still must be completely covered).
13//! - [`test_minimal()`], conversely, requires that *only* required config params are specified in the source.
14//!   This allows to check for breaking changes caused by adding non-optional params to a config.
15//! - [`Tester`] enables more control over the test setup, such as [testing](Tester::new()) entire [`ConfigSchema`]
16//!   (as opposed to single configs), [mocking env variables](Tester::set_env()) etc.
17//!
18//! # Examples
19//!
20//! See [`test()`], [`test_complete()`] and [`test_minimal()`] docs for basic examples of usage.
21//!
22//! ## Mocking env variables
23//!
24//! This is mostly useful for testing [fallbacks](crate::fallback) that read from env variables,
25//! although the mocked env vars are respected by [`Environment`](crate::Environment) config sources
26//! as well.
27//!
28//! ```
29//! # use std::path::PathBuf;
30//! use smart_config::{
31//!     fallback, testing::Tester,
32//!     DescribeConfig, DeserializeConfig, Environment,
33//! };
34//!
35//! #[derive(DescribeConfig, DeserializeConfig)]
36//! struct TestConfig {
37//!     port: u16,
38//!     #[config(
39//!         default_t = "/tmp".into(),
40//!         fallback = &fallback::Env("TMPDIR")
41//!     )]
42//!     temp_dir: PathBuf,
43//! }
44//!
45//! let config: TestConfig = Tester::default()
46//!     .set_env("TMPDIR", "/var/app")
47//!     .set_env("APP_PORT", "8080")
48//!     .test_complete(Environment::prefixed("APP_"))?;
49//! assert_eq!(config.port, 8_080);
50//! assert_eq!(config.temp_dir.as_os_str(), "/var/app");
51//! # anyhow::Ok(())
52//! ```
53
54use std::{any, cell::RefCell, collections::HashMap, marker::PhantomData, mem};
55
56use crate::{
57    ConfigRepository, ConfigSource, DeserializeConfig, ParseErrors,
58    de::DeserializerOptions,
59    metadata::{ConfigMetadata, NestedConfigMetadata, ParamMetadata, RustType},
60    schema::ConfigSchema,
61    value::{Pointer, WithOrigin},
62    visit::{ConfigVisitor, VisitConfig},
63};
64
65// We don't actually use `std::env::set_var()` because it is unsafe (and will be marked as such in future Rust editions).
66// On non-Windows OSes, env access is not synchronized across threads.
67thread_local! {
68    pub(crate) static MOCK_ENV_VARS: RefCell<HashMap<String, String>> = RefCell::default();
69}
70
71#[derive(Debug)]
72pub(crate) struct MockEnvGuard {
73    _not_send: PhantomData<*mut ()>,
74}
75
76impl Default for MockEnvGuard {
77    fn default() -> Self {
78        MOCK_ENV_VARS.with_borrow(|vars| {
79            assert!(
80                vars.is_empty(),
81                "Cannot define mock env vars while another `Tester` is active"
82            );
83        });
84
85        Self {
86            _not_send: PhantomData,
87        }
88    }
89}
90
91impl MockEnvGuard {
92    #[allow(clippy::unused_self)] // used for better type safety
93    pub(crate) fn set_env(&self, name: String, value: String) {
94        MOCK_ENV_VARS.with_borrow_mut(|vars| vars.insert(name, value));
95    }
96}
97
98impl Drop for MockEnvGuard {
99    fn drop(&mut self) {
100        MOCK_ENV_VARS.take(); // Remove all mocked env vars
101    }
102}
103
104/// Tests config deserialization from the provided `sample`. Takes into account param aliases,
105/// performs `sample` preprocessing etc.
106///
107/// # Errors
108///
109/// Propagates parsing errors, which allows testing negative cases.
110///
111/// # Examples
112///
113/// ## Basic usage
114///
115/// ```
116/// # use smart_config::{DescribeConfig, DeserializeConfig};
117/// use smart_config::{metadata::SizeUnit, testing, ByteSize};
118///
119/// #[derive(DescribeConfig, DeserializeConfig)]
120/// struct TestConfig {
121///     #[config(default_t = true)]
122///     flag: bool,
123///     #[config(with = SizeUnit::MiB)]
124///     size_mb: ByteSize,
125/// }
126///
127/// let sample = smart_config::config!("size_mb": 2);
128/// let config: TestConfig = testing::test(sample)?;
129/// assert!(config.flag);
130/// assert_eq!(config.size_mb, ByteSize(2 << 20));
131/// # anyhow::Ok(())
132/// ```
133///
134/// ## Testing errors
135///
136/// ```
137/// # use smart_config::{testing, DescribeConfig, DeserializeConfig};
138/// #[derive(Debug, DescribeConfig, DeserializeConfig)]
139/// struct TestConfig {
140///     #[config(default_t = true, alias = "flag")]
141///     boolean: bool,
142/// }
143///
144/// let sample = smart_config::config!("flag": "no");
145/// let err = testing::test::<TestConfig>(sample).unwrap_err();
146/// let err = err.first();
147/// assert_eq!(err.path(), "boolean");
148/// assert!(err
149///     .inner()
150///     .to_string()
151///     .contains("provided string was not `true` or `false`"));
152/// ```
153pub fn test<C: DeserializeConfig>(sample: impl ConfigSource) -> Result<C, ParseErrors> {
154    Tester::default().test(sample)
155}
156
157/// Tests config deserialization ensuring that *all* declared config params are covered.
158///
159/// # Panics
160///
161/// Panics if the `sample` doesn't recursively cover all params in the config. The config message
162/// will contain paths to the missing params.
163///
164/// # Errors
165///
166/// Propagates parsing errors, which allows testing negative cases.
167///
168/// # Examples
169///
170/// ## Basic usage
171///
172/// ```
173/// # use smart_config::{DescribeConfig, DeserializeConfig};
174/// use smart_config::{metadata::SizeUnit, testing, ByteSize};
175///
176/// #[derive(DescribeConfig, DeserializeConfig)]
177/// struct TestConfig {
178///     #[config(default_t = true, alias = "flag")]
179///     boolean: bool,
180///     #[config(with = SizeUnit::MiB)]
181///     size_mb: ByteSize,
182/// }
183///
184/// let sample = smart_config::config!("flag": "false", "size_mb": 64);
185/// let config: TestConfig = testing::test_complete(sample)?;
186/// assert!(!config.boolean);
187/// assert_eq!(config.size_mb, ByteSize(64 << 20));
188/// # anyhow::Ok(())
189/// ```
190///
191/// ## Panics on incomplete sample
192///
193/// ```should_panic
194/// # use smart_config::{DescribeConfig, DeserializeConfig};
195/// # use smart_config::{metadata::SizeUnit, testing, ByteSize};
196/// #[derive(DescribeConfig, DeserializeConfig)]
197/// struct TestConfig {
198///     #[config(default_t = true, alias = "flag")]
199///     boolean: bool,
200///     #[config(with = SizeUnit::MiB)]
201///     size_mb: ByteSize,
202/// }
203///
204/// let incomplete_sample = smart_config::config!("size_mb": 64);
205/// // Will panic with a message detailing missing params (`flag` in this case)
206/// testing::test_complete::<TestConfig>(incomplete_sample)?;
207/// # anyhow::Ok(())
208/// ```
209#[track_caller] // necessary for assertion panics to be located in the test code, rather than in this crate
210pub fn test_complete<C: DeserializeConfig>(sample: impl ConfigSource) -> Result<C, ParseErrors> {
211    Tester::default().test_complete(sample)
212}
213
214/// Tests config deserialization ensuring that *only* required config params are covered.
215/// This is useful to detect new required params (i.e., backward-incompatible changes).
216///
217/// # Panics
218///
219/// Panics if the `sample` contains non-required params in the config. The config message
220/// will contain paths to the redundant params.
221///
222/// # Errors
223///
224/// Propagates parsing errors, which allows testing negative cases.
225///
226/// # Examples
227///
228/// ## Basic usage
229///
230/// ```
231/// # use smart_config::{DescribeConfig, DeserializeConfig};
232/// use smart_config::{metadata::SizeUnit, testing, ByteSize};
233///
234/// #[derive(DescribeConfig, DeserializeConfig)]
235/// struct TestConfig {
236///     #[config(default_t = true, alias = "flag")]
237///     boolean: bool,
238///     #[config(with = SizeUnit::MiB)]
239///     size_mb: ByteSize,
240/// }
241///
242/// let sample = smart_config::config!("size_mb": 64);
243/// let config: TestConfig = testing::test_minimal(sample)?;
244/// assert!(config.boolean);
245/// assert_eq!(config.size_mb, ByteSize(64 << 20));
246/// # anyhow::Ok(())
247/// ```
248///
249/// ## Panics on redundant sample
250///
251/// ```should_panic
252/// # use smart_config::{DescribeConfig, DeserializeConfig};
253/// # use smart_config::{metadata::SizeUnit, testing, ByteSize};
254/// #[derive(DescribeConfig, DeserializeConfig)]
255/// struct TestConfig {
256///     #[config(default_t = true, alias = "flag")]
257///     boolean: bool,
258///     #[config(with = SizeUnit::MiB)]
259///     size_mb: ByteSize,
260/// }
261///
262/// let redundant_sample = smart_config::config!("flag": "false", "size_mb": 64);
263/// // Will panic with a message detailing redundant params (`boolean` in this case)
264/// testing::test_minimal::<TestConfig>(redundant_sample)?;
265/// # anyhow::Ok(())
266/// ```
267#[track_caller]
268pub fn test_minimal<C: DeserializeConfig>(sample: impl ConfigSource) -> Result<C, ParseErrors> {
269    Tester::default().test_minimal(sample)
270}
271
272#[derive(Debug)]
273enum CompletenessCheckerMode {
274    Complete,
275    Minimal,
276}
277
278#[derive(Debug)]
279#[must_use = "must be put back"]
280struct Checkpoint {
281    prev_config: &'static ConfigMetadata,
282    prev_path: Option<String>,
283}
284
285#[derive(Debug)]
286struct CompletenessChecker<'a> {
287    mode: CompletenessCheckerMode,
288    current_path: String,
289    sample: &'a WithOrigin,
290    config: &'static ConfigMetadata,
291    found_params: HashMap<String, RustType>,
292}
293
294impl<'a> CompletenessChecker<'a> {
295    fn new(
296        mode: CompletenessCheckerMode,
297        sample: &'a WithOrigin,
298        config: &'static ConfigMetadata,
299        config_prefix: &str,
300    ) -> Self {
301        Self {
302            mode,
303            current_path: config_prefix.to_owned(),
304            sample,
305            config,
306            found_params: HashMap::new(),
307        }
308    }
309
310    fn check_param(&mut self, param: &ParamMetadata) {
311        let param_path = Pointer(&self.current_path).join(param.name);
312        let should_add_param = match self.mode {
313            CompletenessCheckerMode::Complete => self.sample.get(Pointer(&param_path)).is_none(),
314            CompletenessCheckerMode::Minimal => {
315                param.default_value.is_some() && self.sample.get(Pointer(&param_path)).is_some()
316            }
317        };
318
319        if should_add_param {
320            self.found_params.insert(param_path, param.rust_type);
321        }
322    }
323
324    fn insert_param(&mut self, param: &ParamMetadata) {
325        let param_path = Pointer(&self.current_path).join(param.name);
326        self.found_params.insert(param_path, param.rust_type);
327    }
328
329    fn push_config(&mut self, config_meta: &NestedConfigMetadata) -> Checkpoint {
330        let prev_config = mem::replace(&mut self.config, config_meta.meta);
331        let prev_path = if config_meta.name.is_empty() {
332            None
333        } else {
334            let nested_path = Pointer(&self.current_path).join(config_meta.name);
335            Some(mem::replace(&mut self.current_path, nested_path))
336        };
337        Checkpoint {
338            prev_config,
339            prev_path,
340        }
341    }
342
343    fn pop_config(&mut self, checkpoint: Checkpoint) {
344        self.config = checkpoint.prev_config;
345        if let Some(path) = checkpoint.prev_path {
346            self.current_path = path;
347        }
348    }
349
350    fn collect_all_params(&mut self) {
351        if let Some(tag) = &self.config.tag {
352            // Only report the tag param as missing, since all other params can be legitimately absent depending on its value.
353            self.insert_param(tag.param);
354            return;
355        }
356
357        for param in self.config.params {
358            self.insert_param(param);
359        }
360        for config_meta in self.config.nested_configs {
361            let checkpoint = self.push_config(config_meta);
362            self.collect_all_params();
363            self.pop_config(checkpoint);
364        }
365    }
366}
367
368impl ConfigVisitor for CompletenessChecker<'_> {
369    fn visit_tag(&mut self, _variant_index: usize) {
370        let param = self.config.tag.unwrap().param;
371        self.check_param(param);
372    }
373
374    fn visit_param(&mut self, param_index: usize, _value: &dyn any::Any) {
375        let param = &self.config.params[param_index];
376        self.check_param(param);
377    }
378
379    fn visit_nested_config(&mut self, config_index: usize, config: &dyn VisitConfig) {
380        let config_meta = &self.config.nested_configs[config_index];
381        let checkpoint = self.push_config(config_meta);
382        config.visit_config(self);
383        self.pop_config(checkpoint);
384    }
385
386    fn visit_nested_opt_config(&mut self, config_index: usize, config: Option<&dyn VisitConfig>) {
387        if let Some(config) = config {
388            self.visit_nested_config(config_index, config);
389        } else if matches!(self.mode, CompletenessCheckerMode::Complete) {
390            let config_meta = &self.config.nested_configs[config_index];
391            let checkpoint = self.push_config(config_meta);
392            self.collect_all_params();
393            self.pop_config(checkpoint);
394        }
395    }
396}
397
398#[derive(Debug)]
399struct TesterData {
400    de_options: DeserializerOptions,
401    schema: ConfigSchema,
402    env_guard: MockEnvGuard,
403}
404
405#[derive(Debug)]
406enum TesterDataGoat<'a> {
407    Owned(TesterData),
408    Borrowed(&'a mut TesterData),
409}
410
411impl AsRef<TesterData> for TesterDataGoat<'_> {
412    fn as_ref(&self) -> &TesterData {
413        match self {
414            Self::Owned(data) => data,
415            Self::Borrowed(data) => data,
416        }
417    }
418}
419
420impl AsMut<TesterData> for TesterDataGoat<'_> {
421    fn as_mut(&mut self) -> &mut TesterData {
422        match self {
423            Self::Owned(data) => data,
424            Self::Borrowed(data) => data,
425        }
426    }
427}
428
429/// Test case builder that allows configuring deserialization options etc.
430///
431/// Compared to [`test()`] / [`test_complete()`] methods, `Tester` has more control over deserialization options.
432/// It also allows to test a [`ConfigSchema`] with multiple configs.
433///
434/// # Examples
435///
436/// ```
437/// use smart_config::{testing::Tester, ConfigSchema};
438/// # use smart_config::{DescribeConfig, DeserializeConfig};
439///
440/// // Assume the following configs and schema are defined.
441/// #[derive(DescribeConfig, DeserializeConfig)]
442/// struct TestConfig {
443///     #[config(default, alias = "flag")]
444///     boolean: bool,
445/// }
446///
447/// #[derive(DescribeConfig, DeserializeConfig)]
448/// struct OtherConfig {
449///     str: Option<String>,
450/// }
451///
452/// fn config_schema() -> ConfigSchema {
453///     let mut schema = ConfigSchema::new(&TestConfig::DESCRIPTION, "test");
454///     schema
455///         .insert(&OtherConfig::DESCRIPTION, "other")
456///         .unwrap();
457///     schema
458/// }
459///
460/// // Set the tester (can be shared across tests).
461/// let schema: ConfigSchema = config_schema();
462/// let mut tester = Tester::new(schema);
463/// // Set shared deserialization options...
464/// tester.coerce_serde_enums().coerce_variant_names();
465///
466/// let sample = smart_config::config!("test.flag": true, "other.str": "?");
467/// let config: TestConfig = tester.for_config().test_complete(sample.clone())?;
468/// assert!(config.boolean);
469/// let config: OtherConfig = tester.for_config().test_complete(sample)?;
470/// assert_eq!(config.str.unwrap(), "?");
471/// # anyhow::Ok(())
472/// ```
473#[derive(Debug)]
474pub struct Tester<'a, C> {
475    data: TesterDataGoat<'a>,
476    _config: PhantomData<C>,
477}
478
479impl<C: DeserializeConfig + VisitConfig> Default for Tester<'static, C> {
480    fn default() -> Self {
481        Self {
482            data: TesterDataGoat::Owned(TesterData {
483                de_options: DeserializerOptions::default(),
484                schema: ConfigSchema::new(&C::DESCRIPTION, ""),
485                env_guard: MockEnvGuard::default(),
486            }),
487            _config: PhantomData,
488        }
489    }
490}
491
492impl Default for Tester<'static, ()> {
493    fn default() -> Self {
494        Self {
495            data: TesterDataGoat::Owned(TesterData {
496                de_options: DeserializerOptions::default(),
497                schema: ConfigSchema::default(),
498                env_guard: MockEnvGuard::default(),
499            }),
500            _config: PhantomData,
501        }
502    }
503}
504
505impl Tester<'_, ()> {
506    /// Creates a tester with the specified schema.
507    pub fn new(schema: ConfigSchema) -> Self {
508        Self {
509            data: TesterDataGoat::Owned(TesterData {
510                de_options: DeserializerOptions::default(),
511                schema,
512                env_guard: MockEnvGuard::default(),
513            }),
514            _config: PhantomData,
515        }
516    }
517
518    /// Specializes this tester for a config.
519    ///
520    /// # Panics
521    ///
522    /// Panics if the config is not contained in the schema, or is contained at multiple locations.
523    pub fn for_config<C: DeserializeConfig + VisitConfig>(&mut self) -> Tester<'_, C> {
524        // Check that there's a single config of the specified type
525        self.data.as_ref().schema.single(&C::DESCRIPTION).unwrap();
526        Tester {
527            data: TesterDataGoat::Borrowed(self.data.as_mut()),
528            _config: PhantomData,
529        }
530    }
531
532    /// Similar to [`Self::for_config()`], but inserts the specified config into the schema rather
533    /// than expecting it to be present.
534    ///
535    /// # Panics
536    ///
537    /// Panics if the config cannot be inserted into the schema.
538    pub fn insert<C: DeserializeConfig + VisitConfig>(
539        &mut self,
540        prefix: &'static str,
541    ) -> Tester<'_, C> {
542        self.data
543            .as_mut()
544            .schema
545            .insert(&C::DESCRIPTION, prefix)
546            .expect("failed inserting config into schema");
547        Tester {
548            data: TesterDataGoat::Borrowed(self.data.as_mut()),
549            _config: PhantomData,
550        }
551    }
552}
553
554impl<C> Tester<'_, C> {
555    /// Enables coercion of enum variant names.
556    pub fn coerce_variant_names(&mut self) -> &mut Self {
557        self.data.as_mut().de_options.coerce_variant_names = true;
558        self
559    }
560
561    /// Enables coercion of serde-style enums.
562    pub fn coerce_serde_enums(&mut self) -> &mut Self {
563        self.data.as_mut().schema.coerce_serde_enums(true);
564        self
565    }
566
567    /// Sets mock environment variables that will be recognized by [`Environment`](crate::Environment)
568    /// and [`Env`](crate::fallback::Env) fallbacks.
569    ///
570    /// Beware that env variable overrides are thread-local; for this reason, `Tester` is not `Send` (cannot be sent to another thread).
571    pub fn set_env(&mut self, var_name: impl Into<String>, value: impl Into<String>) -> &mut Self {
572        self.data
573            .as_mut()
574            .env_guard
575            .set_env(var_name.into(), value.into());
576        self
577    }
578
579    /// Creates an empty repository based on the tester schema and the deserialization options.
580    pub fn new_repository(&self) -> ConfigRepository<'_> {
581        let data = self.data.as_ref();
582        let mut repo = ConfigRepository::new(&data.schema);
583        *repo.deserializer_options() = data.de_options.clone();
584        repo
585    }
586}
587
588impl<C: DeserializeConfig + VisitConfig> Tester<'_, C> {
589    /// Tests config deserialization from the provided `sample`. Takes into account param aliases,
590    /// performs `sample` preprocessing etc.
591    ///
592    /// # Errors
593    ///
594    /// Propagates parsing errors, which allows testing negative cases.
595    ///
596    /// # Examples
597    ///
598    /// See [`test()`] for the examples of usage.
599    #[allow(clippy::missing_panics_doc)] // can only panic if the config is recursively defined, which is impossible
600    pub fn test(&self, sample: impl ConfigSource) -> Result<C, ParseErrors> {
601        let repo = self.new_repository();
602        repo.with(sample).single::<C>().unwrap().parse()
603    }
604
605    /// Tests config deserialization ensuring that *all* declared config params are covered.
606    ///
607    /// # Panics
608    ///
609    /// Panics if the `sample` doesn't recursively cover all params in the config. The config message
610    /// will contain paths to the missing params.
611    ///
612    /// # Errors
613    ///
614    /// Propagates parsing errors, which allows testing negative cases.
615    ///
616    /// # Examples
617    ///
618    /// See [`test_complete()`] for the examples of usage.
619    #[track_caller]
620    pub fn test_complete(&self, sample: impl ConfigSource) -> Result<C, ParseErrors> {
621        let repo = self.new_repository().with(sample);
622        let (missing_params, config) =
623            Self::test_with_checker(&repo, CompletenessCheckerMode::Complete)?;
624        assert!(
625            missing_params.is_empty(),
626            "The provided sample is incomplete; missing params: {missing_params:?}"
627        );
628        Ok(config)
629    }
630
631    fn test_with_checker(
632        repo: &ConfigRepository<'_>,
633        mode: CompletenessCheckerMode,
634    ) -> Result<(HashMap<String, RustType>, C), ParseErrors> {
635        let config_ref = repo.single::<C>().unwrap();
636        let config_prefix = config_ref.config().prefix();
637        let config = config_ref.parse()?;
638        let mut visitor =
639            CompletenessChecker::new(mode, repo.merged(), &C::DESCRIPTION, config_prefix);
640        config.visit_config(&mut visitor);
641        let CompletenessChecker { found_params, .. } = visitor;
642        Ok((found_params, config))
643    }
644
645    /// Tests config deserialization ensuring that only the required config params are present in `sample`.
646    ///
647    /// # Panics
648    ///
649    /// Panics if the `sample` contains params with default values. The config message
650    /// will contain paths to the redundant params.
651    ///
652    /// # Errors
653    ///
654    /// Propagates parsing errors, which allows testing negative cases.
655    ///
656    /// # Examples
657    ///
658    /// See [`test_minimal()`] for the examples of usage.
659    #[track_caller]
660    pub fn test_minimal(&self, sample: impl ConfigSource) -> Result<C, ParseErrors> {
661        let repo = self.new_repository().with(sample);
662        let (redundant_params, config) =
663            Self::test_with_checker(&repo, CompletenessCheckerMode::Minimal)?;
664        assert!(
665            redundant_params.is_empty(),
666            "The provided sample is not minimal; params with default values: {redundant_params:?}"
667        );
668        Ok(config)
669    }
670}
671
672#[cfg(test)]
673mod tests {
674    use std::collections::HashSet;
675
676    use smart_config_derive::DescribeConfig;
677
678    use super::*;
679    use crate::{
680        Environment, Json, config,
681        testonly::{CompoundConfig, DefaultingConfig, EnumConfig, NestedConfig, SimpleEnum},
682    };
683
684    #[test]
685    fn testing_config() {
686        let config = test::<DefaultingConfig>(Json::empty("test.json")).unwrap();
687        assert_eq!(config, DefaultingConfig::default());
688
689        let config = test_minimal::<DefaultingConfig>(Json::empty("test.json")).unwrap();
690        assert_eq!(config, DefaultingConfig::default());
691
692        let json = config!("float": 4.2, "url": ());
693        let config = test::<DefaultingConfig>(json).unwrap();
694        assert_eq!(
695            config,
696            DefaultingConfig {
697                float: Some(4.2),
698                url: None,
699                ..DefaultingConfig::default()
700            }
701        );
702    }
703
704    #[should_panic(expected = "missing params")]
705    #[test]
706    fn panicking_on_incomplete_sample() {
707        let json = config!("renamed": "first", "nested.renamed": "second");
708        test_complete::<CompoundConfig>(json).unwrap();
709    }
710
711    #[should_panic(expected = "sample is not minimal")]
712    #[test]
713    fn panicking_on_redundant_sample() {
714        let json = config!("renamed": "first", "other_int": 23);
715        test_minimal::<NestedConfig>(json).unwrap();
716    }
717
718    #[test]
719    fn minimal_testing() {
720        let json = config!("renamed": "first", "nested.renamed": "second");
721        let config: CompoundConfig = test_minimal(json).unwrap();
722        assert_eq!(config.flat.simple_enum, SimpleEnum::First);
723        assert_eq!(config.nested.simple_enum, SimpleEnum::Second);
724        assert!(config.nested_opt.is_none());
725        assert_eq!(config.nested_default, NestedConfig::default_nested());
726    }
727
728    #[test]
729    fn complete_testing() {
730        let json = config!(
731            "other_int": 123,
732            "renamed": "first",
733            "map": HashMap::from([("test", 3)]),
734            "nested.other_int": 42,
735            "nested.renamed": "second",
736            "nested.map": HashMap::from([("test", 2)]),
737            "nested_opt.other_int": 777,
738            "nested_opt.renamed": "first",
739            "nested_opt.map": HashMap::<&str, u32>::new(),
740            "default.other_int": 11,
741            "default.renamed": "second",
742            "default.map": HashMap::from([("test", 1)]),
743        );
744        let config = test_complete::<CompoundConfig>(json).unwrap();
745        assert_eq!(config.flat.other_int, 123);
746        assert_eq!(config.nested.other_int, 42);
747        assert_eq!(config.nested_default.other_int, 11);
748        let opt = config.nested_opt.unwrap();
749        assert_eq!(opt.other_int, 777);
750        assert_eq!(opt.simple_enum, SimpleEnum::First);
751        assert_eq!(opt.map, HashMap::new());
752    }
753
754    #[test]
755    fn complete_testing_for_env_vars() {
756        let env = Environment::from_dotenv(
757            "test.env",
758            r#"
759            APP_INT=123
760            APP_FLOAT=8.4
761            APP_URL="https://example.com/"
762            APP_SET="first,second"
763            "#,
764        )
765        .unwrap()
766        .strip_prefix("APP_");
767        let config = test_complete::<DefaultingConfig>(env).unwrap();
768        assert_eq!(config.int, 123);
769        assert_eq!(config.float, Some(8.4));
770        assert_eq!(config.url.unwrap(), "https://example.com/");
771        assert_eq!(
772            config.set,
773            HashSet::from([SimpleEnum::First, SimpleEnum::Second])
774        );
775    }
776
777    #[test]
778    fn complete_testing_for_enum_configs() {
779        let json = config!("type": "first");
780        let config = test_complete::<EnumConfig>(json).unwrap();
781        assert_eq!(config, EnumConfig::First);
782
783        let json = config!("type": "Fields", "string": "!", "flag": false, "set": [1, 2]);
784        let config = test_complete::<EnumConfig>(json).unwrap();
785        assert_eq!(
786            config,
787            EnumConfig::WithFields {
788                string: Some("!".to_owned()),
789                flag: false,
790                set: HashSet::from([1, 2]),
791            }
792        );
793    }
794
795    #[should_panic(expected = "missing params")]
796    #[test]
797    fn incomplete_enum_config() {
798        let json = config!("type": "Fields");
799        test_complete::<EnumConfig>(json).unwrap();
800    }
801
802    #[should_panic(expected = "opt.nested_opt.other_int")]
803    #[test]
804    fn panicking_on_incomplete_sample_with_optional_nested_config() {
805        #[derive(Debug, DescribeConfig, DeserializeConfig)]
806        #[config(crate = crate)]
807        struct TestConfig {
808            required: u32,
809            #[config(nest)]
810            opt: Option<CompoundConfig>,
811        }
812
813        let json = config!("required": 42);
814        test_complete::<TestConfig>(json).ok();
815    }
816}