1use super::{NextestConfig, ToolConfigFile, ToolName};
7use crate::errors::{ConfigParseError, ConfigParseErrorKind};
8use camino::{Utf8Path, Utf8PathBuf};
9use semver::Version;
10use serde::{
11 Deserialize, Deserializer,
12 de::{MapAccess, SeqAccess, Visitor},
13};
14use std::{borrow::Cow, collections::BTreeSet, fmt, str::FromStr};
15
16#[derive(Debug, Default, Clone, PartialEq, Eq)]
21pub struct VersionOnlyConfig {
22 nextest_version: NextestVersionConfig,
24
25 experimental: ExperimentalConfig,
27}
28
29impl VersionOnlyConfig {
30 pub fn from_sources<'a, I>(
34 workspace_root: &Utf8Path,
35 config_file: Option<&Utf8Path>,
36 tool_config_files: impl IntoIterator<IntoIter = I>,
37 ) -> Result<Self, ConfigParseError>
38 where
39 I: Iterator<Item = &'a ToolConfigFile> + DoubleEndedIterator,
40 {
41 let tool_config_files_rev = tool_config_files.into_iter().rev();
42
43 Self::read_from_sources(workspace_root, config_file, tool_config_files_rev)
44 }
45
46 pub fn nextest_version(&self) -> &NextestVersionConfig {
48 &self.nextest_version
49 }
50
51 pub fn experimental(&self) -> &ExperimentalConfig {
53 &self.experimental
54 }
55
56 fn read_from_sources<'a>(
57 workspace_root: &Utf8Path,
58 config_file: Option<&Utf8Path>,
59 tool_config_files_rev: impl Iterator<Item = &'a ToolConfigFile>,
60 ) -> Result<Self, ConfigParseError> {
61 let mut nextest_version = NextestVersionConfig::default();
62 let mut known = BTreeSet::new();
63 let mut unknown = BTreeSet::new();
64
65 for ToolConfigFile { config_file, tool } in tool_config_files_rev {
67 if let Some(v) = Self::read_and_deserialize(config_file, Some(tool))?.nextest_version {
68 nextest_version.accumulate(v, Some(tool.clone()));
69 }
70 }
71
72 let config_file = match config_file {
74 Some(file) => Some(Cow::Borrowed(file)),
75 None => {
76 let config_file = workspace_root.join(NextestConfig::CONFIG_PATH);
77 config_file.exists().then_some(Cow::Owned(config_file))
78 }
79 };
80 if let Some(config_file) = config_file {
81 let d = Self::read_and_deserialize(&config_file, None)?;
82 if let Some(v) = d.nextest_version {
83 nextest_version.accumulate(v, None);
84 }
85
86 known.extend(d.experimental.known);
90 unknown.extend(d.experimental.unknown);
91 }
92
93 Ok(Self {
94 nextest_version,
95 experimental: ExperimentalConfig { known, unknown },
96 })
97 }
98
99 fn read_and_deserialize(
100 config_file: &Utf8Path,
101 tool: Option<&ToolName>,
102 ) -> Result<VersionOnlyDeserialize, ConfigParseError> {
103 let toml_str = std::fs::read_to_string(config_file.as_str()).map_err(|error| {
104 ConfigParseError::new(
105 config_file,
106 tool,
107 ConfigParseErrorKind::VersionOnlyReadError(error),
108 )
109 })?;
110 let toml_de = toml::de::Deserializer::parse(&toml_str).map_err(|error| {
111 ConfigParseError::new(
112 config_file,
113 tool,
114 ConfigParseErrorKind::TomlParseError(Box::new(error)),
115 )
116 })?;
117 let v: VersionOnlyDeserialize =
118 serde_path_to_error::deserialize(toml_de).map_err(|error| {
119 ConfigParseError::new(
120 config_file,
121 tool,
122 ConfigParseErrorKind::VersionOnlyDeserializeError(Box::new(error)),
123 )
124 })?;
125 if tool.is_some() && !v.experimental.is_empty() {
126 return Err(ConfigParseError::new(
127 config_file,
128 tool,
129 ConfigParseErrorKind::ExperimentalFeaturesInToolConfig {
130 features: v.experimental.feature_names(),
131 },
132 ));
133 }
134
135 Ok(v)
136 }
137}
138
139#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
141#[serde(rename_all = "kebab-case")]
142struct VersionOnlyDeserialize {
143 #[serde(default)]
144 nextest_version: Option<NextestVersionDeserialize>,
145 #[serde(default)]
146 experimental: ExperimentalDeserialize,
147}
148
149#[derive(Debug, Default, Clone, PartialEq, Eq)]
155pub(crate) struct ExperimentalDeserialize {
156 known: BTreeSet<ConfigExperimental>,
158 unknown: BTreeSet<String>,
160}
161
162impl ExperimentalDeserialize {
163 fn is_empty(&self) -> bool {
165 self.known.is_empty() && self.unknown.is_empty()
166 }
167
168 fn feature_names(&self) -> BTreeSet<String> {
171 let mut names = self.unknown.clone();
172 for feature in &self.known {
173 names.insert(feature.to_string());
174 }
175 names
176 }
177}
178
179impl<'de> Deserialize<'de> for ExperimentalDeserialize {
180 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
181 where
182 D: Deserializer<'de>,
183 {
184 struct ExperimentalVisitor;
185
186 impl<'de> Visitor<'de> for ExperimentalVisitor {
187 type Value = ExperimentalDeserialize;
188
189 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
190 formatter.write_str(
191 "a table ({ setup-scripts = true, benchmarks = true }) \
192 or an array ([\"setup-scripts\", \"benchmarks\"])",
193 )
194 }
195
196 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
197 where
198 A: SeqAccess<'de>,
199 {
200 let mut known = BTreeSet::new();
202 let mut unknown = BTreeSet::new();
203 while let Some(feature_str) = seq.next_element::<String>()? {
204 if let Ok(feature) = feature_str.parse::<ConfigExperimental>() {
205 known.insert(feature);
206 } else {
207 unknown.insert(feature_str);
208 }
209 }
210 Ok(ExperimentalDeserialize { known, unknown })
211 }
212
213 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
214 where
215 A: MapAccess<'de>,
216 {
217 #[derive(Deserialize)]
220 #[serde(rename_all = "kebab-case")]
221 struct TableConfig {
222 #[serde(default)]
223 setup_scripts: bool,
224 #[serde(default)]
225 wrapper_scripts: bool,
226 #[serde(default)]
227 benchmarks: bool,
228 }
229
230 let mut unknown = BTreeSet::new();
231 let de = serde::de::value::MapAccessDeserializer::new(map);
232 let mut cb = |path: serde_ignored::Path| {
233 unknown.insert(path.to_string());
234 };
235 let ignored_de = serde_ignored::Deserializer::new(de, &mut cb);
236 let TableConfig {
237 setup_scripts,
238 wrapper_scripts,
239 benchmarks,
240 } = Deserialize::deserialize(ignored_de).map_err(serde::de::Error::custom)?;
241
242 let mut known = BTreeSet::new();
243 if setup_scripts {
244 known.insert(ConfigExperimental::SetupScripts);
245 }
246 if wrapper_scripts {
247 known.insert(ConfigExperimental::WrapperScripts);
248 }
249 if benchmarks {
250 known.insert(ConfigExperimental::Benchmarks);
251 }
252
253 Ok(ExperimentalDeserialize { known, unknown })
254 }
255 }
256
257 deserializer.deserialize_any(ExperimentalVisitor)
258 }
259}
260
261#[derive(Debug, Default, Clone, PartialEq, Eq)]
267pub struct NextestVersionConfig {
268 pub required: NextestVersionReq,
270
271 pub recommended: NextestVersionReq,
276}
277
278impl NextestVersionConfig {
279 pub(crate) fn accumulate(&mut self, v: NextestVersionDeserialize, v_tool: Option<ToolName>) {
281 if let Some(version) = v.required {
282 self.required.accumulate(version, v_tool.clone());
283 }
284 if let Some(version) = v.recommended {
285 self.recommended.accumulate(version, v_tool);
286 }
287 }
288
289 pub fn eval(
291 &self,
292 current_version: &Version,
293 override_version_check: bool,
294 ) -> NextestVersionEval {
295 match self.required.satisfies(current_version) {
296 Ok(()) => {}
297 Err((required, tool)) => {
298 if override_version_check {
299 return NextestVersionEval::ErrorOverride {
300 required: required.clone(),
301 current: current_version.clone(),
302 tool: tool.cloned(),
303 };
304 } else {
305 return NextestVersionEval::Error {
306 required: required.clone(),
307 current: current_version.clone(),
308 tool: tool.cloned(),
309 };
310 }
311 }
312 }
313
314 match self.recommended.satisfies(current_version) {
315 Ok(()) => NextestVersionEval::Satisfied,
316 Err((recommended, tool)) => {
317 if override_version_check {
318 NextestVersionEval::WarnOverride {
319 recommended: recommended.clone(),
320 current: current_version.clone(),
321 tool: tool.cloned(),
322 }
323 } else {
324 NextestVersionEval::Warn {
325 recommended: recommended.clone(),
326 current: current_version.clone(),
327 tool: tool.cloned(),
328 }
329 }
330 }
331 }
332 }
333}
334
335#[derive(Debug, Default, Clone, PartialEq, Eq)]
340pub struct ExperimentalConfig {
341 known: BTreeSet<ConfigExperimental>,
343
344 unknown: BTreeSet<String>,
346}
347
348impl ExperimentalConfig {
349 pub fn known(&self) -> &BTreeSet<ConfigExperimental> {
351 &self.known
352 }
353
354 pub fn eval(&self) -> ExperimentalConfigEval {
359 if self.unknown.is_empty() {
360 ExperimentalConfigEval::Satisfied
361 } else {
362 ExperimentalConfigEval::UnknownFeatures {
363 unknown: self.unknown.clone(),
364 known: ConfigExperimental::known_features().collect(),
365 }
366 }
367 }
368}
369
370#[derive(Debug, Clone, PartialEq, Eq)]
374pub enum ExperimentalConfigEval {
375 Satisfied,
377
378 UnknownFeatures {
380 unknown: BTreeSet<String>,
382
383 known: BTreeSet<ConfigExperimental>,
385 },
386}
387
388impl ExperimentalConfigEval {
389 pub fn into_error(self, config_file: impl Into<Utf8PathBuf>) -> Option<ConfigParseError> {
393 match self {
394 ExperimentalConfigEval::Satisfied => None,
395 ExperimentalConfigEval::UnknownFeatures { unknown, known } => {
396 Some(ConfigParseError::new(
397 config_file,
398 None,
399 ConfigParseErrorKind::UnknownExperimentalFeatures { unknown, known },
400 ))
401 }
402 }
403 }
404}
405
406#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
408#[non_exhaustive]
409pub enum ConfigExperimental {
410 SetupScripts,
412 WrapperScripts,
414 Benchmarks,
416}
417
418impl ConfigExperimental {
419 pub fn known_features() -> impl Iterator<Item = Self> {
421 vec![Self::SetupScripts, Self::WrapperScripts, Self::Benchmarks].into_iter()
422 }
423
424 pub fn env_var(self) -> Option<&'static str> {
426 match self {
427 Self::SetupScripts => None,
428 Self::WrapperScripts => None,
429 Self::Benchmarks => Some("NEXTEST_EXPERIMENTAL_BENCHMARKS"),
430 }
431 }
432
433 pub fn from_env() -> std::collections::BTreeSet<Self> {
435 let mut set = std::collections::BTreeSet::new();
436 for feature in Self::known_features() {
437 if let Some(env_var) = feature.env_var()
438 && std::env::var(env_var).as_deref() == Ok("1")
439 {
440 set.insert(feature);
441 }
442 }
443 set
444 }
445}
446
447impl FromStr for ConfigExperimental {
448 type Err = ();
449
450 fn from_str(s: &str) -> Result<Self, Self::Err> {
451 match s {
452 "setup-scripts" => Ok(Self::SetupScripts),
453 "wrapper-scripts" => Ok(Self::WrapperScripts),
454 "benchmarks" => Ok(Self::Benchmarks),
455 _ => Err(()),
456 }
457 }
458}
459
460impl fmt::Display for ConfigExperimental {
461 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
462 match self {
463 Self::SetupScripts => write!(f, "setup-scripts"),
464 Self::WrapperScripts => write!(f, "wrapper-scripts"),
465 Self::Benchmarks => write!(f, "benchmarks"),
466 }
467 }
468}
469
470#[derive(Debug, Default, Clone, PartialEq, Eq)]
472pub enum NextestVersionReq {
473 Version {
475 version: Version,
477
478 tool: Option<ToolName>,
480 },
481
482 #[default]
484 None,
485}
486
487impl NextestVersionReq {
488 pub fn version(&self) -> Option<&Version> {
490 match self {
491 NextestVersionReq::Version { version, .. } => Some(version),
492 NextestVersionReq::None => None,
493 }
494 }
495
496 fn accumulate(&mut self, v: Version, v_tool: Option<ToolName>) {
497 match self {
498 NextestVersionReq::Version { version, tool } => {
499 if &v >= version {
502 *version = v;
503 *tool = v_tool;
504 }
505 }
506 NextestVersionReq::None => {
507 *self = NextestVersionReq::Version {
508 version: v,
509 tool: v_tool,
510 };
511 }
512 }
513 }
514
515 fn satisfies(&self, version: &Version) -> Result<(), (&Version, Option<&ToolName>)> {
516 match self {
517 NextestVersionReq::Version {
518 version: required,
519 tool,
520 } => {
521 if version >= required {
522 Ok(())
523 } else {
524 Err((required, tool.as_ref()))
525 }
526 }
527 NextestVersionReq::None => Ok(()),
528 }
529 }
530}
531
532#[derive(Debug, Clone, PartialEq, Eq)]
536pub enum NextestVersionEval {
537 Satisfied,
539
540 Error {
542 required: Version,
544 current: Version,
546 tool: Option<ToolName>,
548 },
549
550 Warn {
552 recommended: Version,
554 current: Version,
556 tool: Option<ToolName>,
558 },
559
560 ErrorOverride {
562 required: Version,
564 current: Version,
566 tool: Option<ToolName>,
568 },
569
570 WarnOverride {
572 recommended: Version,
574 current: Version,
576 tool: Option<ToolName>,
578 },
579}
580
581#[derive(Debug, Clone, PartialEq, Eq)]
587pub(crate) struct NextestVersionDeserialize {
588 required: Option<Version>,
590
591 recommended: Option<Version>,
593}
594
595impl<'de> Deserialize<'de> for NextestVersionDeserialize {
596 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
597 where
598 D: Deserializer<'de>,
599 {
600 struct V;
601
602 impl<'de2> serde::de::Visitor<'de2> for V {
603 type Value = NextestVersionDeserialize;
604
605 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
606 formatter.write_str(
607 "a table ({{ required = \"0.9.20\", recommended = \"0.9.30\" }}) or a string (\"0.9.50\")",
608 )
609 }
610
611 fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
612 where
613 E: serde::de::Error,
614 {
615 let required = parse_version::<E>(s.to_owned())?;
616 Ok(NextestVersionDeserialize {
617 required: Some(required),
618 recommended: None,
619 })
620 }
621
622 fn visit_map<A>(self, map: A) -> std::result::Result<Self::Value, A::Error>
623 where
624 A: serde::de::MapAccess<'de2>,
625 {
626 #[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
627 struct NextestVersionMap {
628 #[serde(default, deserialize_with = "deserialize_version_opt")]
629 required: Option<Version>,
630 #[serde(default, deserialize_with = "deserialize_version_opt")]
631 recommended: Option<Version>,
632 }
633
634 let NextestVersionMap {
635 required,
636 recommended,
637 } = NextestVersionMap::deserialize(serde::de::value::MapAccessDeserializer::new(
638 map,
639 ))?;
640
641 if let (Some(required), Some(recommended)) = (&required, &recommended)
642 && required > recommended
643 {
644 return Err(serde::de::Error::custom(format!(
645 "required version ({required}) must not be greater than recommended version ({recommended})"
646 )));
647 }
648
649 Ok(NextestVersionDeserialize {
650 required,
651 recommended,
652 })
653 }
654 }
655
656 deserializer.deserialize_any(V)
657 }
658}
659
660fn deserialize_version_opt<'de, D>(
665 deserializer: D,
666) -> std::result::Result<Option<Version>, D::Error>
667where
668 D: Deserializer<'de>,
669{
670 let s = Option::<String>::deserialize(deserializer)?;
671 s.map(parse_version::<D::Error>).transpose()
672}
673
674fn parse_version<E>(mut s: String) -> std::result::Result<Version, E>
675where
676 E: serde::de::Error,
677{
678 for ch in s.chars() {
679 if ch == '-' {
680 return Err(E::custom(
681 "pre-release identifiers are not supported in nextest-version",
682 ));
683 } else if ch == '+' {
684 return Err(E::custom(
685 "build metadata is not supported in nextest-version",
686 ));
687 }
688 }
689
690 if s.matches('.').count() == 1 {
693 s.push_str(".0");
695 }
696
697 Version::parse(&s).map_err(E::custom)
698}
699
700#[cfg(test)]
701mod tests {
702 use super::*;
703 use test_case::test_case;
704
705 #[test_case(
706 r#"
707 nextest-version = "0.9"
708 "#,
709 NextestVersionDeserialize { required: Some("0.9.0".parse().unwrap()), recommended: None } ; "basic"
710 )]
711 #[test_case(
712 r#"
713 nextest-version = "0.9.30"
714 "#,
715 NextestVersionDeserialize { required: Some("0.9.30".parse().unwrap()), recommended: None } ; "basic with patch"
716 )]
717 #[test_case(
718 r#"
719 nextest-version = { recommended = "0.9.20" }
720 "#,
721 NextestVersionDeserialize { required: None, recommended: Some("0.9.20".parse().unwrap()) } ; "with warning"
722 )]
723 #[test_case(
724 r#"
725 nextest-version = { required = "0.9.20", recommended = "0.9.25" }
726 "#,
727 NextestVersionDeserialize {
728 required: Some("0.9.20".parse().unwrap()),
729 recommended: Some("0.9.25".parse().unwrap()),
730 } ; "with error and warning"
731 )]
732 fn test_valid_nextest_version(input: &str, expected: NextestVersionDeserialize) {
733 let actual: VersionOnlyDeserialize = toml::from_str(input).unwrap();
734 assert_eq!(actual.nextest_version.unwrap(), expected);
735 }
736
737 #[test_case(
738 r#"
739 nextest-version = 42
740 "#,
741 "a table ({{ required = \"0.9.20\", recommended = \"0.9.30\" }}) or a string (\"0.9.50\")" ; "empty"
742 )]
743 #[test_case(
744 r#"
745 nextest-version = "0.9.30-rc.1"
746 "#,
747 "pre-release identifiers are not supported in nextest-version" ; "pre-release"
748 )]
749 #[test_case(
750 r#"
751 nextest-version = "0.9.40+mybuild"
752 "#,
753 "build metadata is not supported in nextest-version" ; "build metadata"
754 )]
755 #[test_case(
756 r#"
757 nextest-version = { required = "0.9.20", recommended = "0.9.10" }
758 "#,
759 "required version (0.9.20) must not be greater than recommended version (0.9.10)" ; "error greater than warning"
760 )]
761 fn test_invalid_nextest_version(input: &str, error_message: &str) {
762 let err = toml::from_str::<VersionOnlyDeserialize>(input).unwrap_err();
763 assert!(
764 err.to_string().contains(error_message),
765 "error `{err}` contains `{error_message}`"
766 );
767 }
768
769 fn tool_name(s: &str) -> ToolName {
770 ToolName::new(s.into()).unwrap()
771 }
772
773 #[test]
774 fn test_accumulate() {
775 let mut nextest_version = NextestVersionConfig::default();
776 nextest_version.accumulate(
777 NextestVersionDeserialize {
778 required: Some("0.9.20".parse().unwrap()),
779 recommended: None,
780 },
781 Some(tool_name("tool1")),
782 );
783 nextest_version.accumulate(
784 NextestVersionDeserialize {
785 required: Some("0.9.30".parse().unwrap()),
786 recommended: Some("0.9.35".parse().unwrap()),
787 },
788 Some(tool_name("tool2")),
789 );
790 nextest_version.accumulate(
791 NextestVersionDeserialize {
792 required: None,
793 recommended: Some("0.9.25".parse().unwrap()),
796 },
797 Some(tool_name("tool3")),
798 );
799 nextest_version.accumulate(
800 NextestVersionDeserialize {
801 required: Some("0.9.30".parse().unwrap()),
804 recommended: None,
805 },
806 Some(tool_name("tool4")),
807 );
808
809 assert_eq!(
810 nextest_version,
811 NextestVersionConfig {
812 required: NextestVersionReq::Version {
813 version: "0.9.30".parse().unwrap(),
814 tool: Some(tool_name("tool4")),
815 },
816 recommended: NextestVersionReq::Version {
817 version: "0.9.35".parse().unwrap(),
818 tool: Some(tool_name("tool2")),
819 },
820 }
821 );
822 }
823
824 #[test]
825 fn test_from_env_benchmarks() {
826 unsafe { std::env::set_var("NEXTEST_EXPERIMENTAL_BENCHMARKS", "1") };
829 assert!(ConfigExperimental::from_env().contains(&ConfigExperimental::Benchmarks));
830
831 unsafe { std::env::set_var("NEXTEST_EXPERIMENTAL_BENCHMARKS", "0") };
835 assert!(!ConfigExperimental::from_env().contains(&ConfigExperimental::Benchmarks));
836
837 unsafe { std::env::set_var("NEXTEST_EXPERIMENTAL_BENCHMARKS", "true") };
840 assert!(!ConfigExperimental::from_env().contains(&ConfigExperimental::Benchmarks));
841
842 unsafe { std::env::set_var("NEXTEST_EXPERIMENTAL_BENCHMARKS", "1") };
847 let set = ConfigExperimental::from_env();
848 assert!(!set.contains(&ConfigExperimental::SetupScripts));
849 assert!(!set.contains(&ConfigExperimental::WrapperScripts));
850 }
851
852 #[test]
853 fn test_experimental_formats() {
854 let input = r#"experimental = ["setup-scripts", "benchmarks"]"#;
856 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
857 assert_eq!(
858 d.experimental.known,
859 BTreeSet::from([
860 ConfigExperimental::SetupScripts,
861 ConfigExperimental::Benchmarks
862 ]),
863 "expected 2 known features"
864 );
865 assert!(d.experimental.unknown.is_empty());
866
867 let input = r#"experimental = []"#;
869 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
870 assert!(
871 d.experimental.is_empty(),
872 "expected empty, got {:?}",
873 d.experimental
874 );
875
876 let input = r#"experimental = ["setup-scripts", "unknown-feature"]"#;
878 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
879 assert_eq!(
880 d.experimental.known,
881 BTreeSet::from([ConfigExperimental::SetupScripts])
882 );
883 assert_eq!(
884 d.experimental.unknown,
885 BTreeSet::from(["unknown-feature".to_owned()])
886 );
887
888 let input = r#"
890[experimental]
891setup-scripts = true
892benchmarks = true
893"#;
894 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
895 assert_eq!(
896 d.experimental.known,
897 BTreeSet::from([
898 ConfigExperimental::SetupScripts,
899 ConfigExperimental::Benchmarks
900 ])
901 );
902 assert!(d.experimental.unknown.is_empty());
903
904 let input = r#"[experimental]"#;
906 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
907 assert!(
908 d.experimental.is_empty(),
909 "expected empty, got {:?}",
910 d.experimental
911 );
912
913 let input = r#"
915[experimental]
916setup-scripts = false
917"#;
918 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
919 assert!(
920 d.experimental.is_empty(),
921 "expected empty, got {:?}",
922 d.experimental
923 );
924
925 let input = r#"
927[experimental]
928setup-scripts = true
929unknown-feature = true
930"#;
931 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
932 assert_eq!(
933 d.experimental.known,
934 BTreeSet::from([ConfigExperimental::SetupScripts])
935 );
936 assert!(d.experimental.unknown.contains("unknown-feature"));
937
938 let input = r#"experimental = 42"#;
940 let err = toml::from_str::<VersionOnlyDeserialize>(input).unwrap_err();
941 let err_str = err.to_string();
942 assert!(
943 err_str.contains("expected a table") && err_str.contains("or an array"),
944 "expected error to mention both formats, got: {}",
945 err_str
946 );
947
948 let input = r#"experimental = "setup-scripts""#;
949 let err = toml::from_str::<VersionOnlyDeserialize>(input).unwrap_err();
950 let err_str = err.to_string();
951 assert!(
952 err_str.contains("expected a table") && err_str.contains("or an array"),
953 "expected error to mention both formats, got: {}",
954 err_str
955 );
956 }
957}