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(¶m_path)).is_none(),
314 CompletenessCheckerMode::Minimal => {
315 param.default_value.is_some() && self.sample.get(Pointer(¶m_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}