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#[cfg(feature = "config-schema")]
262impl schemars::JsonSchema for ExperimentalDeserialize {
263 fn schema_name() -> Cow<'static, str> {
264 "ExperimentalDeserialize".into()
265 }
266
267 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
268 schemars::json_schema!({
269 "oneOf": [
270 {
271 "type": "array",
272 "items": {
273 "type": "string",
274 "enum": ["setup-scripts", "wrapper-scripts", "benchmarks"],
275 },
276 },
277 {
278 "type": "object",
279 "properties": {
280 "setup-scripts": generator.subschema_for::<bool>(),
281 "wrapper-scripts": generator.subschema_for::<bool>(),
282 "benchmarks": generator.subschema_for::<bool>(),
283 },
284 "additionalProperties": true,
285 }
286 ]
287 })
288 }
289}
290
291#[derive(Debug, Default, Clone, PartialEq, Eq)]
297pub struct NextestVersionConfig {
298 pub required: NextestVersionReq,
300
301 pub recommended: NextestVersionReq,
306}
307
308impl NextestVersionConfig {
309 pub(crate) fn accumulate(&mut self, v: NextestVersionDeserialize, v_tool: Option<ToolName>) {
311 if let Some(version) = v.required {
312 self.required.accumulate(version, v_tool.clone());
313 }
314 if let Some(version) = v.recommended {
315 self.recommended.accumulate(version, v_tool);
316 }
317 }
318
319 pub fn eval(
321 &self,
322 current_version: &Version,
323 override_version_check: bool,
324 ) -> NextestVersionEval {
325 match self.required.satisfies(current_version) {
326 Ok(()) => {}
327 Err((required, tool)) => {
328 if override_version_check {
329 return NextestVersionEval::ErrorOverride {
330 required: required.clone(),
331 current: current_version.clone(),
332 tool: tool.cloned(),
333 };
334 } else {
335 return NextestVersionEval::Error {
336 required: required.clone(),
337 current: current_version.clone(),
338 tool: tool.cloned(),
339 };
340 }
341 }
342 }
343
344 match self.recommended.satisfies(current_version) {
345 Ok(()) => NextestVersionEval::Satisfied,
346 Err((recommended, tool)) => {
347 if override_version_check {
348 NextestVersionEval::WarnOverride {
349 recommended: recommended.clone(),
350 current: current_version.clone(),
351 tool: tool.cloned(),
352 }
353 } else {
354 NextestVersionEval::Warn {
355 recommended: recommended.clone(),
356 current: current_version.clone(),
357 tool: tool.cloned(),
358 }
359 }
360 }
361 }
362 }
363}
364
365#[derive(Debug, Default, Clone, PartialEq, Eq)]
370pub struct ExperimentalConfig {
371 known: BTreeSet<ConfigExperimental>,
373
374 unknown: BTreeSet<String>,
376}
377
378impl ExperimentalConfig {
379 pub fn known(&self) -> &BTreeSet<ConfigExperimental> {
381 &self.known
382 }
383
384 pub fn eval(&self) -> ExperimentalConfigEval {
389 if self.unknown.is_empty() {
390 ExperimentalConfigEval::Satisfied
391 } else {
392 ExperimentalConfigEval::UnknownFeatures {
393 unknown: self.unknown.clone(),
394 known: ConfigExperimental::known_features().collect(),
395 }
396 }
397 }
398}
399
400#[derive(Debug, Clone, PartialEq, Eq)]
404pub enum ExperimentalConfigEval {
405 Satisfied,
407
408 UnknownFeatures {
410 unknown: BTreeSet<String>,
412
413 known: BTreeSet<ConfigExperimental>,
415 },
416}
417
418impl ExperimentalConfigEval {
419 pub fn into_error(self, config_file: impl Into<Utf8PathBuf>) -> Option<ConfigParseError> {
423 match self {
424 ExperimentalConfigEval::Satisfied => None,
425 ExperimentalConfigEval::UnknownFeatures { unknown, known } => {
426 Some(ConfigParseError::new(
427 config_file,
428 None,
429 ConfigParseErrorKind::UnknownExperimentalFeatures { unknown, known },
430 ))
431 }
432 }
433 }
434}
435
436#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
438#[non_exhaustive]
439pub enum ConfigExperimental {
440 SetupScripts,
442 WrapperScripts,
444 Benchmarks,
446}
447
448impl ConfigExperimental {
449 pub fn known_features() -> impl Iterator<Item = Self> {
451 vec![Self::SetupScripts, Self::WrapperScripts, Self::Benchmarks].into_iter()
452 }
453
454 pub fn env_var(self) -> Option<&'static str> {
456 match self {
457 Self::SetupScripts => None,
458 Self::WrapperScripts => None,
459 Self::Benchmarks => Some("NEXTEST_EXPERIMENTAL_BENCHMARKS"),
460 }
461 }
462
463 pub fn from_env() -> std::collections::BTreeSet<Self> {
465 let mut set = std::collections::BTreeSet::new();
466 for feature in Self::known_features() {
467 if let Some(env_var) = feature.env_var()
468 && std::env::var(env_var).as_deref() == Ok("1")
469 {
470 set.insert(feature);
471 }
472 }
473 set
474 }
475}
476
477impl FromStr for ConfigExperimental {
478 type Err = ();
479
480 fn from_str(s: &str) -> Result<Self, Self::Err> {
481 match s {
482 "setup-scripts" => Ok(Self::SetupScripts),
483 "wrapper-scripts" => Ok(Self::WrapperScripts),
484 "benchmarks" => Ok(Self::Benchmarks),
485 _ => Err(()),
486 }
487 }
488}
489
490impl fmt::Display for ConfigExperimental {
491 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
492 match self {
493 Self::SetupScripts => write!(f, "setup-scripts"),
494 Self::WrapperScripts => write!(f, "wrapper-scripts"),
495 Self::Benchmarks => write!(f, "benchmarks"),
496 }
497 }
498}
499
500#[derive(Debug, Default, Clone, PartialEq, Eq)]
502pub enum NextestVersionReq {
503 Version {
505 version: Version,
507
508 tool: Option<ToolName>,
510 },
511
512 #[default]
514 None,
515}
516
517impl NextestVersionReq {
518 pub fn version(&self) -> Option<&Version> {
520 match self {
521 NextestVersionReq::Version { version, .. } => Some(version),
522 NextestVersionReq::None => None,
523 }
524 }
525
526 fn accumulate(&mut self, v: Version, v_tool: Option<ToolName>) {
527 match self {
528 NextestVersionReq::Version { version, tool } => {
529 if &v >= version {
532 *version = v;
533 *tool = v_tool;
534 }
535 }
536 NextestVersionReq::None => {
537 *self = NextestVersionReq::Version {
538 version: v,
539 tool: v_tool,
540 };
541 }
542 }
543 }
544
545 fn satisfies(&self, version: &Version) -> Result<(), (&Version, Option<&ToolName>)> {
546 match self {
547 NextestVersionReq::Version {
548 version: required,
549 tool,
550 } => {
551 if version >= required {
552 Ok(())
553 } else {
554 Err((required, tool.as_ref()))
555 }
556 }
557 NextestVersionReq::None => Ok(()),
558 }
559 }
560}
561
562#[derive(Debug, Clone, PartialEq, Eq)]
566pub enum NextestVersionEval {
567 Satisfied,
569
570 Error {
572 required: Version,
574 current: Version,
576 tool: Option<ToolName>,
578 },
579
580 Warn {
582 recommended: Version,
584 current: Version,
586 tool: Option<ToolName>,
588 },
589
590 ErrorOverride {
592 required: Version,
594 current: Version,
596 tool: Option<ToolName>,
598 },
599
600 WarnOverride {
602 recommended: Version,
604 current: Version,
606 tool: Option<ToolName>,
608 },
609}
610
611#[derive(Debug, Clone, PartialEq, Eq)]
617pub(crate) struct NextestVersionDeserialize {
618 required: Option<Version>,
620
621 recommended: Option<Version>,
623}
624
625impl<'de> Deserialize<'de> for NextestVersionDeserialize {
626 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
627 where
628 D: Deserializer<'de>,
629 {
630 struct V;
631
632 impl<'de2> serde::de::Visitor<'de2> for V {
633 type Value = NextestVersionDeserialize;
634
635 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
636 formatter.write_str(
637 "a table ({{ required = \"0.9.20\", recommended = \"0.9.30\" }}) or a string (\"0.9.50\")",
638 )
639 }
640
641 fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
642 where
643 E: serde::de::Error,
644 {
645 let required = parse_version::<E>(s.to_owned())?;
646 Ok(NextestVersionDeserialize {
647 required: Some(required),
648 recommended: None,
649 })
650 }
651
652 fn visit_map<A>(self, map: A) -> std::result::Result<Self::Value, A::Error>
653 where
654 A: serde::de::MapAccess<'de2>,
655 {
656 #[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
657 struct NextestVersionMap {
658 #[serde(default, deserialize_with = "deserialize_version_opt")]
659 required: Option<Version>,
660 #[serde(default, deserialize_with = "deserialize_version_opt")]
661 recommended: Option<Version>,
662 }
663
664 let NextestVersionMap {
665 required,
666 recommended,
667 } = NextestVersionMap::deserialize(serde::de::value::MapAccessDeserializer::new(
668 map,
669 ))?;
670
671 if let (Some(required), Some(recommended)) = (&required, &recommended)
672 && required > recommended
673 {
674 return Err(serde::de::Error::custom(format!(
675 "required version ({required}) must not be greater than recommended version ({recommended})"
676 )));
677 }
678
679 Ok(NextestVersionDeserialize {
680 required,
681 recommended,
682 })
683 }
684 }
685
686 deserializer.deserialize_any(V)
687 }
688}
689
690#[cfg(feature = "config-schema")]
691impl schemars::JsonSchema for NextestVersionDeserialize {
692 fn schema_name() -> Cow<'static, str> {
693 "NextestVersionDeserialize".into()
694 }
695
696 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
697 schemars::json_schema!({
698 "oneOf": [
699 generator.subschema_for::<String>(),
700 {
701 "type": "object",
702 "properties": {
703 "required": generator.subschema_for::<String>(),
704 "recommended": generator.subschema_for::<String>(),
705 },
706 "additionalProperties": false,
707 }
708 ]
709 })
710 }
711}
712
713fn deserialize_version_opt<'de, D>(
718 deserializer: D,
719) -> std::result::Result<Option<Version>, D::Error>
720where
721 D: Deserializer<'de>,
722{
723 let s = Option::<String>::deserialize(deserializer)?;
724 s.map(parse_version::<D::Error>).transpose()
725}
726
727fn parse_version<E>(mut s: String) -> std::result::Result<Version, E>
728where
729 E: serde::de::Error,
730{
731 for ch in s.chars() {
732 if ch == '-' {
733 return Err(E::custom(
734 "pre-release identifiers are not supported in nextest-version",
735 ));
736 } else if ch == '+' {
737 return Err(E::custom(
738 "build metadata is not supported in nextest-version",
739 ));
740 }
741 }
742
743 if s.matches('.').count() == 1 {
746 s.push_str(".0");
748 }
749
750 Version::parse(&s).map_err(E::custom)
751}
752
753#[cfg(test)]
754mod tests {
755 use super::*;
756 use test_case::test_case;
757
758 #[test_case(
759 r#"
760 nextest-version = "0.9"
761 "#,
762 NextestVersionDeserialize { required: Some("0.9.0".parse().unwrap()), recommended: None } ; "basic"
763 )]
764 #[test_case(
765 r#"
766 nextest-version = "0.9.30"
767 "#,
768 NextestVersionDeserialize { required: Some("0.9.30".parse().unwrap()), recommended: None } ; "basic with patch"
769 )]
770 #[test_case(
771 r#"
772 nextest-version = { recommended = "0.9.20" }
773 "#,
774 NextestVersionDeserialize { required: None, recommended: Some("0.9.20".parse().unwrap()) } ; "with warning"
775 )]
776 #[test_case(
777 r#"
778 nextest-version = { required = "0.9.20", recommended = "0.9.25" }
779 "#,
780 NextestVersionDeserialize {
781 required: Some("0.9.20".parse().unwrap()),
782 recommended: Some("0.9.25".parse().unwrap()),
783 } ; "with error and warning"
784 )]
785 fn test_valid_nextest_version(input: &str, expected: NextestVersionDeserialize) {
786 let actual: VersionOnlyDeserialize = toml::from_str(input).unwrap();
787 assert_eq!(actual.nextest_version.unwrap(), expected);
788 }
789
790 #[test_case(
791 r#"
792 nextest-version = 42
793 "#,
794 "a table ({{ required = \"0.9.20\", recommended = \"0.9.30\" }}) or a string (\"0.9.50\")" ; "empty"
795 )]
796 #[test_case(
797 r#"
798 nextest-version = "0.9.30-rc.1"
799 "#,
800 "pre-release identifiers are not supported in nextest-version" ; "pre-release"
801 )]
802 #[test_case(
803 r#"
804 nextest-version = "0.9.40+mybuild"
805 "#,
806 "build metadata is not supported in nextest-version" ; "build metadata"
807 )]
808 #[test_case(
809 r#"
810 nextest-version = { required = "0.9.20", recommended = "0.9.10" }
811 "#,
812 "required version (0.9.20) must not be greater than recommended version (0.9.10)" ; "error greater than warning"
813 )]
814 fn test_invalid_nextest_version(input: &str, error_message: &str) {
815 let err = toml::from_str::<VersionOnlyDeserialize>(input).unwrap_err();
816 assert!(
817 err.to_string().contains(error_message),
818 "error `{err}` contains `{error_message}`"
819 );
820 }
821
822 fn tool_name(s: &str) -> ToolName {
823 ToolName::new(s.into()).unwrap()
824 }
825
826 #[test]
827 fn test_accumulate() {
828 let mut nextest_version = NextestVersionConfig::default();
829 nextest_version.accumulate(
830 NextestVersionDeserialize {
831 required: Some("0.9.20".parse().unwrap()),
832 recommended: None,
833 },
834 Some(tool_name("tool1")),
835 );
836 nextest_version.accumulate(
837 NextestVersionDeserialize {
838 required: Some("0.9.30".parse().unwrap()),
839 recommended: Some("0.9.35".parse().unwrap()),
840 },
841 Some(tool_name("tool2")),
842 );
843 nextest_version.accumulate(
844 NextestVersionDeserialize {
845 required: None,
846 recommended: Some("0.9.25".parse().unwrap()),
849 },
850 Some(tool_name("tool3")),
851 );
852 nextest_version.accumulate(
853 NextestVersionDeserialize {
854 required: Some("0.9.30".parse().unwrap()),
857 recommended: None,
858 },
859 Some(tool_name("tool4")),
860 );
861
862 assert_eq!(
863 nextest_version,
864 NextestVersionConfig {
865 required: NextestVersionReq::Version {
866 version: "0.9.30".parse().unwrap(),
867 tool: Some(tool_name("tool4")),
868 },
869 recommended: NextestVersionReq::Version {
870 version: "0.9.35".parse().unwrap(),
871 tool: Some(tool_name("tool2")),
872 },
873 }
874 );
875 }
876
877 #[test]
878 fn test_from_env_benchmarks() {
879 unsafe { std::env::set_var("NEXTEST_EXPERIMENTAL_BENCHMARKS", "1") };
882 assert!(ConfigExperimental::from_env().contains(&ConfigExperimental::Benchmarks));
883
884 unsafe { std::env::set_var("NEXTEST_EXPERIMENTAL_BENCHMARKS", "0") };
888 assert!(!ConfigExperimental::from_env().contains(&ConfigExperimental::Benchmarks));
889
890 unsafe { std::env::set_var("NEXTEST_EXPERIMENTAL_BENCHMARKS", "true") };
893 assert!(!ConfigExperimental::from_env().contains(&ConfigExperimental::Benchmarks));
894
895 unsafe { std::env::set_var("NEXTEST_EXPERIMENTAL_BENCHMARKS", "1") };
900 let set = ConfigExperimental::from_env();
901 assert!(!set.contains(&ConfigExperimental::SetupScripts));
902 assert!(!set.contains(&ConfigExperimental::WrapperScripts));
903 }
904
905 #[test]
906 fn test_experimental_formats() {
907 let input = r#"experimental = ["setup-scripts", "benchmarks"]"#;
909 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
910 assert_eq!(
911 d.experimental.known,
912 BTreeSet::from([
913 ConfigExperimental::SetupScripts,
914 ConfigExperimental::Benchmarks
915 ]),
916 "expected 2 known features"
917 );
918 assert!(d.experimental.unknown.is_empty());
919
920 let input = r#"experimental = []"#;
922 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
923 assert!(
924 d.experimental.is_empty(),
925 "expected empty, got {:?}",
926 d.experimental
927 );
928
929 let input = r#"experimental = ["setup-scripts", "unknown-feature"]"#;
931 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
932 assert_eq!(
933 d.experimental.known,
934 BTreeSet::from([ConfigExperimental::SetupScripts])
935 );
936 assert_eq!(
937 d.experimental.unknown,
938 BTreeSet::from(["unknown-feature".to_owned()])
939 );
940
941 let input = r#"
943[experimental]
944setup-scripts = true
945benchmarks = true
946"#;
947 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
948 assert_eq!(
949 d.experimental.known,
950 BTreeSet::from([
951 ConfigExperimental::SetupScripts,
952 ConfigExperimental::Benchmarks
953 ])
954 );
955 assert!(d.experimental.unknown.is_empty());
956
957 let input = r#"[experimental]"#;
959 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
960 assert!(
961 d.experimental.is_empty(),
962 "expected empty, got {:?}",
963 d.experimental
964 );
965
966 let input = r#"
968[experimental]
969setup-scripts = false
970"#;
971 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
972 assert!(
973 d.experimental.is_empty(),
974 "expected empty, got {:?}",
975 d.experimental
976 );
977
978 let input = r#"
980[experimental]
981setup-scripts = true
982unknown-feature = true
983"#;
984 let d: VersionOnlyDeserialize = toml::from_str(input).unwrap();
985 assert_eq!(
986 d.experimental.known,
987 BTreeSet::from([ConfigExperimental::SetupScripts])
988 );
989 assert!(d.experimental.unknown.contains("unknown-feature"));
990
991 let input = r#"experimental = 42"#;
993 let err = toml::from_str::<VersionOnlyDeserialize>(input).unwrap_err();
994 let err_str = err.to_string();
995 assert!(
996 err_str.contains("expected a table") && err_str.contains("or an array"),
997 "expected error to mention both formats, got: {}",
998 err_str
999 );
1000
1001 let input = r#"experimental = "setup-scripts""#;
1002 let err = toml::from_str::<VersionOnlyDeserialize>(input).unwrap_err();
1003 let err_str = err.to_string();
1004 assert!(
1005 err_str.contains("expected a table") && err_str.contains("or an array"),
1006 "expected error to mention both formats, got: {}",
1007 err_str
1008 );
1009 }
1010}