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 fn accumulate(&mut self, v: Version, v_tool: Option<ToolName>) {
489 match self {
490 NextestVersionReq::Version { version, tool } => {
491 if &v >= version {
494 *version = v;
495 *tool = v_tool;
496 }
497 }
498 NextestVersionReq::None => {
499 *self = NextestVersionReq::Version {
500 version: v,
501 tool: v_tool,
502 };
503 }
504 }
505 }
506
507 fn satisfies(&self, version: &Version) -> Result<(), (&Version, Option<&ToolName>)> {
508 match self {
509 NextestVersionReq::Version {
510 version: required,
511 tool,
512 } => {
513 if version >= required {
514 Ok(())
515 } else {
516 Err((required, tool.as_ref()))
517 }
518 }
519 NextestVersionReq::None => Ok(()),
520 }
521 }
522}
523
524#[derive(Debug, Clone, PartialEq, Eq)]
528pub enum NextestVersionEval {
529 Satisfied,
531
532 Error {
534 required: Version,
536 current: Version,
538 tool: Option<ToolName>,
540 },
541
542 Warn {
544 recommended: Version,
546 current: Version,
548 tool: Option<ToolName>,
550 },
551
552 ErrorOverride {
554 required: Version,
556 current: Version,
558 tool: Option<ToolName>,
560 },
561
562 WarnOverride {
564 recommended: Version,
566 current: Version,
568 tool: Option<ToolName>,
570 },
571}
572
573#[derive(Debug, Clone, PartialEq, Eq)]
579pub(crate) struct NextestVersionDeserialize {
580 required: Option<Version>,
582
583 recommended: Option<Version>,
585}
586
587impl<'de> Deserialize<'de> for NextestVersionDeserialize {
588 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
589 where
590 D: Deserializer<'de>,
591 {
592 struct V;
593
594 impl<'de2> serde::de::Visitor<'de2> for V {
595 type Value = NextestVersionDeserialize;
596
597 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
598 formatter.write_str(
599 "a table ({{ required = \"0.9.20\", recommended = \"0.9.30\" }}) or a string (\"0.9.50\")",
600 )
601 }
602
603 fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
604 where
605 E: serde::de::Error,
606 {
607 let required = parse_version::<E>(s.to_owned())?;
608 Ok(NextestVersionDeserialize {
609 required: Some(required),
610 recommended: None,
611 })
612 }
613
614 fn visit_map<A>(self, map: A) -> std::result::Result<Self::Value, A::Error>
615 where
616 A: serde::de::MapAccess<'de2>,
617 {
618 #[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
619 struct NextestVersionMap {
620 #[serde(default, deserialize_with = "deserialize_version_opt")]
621 required: Option<Version>,
622 #[serde(default, deserialize_with = "deserialize_version_opt")]
623 recommended: Option<Version>,
624 }
625
626 let NextestVersionMap {
627 required,
628 recommended,
629 } = NextestVersionMap::deserialize(serde::de::value::MapAccessDeserializer::new(
630 map,
631 ))?;
632
633 if let (Some(required), Some(recommended)) = (&required, &recommended)
634 && required > recommended
635 {
636 return Err(serde::de::Error::custom(format!(
637 "required version ({required}) must not be greater than recommended version ({recommended})"
638 )));
639 }
640
641 Ok(NextestVersionDeserialize {
642 required,
643 recommended,
644 })
645 }
646 }
647
648 deserializer.deserialize_any(V)
649 }
650}
651
652fn deserialize_version_opt<'de, D>(
657 deserializer: D,
658) -> std::result::Result<Option<Version>, D::Error>
659where
660 D: Deserializer<'de>,
661{
662 let s = Option::<String>::deserialize(deserializer)?;
663 s.map(parse_version::<D::Error>).transpose()
664}
665
666fn parse_version<E>(mut s: String) -> std::result::Result<Version, E>
667where
668 E: serde::de::Error,
669{
670 for ch in s.chars() {
671 if ch == '-' {
672 return Err(E::custom(
673 "pre-release identifiers are not supported in nextest-version",
674 ));
675 } else if ch == '+' {
676 return Err(E::custom(
677 "build metadata is not supported in nextest-version",
678 ));
679 }
680 }
681
682 if s.matches('.').count() == 1 {
685 s.push_str(".0");
687 }
688
689 Version::parse(&s).map_err(E::custom)
690}
691
692#[cfg(test)]
693mod tests {
694 use super::*;
695 use test_case::test_case;
696
697 #[test_case(
698 r#"
699 nextest-version = "0.9"
700 "#,
701 NextestVersionDeserialize { required: Some("0.9.0".parse().unwrap()), recommended: None } ; "basic"
702 )]
703 #[test_case(
704 r#"
705 nextest-version = "0.9.30"
706 "#,
707 NextestVersionDeserialize { required: Some("0.9.30".parse().unwrap()), recommended: None } ; "basic with patch"
708 )]
709 #[test_case(
710 r#"
711 nextest-version = { recommended = "0.9.20" }
712 "#,
713 NextestVersionDeserialize { required: None, recommended: Some("0.9.20".parse().unwrap()) } ; "with warning"
714 )]
715 #[test_case(
716 r#"
717 nextest-version = { required = "0.9.20", recommended = "0.9.25" }
718 "#,
719 NextestVersionDeserialize {
720 required: Some("0.9.20".parse().unwrap()),
721 recommended: Some("0.9.25".parse().unwrap()),
722 } ; "with error and warning"
723 )]
724 fn test_valid_nextest_version(input: &str, expected: NextestVersionDeserialize) {
725 let actual: VersionOnlyDeserialize = toml::from_str(input).unwrap();
726 assert_eq!(actual.nextest_version.unwrap(), expected);
727 }
728
729 #[test_case(
730 r#"
731 nextest-version = 42
732 "#,
733 "a table ({{ required = \"0.9.20\", recommended = \"0.9.30\" }}) or a string (\"0.9.50\")" ; "empty"
734 )]
735 #[test_case(
736 r#"
737 nextest-version = "0.9.30-rc.1"
738 "#,
739 "pre-release identifiers are not supported in nextest-version" ; "pre-release"
740 )]
741 #[test_case(
742 r#"
743 nextest-version = "0.9.40+mybuild"
744 "#,
745 "build metadata is not supported in nextest-version" ; "build metadata"
746 )]
747 #[test_case(
748 r#"
749 nextest-version = { required = "0.9.20", recommended = "0.9.10" }
750 "#,
751 "required version (0.9.20) must not be greater than recommended version (0.9.10)" ; "error greater than warning"
752 )]
753 fn test_invalid_nextest_version(input: &str, error_message: &str) {
754 let err = toml::from_str::<VersionOnlyDeserialize>(input).unwrap_err();
755 assert!(
756 err.to_string().contains(error_message),
757 "error `{err}` contains `{error_message}`"
758 );
759 }
760
761 fn tool_name(s: &str) -> ToolName {
762 ToolName::new(s.into()).unwrap()
763 }
764
765 #[test]
766 fn test_accumulate() {
767 let mut nextest_version = NextestVersionConfig::default();
768 nextest_version.accumulate(
769 NextestVersionDeserialize {
770 required: Some("0.9.20".parse().unwrap()),
771 recommended: None,
772 },
773 Some(tool_name("tool1")),
774 );
775 nextest_version.accumulate(
776 NextestVersionDeserialize {
777 required: Some("0.9.30".parse().unwrap()),
778 recommended: Some("0.9.35".parse().unwrap()),
779 },
780 Some(tool_name("tool2")),
781 );
782 nextest_version.accumulate(
783 NextestVersionDeserialize {
784 required: None,
785 recommended: Some("0.9.25".parse().unwrap()),
788 },
789 Some(tool_name("tool3")),
790 );
791 nextest_version.accumulate(
792 NextestVersionDeserialize {
793 required: Some("0.9.30".parse().unwrap()),
796 recommended: None,
797 },
798 Some(tool_name("tool4")),
799 );
800
801 assert_eq!(
802 nextest_version,
803 NextestVersionConfig {
804 required: NextestVersionReq::Version {
805 version: "0.9.30".parse().unwrap(),
806 tool: Some(tool_name("tool4")),
807 },
808 recommended: NextestVersionReq::Version {
809 version: "0.9.35".parse().unwrap(),
810 tool: Some(tool_name("tool2")),
811 },
812 }
813 );
814 }
815
816 #[test]
817 fn test_from_env_benchmarks() {
818 unsafe { std::env::set_var("NEXTEST_EXPERIMENTAL_BENCHMARKS", "1") };
821 assert!(ConfigExperimental::from_env().contains(&ConfigExperimental::Benchmarks));
822
823 unsafe { std::env::set_var("NEXTEST_EXPERIMENTAL_BENCHMARKS", "0") };
827 assert!(!ConfigExperimental::from_env().contains(&ConfigExperimental::Benchmarks));
828
829 unsafe { std::env::set_var("NEXTEST_EXPERIMENTAL_BENCHMARKS", "true") };
832 assert!(!ConfigExperimental::from_env().contains(&ConfigExperimental::Benchmarks));
833
834 unsafe { std::env::set_var("NEXTEST_EXPERIMENTAL_BENCHMARKS", "1") };
839 let set = ConfigExperimental::from_env();
840 assert!(!set.contains(&ConfigExperimental::SetupScripts));
841 assert!(!set.contains(&ConfigExperimental::WrapperScripts));
842 }
843
844 #[test]
845 fn test_experimental_formats() {
846 let input = r#"experimental = ["setup-scripts", "benchmarks"]"#;
848 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
849 assert_eq!(
850 d.experimental.known,
851 BTreeSet::from([
852 ConfigExperimental::SetupScripts,
853 ConfigExperimental::Benchmarks
854 ]),
855 "expected 2 known features"
856 );
857 assert!(d.experimental.unknown.is_empty());
858
859 let input = r#"experimental = []"#;
861 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
862 assert!(
863 d.experimental.is_empty(),
864 "expected empty, got {:?}",
865 d.experimental
866 );
867
868 let input = r#"experimental = ["setup-scripts", "unknown-feature"]"#;
870 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
871 assert_eq!(
872 d.experimental.known,
873 BTreeSet::from([ConfigExperimental::SetupScripts])
874 );
875 assert_eq!(
876 d.experimental.unknown,
877 BTreeSet::from(["unknown-feature".to_owned()])
878 );
879
880 let input = r#"
882[experimental]
883setup-scripts = true
884benchmarks = true
885"#;
886 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
887 assert_eq!(
888 d.experimental.known,
889 BTreeSet::from([
890 ConfigExperimental::SetupScripts,
891 ConfigExperimental::Benchmarks
892 ])
893 );
894 assert!(d.experimental.unknown.is_empty());
895
896 let input = r#"[experimental]"#;
898 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
899 assert!(
900 d.experimental.is_empty(),
901 "expected empty, got {:?}",
902 d.experimental
903 );
904
905 let input = r#"
907[experimental]
908setup-scripts = false
909"#;
910 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
911 assert!(
912 d.experimental.is_empty(),
913 "expected empty, got {:?}",
914 d.experimental
915 );
916
917 let input = r#"
919[experimental]
920setup-scripts = true
921unknown-feature = true
922"#;
923 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
924 assert_eq!(
925 d.experimental.known,
926 BTreeSet::from([ConfigExperimental::SetupScripts])
927 );
928 assert!(d.experimental.unknown.contains("unknown-feature"));
929
930 let input = r#"experimental = 42"#;
932 let err = toml::from_str::<VersionOnlyDeserialize>(input).unwrap_err();
933 let err_str = err.to_string();
934 assert!(
935 err_str.contains("expected a table") && err_str.contains("or an array"),
936 "expected error to mention both formats, got: {}",
937 err_str
938 );
939
940 let input = r#"experimental = "setup-scripts""#;
941 let err = toml::from_str::<VersionOnlyDeserialize>(input).unwrap_err();
942 let err_str = err.to_string();
943 assert!(
944 err_str.contains("expected a table") && err_str.contains("or an array"),
945 "expected error to mention both formats, got: {}",
946 err_str
947 );
948 }
949}