smart_config/
testing.rs

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