1use crate::{
7 cargo_config::{TargetTriple, TargetTripleSource},
8 config::{
9 core::{ConfigExperimental, ToolName},
10 elements::{CustomTestGroup, TestGroup},
11 scripts::{ProfileScriptType, ScriptId, ScriptType},
12 },
13 helpers::{display_exited_with, dylib_path_envvar},
14 indenter::{DisplayIndented, indented},
15 redact::Redactor,
16 reuse_build::{ArchiveFormat, ArchiveStep},
17 target_runner::PlatformRunnerSource,
18};
19use camino::{FromPathBufError, Utf8Path, Utf8PathBuf};
20use config::ConfigError;
21use itertools::{Either, Itertools};
22use nextest_filtering::errors::FiltersetParseErrors;
23use nextest_metadata::RustBinaryId;
24use smol_str::SmolStr;
25use std::{
26 borrow::Cow,
27 collections::BTreeSet,
28 env::JoinPathsError,
29 fmt::{self, Write as _},
30 process::ExitStatus,
31 sync::Arc,
32};
33use target_spec_miette::IntoMietteDiagnostic;
34use thiserror::Error;
35
36#[derive(Debug, Error)]
38#[error(
39 "failed to parse nextest config at `{config_file}`{}",
40 provided_by_tool(tool.as_ref())
41)]
42#[non_exhaustive]
43pub struct ConfigParseError {
44 config_file: Utf8PathBuf,
45 tool: Option<ToolName>,
46 #[source]
47 kind: ConfigParseErrorKind,
48}
49
50impl ConfigParseError {
51 pub(crate) fn new(
52 config_file: impl Into<Utf8PathBuf>,
53 tool: Option<&ToolName>,
54 kind: ConfigParseErrorKind,
55 ) -> Self {
56 Self {
57 config_file: config_file.into(),
58 tool: tool.cloned(),
59 kind,
60 }
61 }
62
63 pub fn config_file(&self) -> &Utf8Path {
65 &self.config_file
66 }
67
68 pub fn tool(&self) -> Option<&ToolName> {
70 self.tool.as_ref()
71 }
72
73 pub fn kind(&self) -> &ConfigParseErrorKind {
75 &self.kind
76 }
77}
78
79pub fn provided_by_tool(tool: Option<&ToolName>) -> String {
81 match tool {
82 Some(tool) => format!(" provided by tool `{tool}`"),
83 None => String::new(),
84 }
85}
86
87#[derive(Debug, Error)]
91#[non_exhaustive]
92pub enum ConfigParseErrorKind {
93 #[error(transparent)]
95 BuildError(Box<ConfigError>),
96 #[error(transparent)]
98 TomlParseError(Box<toml::de::Error>),
99 #[error(transparent)]
100 DeserializeError(Box<serde_path_to_error::Error<ConfigError>>),
102 #[error(transparent)]
104 VersionOnlyReadError(std::io::Error),
105 #[error(transparent)]
107 VersionOnlyDeserializeError(Box<serde_path_to_error::Error<toml::de::Error>>),
108 #[error("error parsing compiled data (destructure this variant for more details)")]
110 CompileErrors(Vec<ConfigCompileError>),
111 #[error("invalid test groups defined: {}\n(test groups cannot start with '@tool:' unless specified by a tool)", .0.iter().join(", "))]
113 InvalidTestGroupsDefined(BTreeSet<CustomTestGroup>),
114 #[error(
116 "invalid test groups defined by tool: {}\n(test groups must start with '@tool:<tool-name>:')", .0.iter().join(", "))]
117 InvalidTestGroupsDefinedByTool(BTreeSet<CustomTestGroup>),
118 #[error("unknown test groups specified by config (destructure this variant for more details)")]
120 UnknownTestGroups {
121 errors: Vec<UnknownTestGroupError>,
123
124 known_groups: BTreeSet<TestGroup>,
126 },
127 #[error(
129 "both `[script.*]` and `[scripts.*]` defined\n\
130 (hint: [script.*] will be removed in the future: switch to [scripts.setup.*])"
131 )]
132 BothScriptAndScriptsDefined,
133 #[error("invalid config scripts defined: {}\n(config scripts cannot start with '@tool:' unless specified by a tool)", .0.iter().join(", "))]
135 InvalidConfigScriptsDefined(BTreeSet<ScriptId>),
136 #[error(
138 "invalid config scripts defined by tool: {}\n(config scripts must start with '@tool:<tool-name>:')", .0.iter().join(", "))]
139 InvalidConfigScriptsDefinedByTool(BTreeSet<ScriptId>),
140 #[error(
142 "config script names used more than once: {}\n\
143 (config script names must be unique across all script types)", .0.iter().join(", ")
144 )]
145 DuplicateConfigScriptNames(BTreeSet<ScriptId>),
146 #[error(
148 "errors in profile-specific config scripts (destructure this variant for more details)"
149 )]
150 ProfileScriptErrors {
151 errors: Box<ProfileScriptErrors>,
153
154 known_scripts: BTreeSet<ScriptId>,
156 },
157 #[error("unknown experimental features defined (destructure this variant for more details)")]
159 UnknownExperimentalFeatures {
160 unknown: BTreeSet<String>,
162
163 known: BTreeSet<ConfigExperimental>,
165 },
166 #[error(
170 "tool config file specifies experimental features `{}` \
171 -- only repository config files can do so",
172 .features.iter().join(", "),
173 )]
174 ExperimentalFeaturesInToolConfig {
175 features: BTreeSet<String>,
177 },
178 #[error("experimental features used but not enabled: {}", .missing_features.iter().join(", "))]
180 ExperimentalFeaturesNotEnabled {
181 missing_features: BTreeSet<ConfigExperimental>,
183 },
184 #[error("inheritance error(s) detected: {}", .0.iter().join(", "))]
186 InheritanceErrors(Vec<InheritsError>),
187}
188
189#[derive(Debug)]
192#[non_exhaustive]
193pub struct ConfigCompileError {
194 pub profile_name: String,
196
197 pub section: ConfigCompileSection,
199
200 pub kind: ConfigCompileErrorKind,
202}
203
204#[derive(Debug)]
207pub enum ConfigCompileSection {
208 DefaultFilter,
210
211 Override(usize),
213
214 Script(usize),
216}
217
218#[derive(Debug)]
220#[non_exhaustive]
221pub enum ConfigCompileErrorKind {
222 ConstraintsNotSpecified {
224 default_filter_specified: bool,
229 },
230
231 FilterAndDefaultFilterSpecified,
235
236 Parse {
238 host_parse_error: Option<target_spec::Error>,
240
241 target_parse_error: Option<target_spec::Error>,
243
244 filter_parse_errors: Vec<FiltersetParseErrors>,
246 },
247}
248
249impl ConfigCompileErrorKind {
250 pub fn reports(&self) -> impl Iterator<Item = miette::Report> + '_ {
252 match self {
253 Self::ConstraintsNotSpecified {
254 default_filter_specified,
255 } => {
256 let message = if *default_filter_specified {
257 "for override with `default-filter`, `platform` must also be specified"
258 } else {
259 "at least one of `platform` and `filter` must be specified"
260 };
261 Either::Left(std::iter::once(miette::Report::msg(message)))
262 }
263 Self::FilterAndDefaultFilterSpecified => {
264 Either::Left(std::iter::once(miette::Report::msg(
265 "at most one of `filter` and `default-filter` must be specified",
266 )))
267 }
268 Self::Parse {
269 host_parse_error,
270 target_parse_error,
271 filter_parse_errors,
272 } => {
273 let host_parse_report = host_parse_error
274 .as_ref()
275 .map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
276 let target_parse_report = target_parse_error
277 .as_ref()
278 .map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
279 let filter_parse_reports =
280 filter_parse_errors.iter().flat_map(|filter_parse_errors| {
281 filter_parse_errors.errors.iter().map(|single_error| {
282 miette::Report::new(single_error.clone())
283 .with_source_code(filter_parse_errors.input.to_owned())
284 })
285 });
286
287 Either::Right(
288 host_parse_report
289 .into_iter()
290 .chain(target_parse_report)
291 .chain(filter_parse_reports),
292 )
293 }
294 }
295 }
296}
297
298#[derive(Clone, Debug, Error)]
300#[error("test priority ({priority}) out of range: must be between -100 and 100, both inclusive")]
301pub struct TestPriorityOutOfRange {
302 pub priority: i8,
304}
305
306#[derive(Clone, Debug, Error)]
308pub enum ChildStartError {
309 #[error("error creating temporary path for setup script")]
311 TempPath(#[source] Arc<std::io::Error>),
312
313 #[error("error spawning child process")]
315 Spawn(#[source] Arc<std::io::Error>),
316}
317
318#[derive(Clone, Debug, Error)]
320pub enum SetupScriptOutputError {
321 #[error("error opening environment file `{path}`")]
323 EnvFileOpen {
324 path: Utf8PathBuf,
326
327 #[source]
329 error: Arc<std::io::Error>,
330 },
331
332 #[error("error reading environment file `{path}`")]
334 EnvFileRead {
335 path: Utf8PathBuf,
337
338 #[source]
340 error: Arc<std::io::Error>,
341 },
342
343 #[error("line `{line}` in environment file `{path}` not in KEY=VALUE format")]
345 EnvFileParse {
346 path: Utf8PathBuf,
348 line: String,
350 },
351
352 #[error("key `{key}` begins with `NEXTEST`, which is reserved for internal use")]
354 EnvFileReservedKey {
355 key: String,
357 },
358}
359
360#[derive(Clone, Debug)]
365pub struct ErrorList<T> {
366 description: &'static str,
368 inner: Vec<T>,
370}
371
372impl<T: std::error::Error> ErrorList<T> {
373 pub(crate) fn new<U>(description: &'static str, errors: Vec<U>) -> Option<Self>
374 where
375 T: From<U>,
376 {
377 if errors.is_empty() {
378 None
379 } else {
380 Some(Self {
381 description,
382 inner: errors.into_iter().map(T::from).collect(),
383 })
384 }
385 }
386
387 pub(crate) fn short_message(&self) -> String {
389 let string = self.to_string();
390 match string.lines().next() {
391 Some(first_line) => first_line.trim_end_matches(':').to_string(),
393 None => String::new(),
394 }
395 }
396
397 pub fn description(&self) -> &'static str {
399 self.description
400 }
401
402 pub fn iter(&self) -> impl Iterator<Item = &T> {
404 self.inner.iter()
405 }
406
407 pub fn map<U, F>(self, f: F) -> ErrorList<U>
409 where
410 U: std::error::Error,
411 F: FnMut(T) -> U,
412 {
413 ErrorList {
414 description: self.description,
415 inner: self.inner.into_iter().map(f).collect(),
416 }
417 }
418}
419
420impl<T: std::error::Error> IntoIterator for ErrorList<T> {
421 type Item = T;
422 type IntoIter = std::vec::IntoIter<T>;
423
424 fn into_iter(self) -> Self::IntoIter {
425 self.inner.into_iter()
426 }
427}
428
429impl<T: std::error::Error> fmt::Display for ErrorList<T> {
430 fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result {
431 if self.inner.len() == 1 {
433 return write!(f, "{}", self.inner[0]);
434 }
435
436 writeln!(
438 f,
439 "{} errors occurred {}:",
440 self.inner.len(),
441 self.description,
442 )?;
443 for error in &self.inner {
444 let mut indent = indented(f).with_str(" ").skip_initial();
445 writeln!(indent, "* {}", DisplayErrorChain::new(error))?;
446 f = indent.into_inner();
447 }
448 Ok(())
449 }
450}
451
452impl<T: std::error::Error> std::error::Error for ErrorList<T> {
453 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
454 if self.inner.len() == 1 {
455 self.inner[0].source()
456 } else {
457 None
460 }
461 }
462}
463
464pub(crate) struct DisplayErrorChain<E> {
469 error: E,
470 initial_indent: &'static str,
471}
472
473impl<E: std::error::Error> DisplayErrorChain<E> {
474 pub(crate) fn new(error: E) -> Self {
475 Self {
476 error,
477 initial_indent: "",
478 }
479 }
480
481 pub(crate) fn new_with_initial_indent(initial_indent: &'static str, error: E) -> Self {
482 Self {
483 error,
484 initial_indent,
485 }
486 }
487}
488
489impl<E> fmt::Display for DisplayErrorChain<E>
490where
491 E: std::error::Error,
492{
493 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
494 let mut writer = indented(f).with_str(self.initial_indent);
495 write!(writer, "{}", self.error)?;
496
497 let Some(mut cause) = self.error.source() else {
498 return Ok(());
499 };
500
501 write!(writer, "\n caused by:")?;
502
503 loop {
504 writeln!(writer)?;
505 let mut indent = indented(&mut writer).with_str(" ").skip_initial();
507 write!(indent, " - {cause}")?;
508
509 let Some(next_cause) = cause.source() else {
510 break Ok(());
511 };
512
513 cause = next_cause;
514 }
515 }
516}
517
518#[derive(Clone, Debug, Error)]
520pub enum ChildError {
521 #[error(transparent)]
523 Fd(#[from] ChildFdError),
524
525 #[error(transparent)]
527 SetupScriptOutput(#[from] SetupScriptOutputError),
528}
529
530#[derive(Clone, Debug, Error)]
532pub enum ChildFdError {
533 #[error("error reading standard output")]
535 ReadStdout(#[source] Arc<std::io::Error>),
536
537 #[error("error reading standard error")]
539 ReadStderr(#[source] Arc<std::io::Error>),
540
541 #[error("error reading combined stream")]
543 ReadCombined(#[source] Arc<std::io::Error>),
544
545 #[error("error waiting for child process to exit")]
547 Wait(#[source] Arc<std::io::Error>),
548}
549
550#[derive(Clone, Debug, Eq, PartialEq)]
552#[non_exhaustive]
553pub struct UnknownTestGroupError {
554 pub profile_name: String,
556
557 pub name: TestGroup,
559}
560
561#[derive(Clone, Debug, Eq, PartialEq)]
564pub struct ProfileUnknownScriptError {
565 pub profile_name: String,
567
568 pub name: ScriptId,
570}
571
572#[derive(Clone, Debug, Eq, PartialEq)]
575pub struct ProfileWrongConfigScriptTypeError {
576 pub profile_name: String,
578
579 pub name: ScriptId,
581
582 pub attempted: ProfileScriptType,
584
585 pub actual: ScriptType,
587}
588
589#[derive(Clone, Debug, Eq, PartialEq)]
592pub struct ProfileListScriptUsesRunFiltersError {
593 pub profile_name: String,
595
596 pub name: ScriptId,
598
599 pub script_type: ProfileScriptType,
601
602 pub filters: BTreeSet<String>,
604}
605
606#[derive(Clone, Debug, Default)]
608pub struct ProfileScriptErrors {
609 pub unknown_scripts: Vec<ProfileUnknownScriptError>,
611
612 pub wrong_script_types: Vec<ProfileWrongConfigScriptTypeError>,
614
615 pub list_scripts_using_run_filters: Vec<ProfileListScriptUsesRunFiltersError>,
617}
618
619impl ProfileScriptErrors {
620 pub fn is_empty(&self) -> bool {
622 self.unknown_scripts.is_empty()
623 && self.wrong_script_types.is_empty()
624 && self.list_scripts_using_run_filters.is_empty()
625 }
626}
627
628#[derive(Clone, Debug, Error)]
630#[error("profile `{profile}` not found (known profiles: {})", .all_profiles.join(", "))]
631pub struct ProfileNotFound {
632 profile: String,
633 all_profiles: Vec<String>,
634}
635
636impl ProfileNotFound {
637 pub(crate) fn new(
638 profile: impl Into<String>,
639 all_profiles: impl IntoIterator<Item = impl Into<String>>,
640 ) -> Self {
641 let mut all_profiles: Vec<_> = all_profiles.into_iter().map(|s| s.into()).collect();
642 all_profiles.sort_unstable();
643 Self {
644 profile: profile.into(),
645 all_profiles,
646 }
647 }
648}
649
650#[derive(Clone, Debug, Error, Eq, PartialEq)]
652pub enum InvalidIdentifier {
653 #[error("identifier is empty")]
655 Empty,
656
657 #[error("invalid identifier `{0}`")]
659 InvalidXid(SmolStr),
660
661 #[error("tool identifier not of the form \"@tool:tool-name:identifier\": `{0}`")]
663 ToolIdentifierInvalidFormat(SmolStr),
664
665 #[error("tool identifier has empty component: `{0}`")]
667 ToolComponentEmpty(SmolStr),
668
669 #[error("invalid tool identifier `{0}`")]
671 ToolIdentifierInvalidXid(SmolStr),
672}
673
674#[derive(Clone, Debug, Error, Eq, PartialEq)]
676pub enum InvalidToolName {
677 #[error("tool name is empty")]
679 Empty,
680
681 #[error("invalid tool name `{0}`")]
683 InvalidXid(SmolStr),
684
685 #[error("tool name cannot start with \"@tool\": `{0}`")]
687 StartsWithToolPrefix(SmolStr),
688}
689
690#[derive(Clone, Debug, Error)]
692#[error("invalid custom test group name: {0}")]
693pub struct InvalidCustomTestGroupName(pub InvalidIdentifier);
694
695#[derive(Clone, Debug, Error)]
697#[error("invalid configuration script name: {0}")]
698pub struct InvalidConfigScriptName(pub InvalidIdentifier);
699
700#[derive(Clone, Debug, Error, PartialEq, Eq)]
702pub enum ToolConfigFileParseError {
703 #[error(
704 "tool-config-file has invalid format: {input}\n(hint: tool configs must be in the format <tool-name>:<path>)"
705 )]
706 InvalidFormat {
708 input: String,
710 },
711
712 #[error("tool-config-file has invalid tool name: {input}")]
714 InvalidToolName {
715 input: String,
717
718 #[source]
720 error: InvalidToolName,
721 },
722
723 #[error("tool-config-file has empty config file path: {input}")]
725 EmptyConfigFile {
726 input: String,
728 },
729
730 #[error("tool-config-file is not an absolute path: {config_file}")]
732 ConfigFileNotAbsolute {
733 config_file: Utf8PathBuf,
735 },
736}
737
738#[derive(Debug, Error)]
740#[non_exhaustive]
741pub enum UserConfigError {
742 #[error("failed to read user config at {path}")]
744 Read {
745 path: Utf8PathBuf,
747 #[source]
749 error: std::io::Error,
750 },
751
752 #[error("failed to parse user config at {path}")]
754 Parse {
755 path: Utf8PathBuf,
757 #[source]
759 error: toml::de::Error,
760 },
761
762 #[error("user config path contains non-UTF-8 characters")]
764 NonUtf8Path {
765 #[source]
767 error: FromPathBufError,
768 },
769
770 #[error(
772 "for user config at {path}, failed to compile platform spec in [[overrides]] at index {index}"
773 )]
774 OverridePlatformSpec {
775 path: Utf8PathBuf,
777 index: usize,
779 #[source]
781 error: target_spec::Error,
782 },
783}
784
785#[derive(Clone, Debug, Error)]
787#[error("unrecognized value for max-fail: {reason}")]
788pub struct MaxFailParseError {
789 pub reason: String,
791}
792
793impl MaxFailParseError {
794 pub(crate) fn new(reason: impl Into<String>) -> Self {
795 Self {
796 reason: reason.into(),
797 }
798 }
799}
800
801#[derive(Clone, Debug, Error)]
803#[error(
804 "unrecognized value for stress-count: {input}\n\
805 (hint: expected either a positive integer or \"infinite\")"
806)]
807pub struct StressCountParseError {
808 pub input: String,
810}
811
812impl StressCountParseError {
813 pub(crate) fn new(input: impl Into<String>) -> Self {
814 Self {
815 input: input.into(),
816 }
817 }
818}
819
820#[derive(Clone, Debug, Error)]
822#[non_exhaustive]
823pub enum DebuggerCommandParseError {
824 #[error(transparent)]
826 ShellWordsParse(shell_words::ParseError),
827
828 #[error("debugger command cannot be empty")]
830 EmptyCommand,
831}
832
833#[derive(Clone, Debug, Error)]
835#[non_exhaustive]
836pub enum TracerCommandParseError {
837 #[error(transparent)]
839 ShellWordsParse(shell_words::ParseError),
840
841 #[error("tracer command cannot be empty")]
843 EmptyCommand,
844}
845
846#[derive(Clone, Debug, Error)]
848#[error(
849 "unrecognized value for test-threads: {input}\n(hint: expected either an integer or \"num-cpus\")"
850)]
851pub struct TestThreadsParseError {
852 pub input: String,
854}
855
856impl TestThreadsParseError {
857 pub(crate) fn new(input: impl Into<String>) -> Self {
858 Self {
859 input: input.into(),
860 }
861 }
862}
863
864#[derive(Clone, Debug, Error)]
867pub struct PartitionerBuilderParseError {
868 expected_format: Option<&'static str>,
869 message: Cow<'static, str>,
870}
871
872impl PartitionerBuilderParseError {
873 pub(crate) fn new(
874 expected_format: Option<&'static str>,
875 message: impl Into<Cow<'static, str>>,
876 ) -> Self {
877 Self {
878 expected_format,
879 message: message.into(),
880 }
881 }
882}
883
884impl fmt::Display for PartitionerBuilderParseError {
885 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
886 match self.expected_format {
887 Some(format) => {
888 write!(
889 f,
890 "partition must be in the format \"{}\":\n{}",
891 format, self.message
892 )
893 }
894 None => write!(f, "{}", self.message),
895 }
896 }
897}
898
899#[derive(Clone, Debug, Error)]
902pub enum TestFilterBuilderError {
903 #[error("error constructing test filters")]
905 Construct {
906 #[from]
908 error: aho_corasick::BuildError,
909 },
910}
911
912#[derive(Debug, Error)]
914pub enum PathMapperConstructError {
915 #[error("{kind} `{input}` failed to canonicalize")]
917 Canonicalization {
918 kind: PathMapperConstructKind,
920
921 input: Utf8PathBuf,
923
924 #[source]
926 err: std::io::Error,
927 },
928 #[error("{kind} `{input}` canonicalized to a non-UTF-8 path")]
930 NonUtf8Path {
931 kind: PathMapperConstructKind,
933
934 input: Utf8PathBuf,
936
937 #[source]
939 err: FromPathBufError,
940 },
941 #[error("{kind} `{canonicalized_path}` is not a directory")]
943 NotADirectory {
944 kind: PathMapperConstructKind,
946
947 input: Utf8PathBuf,
949
950 canonicalized_path: Utf8PathBuf,
952 },
953}
954
955impl PathMapperConstructError {
956 pub fn kind(&self) -> PathMapperConstructKind {
958 match self {
959 Self::Canonicalization { kind, .. }
960 | Self::NonUtf8Path { kind, .. }
961 | Self::NotADirectory { kind, .. } => *kind,
962 }
963 }
964
965 pub fn input(&self) -> &Utf8Path {
967 match self {
968 Self::Canonicalization { input, .. }
969 | Self::NonUtf8Path { input, .. }
970 | Self::NotADirectory { input, .. } => input,
971 }
972 }
973}
974
975#[derive(Copy, Clone, Debug, PartialEq, Eq)]
980pub enum PathMapperConstructKind {
981 WorkspaceRoot,
983
984 TargetDir,
986}
987
988impl fmt::Display for PathMapperConstructKind {
989 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
990 match self {
991 Self::WorkspaceRoot => write!(f, "remapped workspace root"),
992 Self::TargetDir => write!(f, "remapped target directory"),
993 }
994 }
995}
996
997#[derive(Debug, Error)]
999pub enum RustBuildMetaParseError {
1000 #[error("error deserializing platform from build metadata")]
1002 PlatformDeserializeError(#[from] target_spec::Error),
1003
1004 #[error("the host platform could not be determined")]
1006 DetectBuildTargetError(#[source] target_spec::Error),
1007
1008 #[error("unsupported features in the build metadata: {message}")]
1010 Unsupported {
1011 message: String,
1013 },
1014}
1015
1016#[derive(Clone, Debug, thiserror::Error)]
1019#[error("invalid format version: {input}")]
1020pub struct FormatVersionError {
1021 pub input: String,
1023 #[source]
1025 pub error: FormatVersionErrorInner,
1026}
1027
1028#[derive(Clone, Debug, thiserror::Error)]
1030pub enum FormatVersionErrorInner {
1031 #[error("expected format version in form of `{expected}`")]
1033 InvalidFormat {
1034 expected: &'static str,
1036 },
1037 #[error("version component `{which}` could not be parsed as an integer")]
1039 InvalidInteger {
1040 which: &'static str,
1042 #[source]
1044 err: std::num::ParseIntError,
1045 },
1046 #[error("version component `{which}` value {value} is out of range {range:?}")]
1048 InvalidValue {
1049 which: &'static str,
1051 value: u8,
1053 range: std::ops::Range<u8>,
1055 },
1056}
1057
1058#[derive(Debug, Error)]
1061#[non_exhaustive]
1062pub enum FromMessagesError {
1063 #[error("error reading Cargo JSON messages")]
1065 ReadMessages(#[source] std::io::Error),
1066
1067 #[error("error querying package graph")]
1069 PackageGraph(#[source] guppy::Error),
1070
1071 #[error("missing kind for target {binary_name} in package {package_name}")]
1073 MissingTargetKind {
1074 package_name: String,
1076 binary_name: String,
1078 },
1079}
1080
1081#[derive(Debug, Error)]
1083#[non_exhaustive]
1084pub enum CreateTestListError {
1085 #[error(
1087 "for `{binary_id}`, current directory `{cwd}` is not a directory\n\
1088 (hint: ensure project source is available at this location)"
1089 )]
1090 CwdIsNotDir {
1091 binary_id: RustBinaryId,
1093
1094 cwd: Utf8PathBuf,
1096 },
1097
1098 #[error(
1100 "for `{binary_id}`, running command `{}` failed to execute",
1101 shell_words::join(command)
1102 )]
1103 CommandExecFail {
1104 binary_id: RustBinaryId,
1106
1107 command: Vec<String>,
1109
1110 #[source]
1112 error: std::io::Error,
1113 },
1114
1115 #[error(
1117 "for `{binary_id}`, command `{}` {}\n--- stdout:\n{}\n--- stderr:\n{}\n---",
1118 shell_words::join(command),
1119 display_exited_with(*exit_status),
1120 String::from_utf8_lossy(stdout),
1121 String::from_utf8_lossy(stderr),
1122 )]
1123 CommandFail {
1124 binary_id: RustBinaryId,
1126
1127 command: Vec<String>,
1129
1130 exit_status: ExitStatus,
1132
1133 stdout: Vec<u8>,
1135
1136 stderr: Vec<u8>,
1138 },
1139
1140 #[error(
1142 "for `{binary_id}`, command `{}` produced non-UTF-8 output:\n--- stdout:\n{}\n--- stderr:\n{}\n---",
1143 shell_words::join(command),
1144 String::from_utf8_lossy(stdout),
1145 String::from_utf8_lossy(stderr)
1146 )]
1147 CommandNonUtf8 {
1148 binary_id: RustBinaryId,
1150
1151 command: Vec<String>,
1153
1154 stdout: Vec<u8>,
1156
1157 stderr: Vec<u8>,
1159 },
1160
1161 #[error("for `{binary_id}`, {message}\nfull output:\n{full_output}")]
1163 ParseLine {
1164 binary_id: RustBinaryId,
1166
1167 message: Cow<'static, str>,
1169
1170 full_output: String,
1172 },
1173
1174 #[error(
1176 "error joining dynamic library paths for {}: [{}]",
1177 dylib_path_envvar(),
1178 itertools::join(.new_paths, ", ")
1179 )]
1180 DylibJoinPaths {
1181 new_paths: Vec<Utf8PathBuf>,
1183
1184 #[source]
1186 error: JoinPathsError,
1187 },
1188
1189 #[error("error creating Tokio runtime")]
1191 TokioRuntimeCreate(#[source] std::io::Error),
1192}
1193
1194impl CreateTestListError {
1195 pub(crate) fn parse_line(
1196 binary_id: RustBinaryId,
1197 message: impl Into<Cow<'static, str>>,
1198 full_output: impl Into<String>,
1199 ) -> Self {
1200 Self::ParseLine {
1201 binary_id,
1202 message: message.into(),
1203 full_output: full_output.into(),
1204 }
1205 }
1206
1207 pub(crate) fn dylib_join_paths(new_paths: Vec<Utf8PathBuf>, error: JoinPathsError) -> Self {
1208 Self::DylibJoinPaths { new_paths, error }
1209 }
1210}
1211
1212#[derive(Debug, Error)]
1214#[non_exhaustive]
1215pub enum WriteTestListError {
1216 #[error("error writing to output")]
1218 Io(#[source] std::io::Error),
1219
1220 #[error("error serializing to JSON")]
1222 Json(#[source] serde_json::Error),
1223}
1224
1225#[derive(Debug, Error)]
1229pub enum ConfigureHandleInheritanceError {
1230 #[cfg(windows)]
1232 #[error("error configuring handle inheritance")]
1233 WindowsError(#[from] std::io::Error),
1234}
1235
1236#[derive(Debug, Error)]
1238#[non_exhaustive]
1239pub enum TestRunnerBuildError {
1240 #[error("error creating Tokio runtime")]
1242 TokioRuntimeCreate(#[source] std::io::Error),
1243
1244 #[error("error setting up signals")]
1246 SignalHandlerSetupError(#[from] SignalHandlerSetupError),
1247}
1248
1249#[derive(Debug, Error)]
1251pub struct TestRunnerExecuteErrors<E> {
1252 pub report_error: Option<E>,
1254
1255 pub join_errors: Vec<tokio::task::JoinError>,
1258}
1259
1260impl<E: std::error::Error> fmt::Display for TestRunnerExecuteErrors<E> {
1261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1262 if let Some(report_error) = &self.report_error {
1263 write!(f, "error reporting results: {report_error}")?;
1264 }
1265
1266 if !self.join_errors.is_empty() {
1267 if self.report_error.is_some() {
1268 write!(f, "; ")?;
1269 }
1270
1271 write!(f, "errors joining tasks: ")?;
1272
1273 for (i, join_error) in self.join_errors.iter().enumerate() {
1274 if i > 0 {
1275 write!(f, ", ")?;
1276 }
1277
1278 write!(f, "{join_error}")?;
1279 }
1280 }
1281
1282 Ok(())
1283 }
1284}
1285
1286#[derive(Debug, Error)]
1290#[error(
1291 "could not detect archive format from file name `{file_name}` (supported extensions: {})",
1292 supported_extensions()
1293)]
1294pub struct UnknownArchiveFormat {
1295 pub file_name: String,
1297}
1298
1299fn supported_extensions() -> String {
1300 ArchiveFormat::SUPPORTED_FORMATS
1301 .iter()
1302 .map(|(extension, _)| *extension)
1303 .join(", ")
1304}
1305
1306#[derive(Debug, Error)]
1308#[non_exhaustive]
1309pub enum ArchiveCreateError {
1310 #[error("error creating binary list")]
1312 CreateBinaryList(#[source] WriteTestListError),
1313
1314 #[error("extra path `{}` not found", .redactor.redact_path(path))]
1316 MissingExtraPath {
1317 path: Utf8PathBuf,
1319
1320 redactor: Redactor,
1325 },
1326
1327 #[error("while archiving {step}, error writing {} `{path}` to archive", kind_str(*.is_dir))]
1329 InputFileRead {
1330 step: ArchiveStep,
1332
1333 path: Utf8PathBuf,
1335
1336 is_dir: Option<bool>,
1338
1339 #[source]
1341 error: std::io::Error,
1342 },
1343
1344 #[error("error reading directory entry from `{path}")]
1346 DirEntryRead {
1347 path: Utf8PathBuf,
1349
1350 #[source]
1352 error: std::io::Error,
1353 },
1354
1355 #[error("error writing to archive")]
1357 OutputArchiveIo(#[source] std::io::Error),
1358
1359 #[error("error reporting archive status")]
1361 ReporterIo(#[source] std::io::Error),
1362}
1363
1364fn kind_str(is_dir: Option<bool>) -> &'static str {
1365 match is_dir {
1366 Some(true) => "directory",
1367 Some(false) => "file",
1368 None => "path",
1369 }
1370}
1371
1372#[derive(Debug, Error)]
1374pub enum MetadataMaterializeError {
1375 #[error("I/O error reading metadata file `{path}`")]
1377 Read {
1378 path: Utf8PathBuf,
1380
1381 #[source]
1383 error: std::io::Error,
1384 },
1385
1386 #[error("error deserializing metadata file `{path}`")]
1388 Deserialize {
1389 path: Utf8PathBuf,
1391
1392 #[source]
1394 error: serde_json::Error,
1395 },
1396
1397 #[error("error parsing Rust build metadata from `{path}`")]
1399 RustBuildMeta {
1400 path: Utf8PathBuf,
1402
1403 #[source]
1405 error: RustBuildMetaParseError,
1406 },
1407
1408 #[error("error building package graph from `{path}`")]
1410 PackageGraphConstruct {
1411 path: Utf8PathBuf,
1413
1414 #[source]
1416 error: guppy::Error,
1417 },
1418}
1419
1420#[derive(Debug, Error)]
1424#[non_exhaustive]
1425pub enum ArchiveReadError {
1426 #[error("I/O error reading archive")]
1428 Io(#[source] std::io::Error),
1429
1430 #[error("path in archive `{}` wasn't valid UTF-8", String::from_utf8_lossy(.0))]
1432 NonUtf8Path(Vec<u8>),
1433
1434 #[error("path in archive `{0}` doesn't start with `target/`")]
1436 NoTargetPrefix(Utf8PathBuf),
1437
1438 #[error("path in archive `{path}` contains an invalid component `{component}`")]
1440 InvalidComponent {
1441 path: Utf8PathBuf,
1443
1444 component: String,
1446 },
1447
1448 #[error("corrupted archive: checksum read error for path `{path}`")]
1450 ChecksumRead {
1451 path: Utf8PathBuf,
1453
1454 #[source]
1456 error: std::io::Error,
1457 },
1458
1459 #[error("corrupted archive: invalid checksum for path `{path}`")]
1461 InvalidChecksum {
1462 path: Utf8PathBuf,
1464
1465 expected: u32,
1467
1468 actual: u32,
1470 },
1471
1472 #[error("metadata file `{0}` not found in archive")]
1474 MetadataFileNotFound(&'static Utf8Path),
1475
1476 #[error("error deserializing metadata file `{path}` in archive")]
1478 MetadataDeserializeError {
1479 path: &'static Utf8Path,
1481
1482 #[source]
1484 error: serde_json::Error,
1485 },
1486
1487 #[error("error building package graph from `{path}` in archive")]
1489 PackageGraphConstructError {
1490 path: &'static Utf8Path,
1492
1493 #[source]
1495 error: guppy::Error,
1496 },
1497}
1498
1499#[derive(Debug, Error)]
1503#[non_exhaustive]
1504pub enum ArchiveExtractError {
1505 #[error("error creating temporary directory")]
1507 TempDirCreate(#[source] std::io::Error),
1508
1509 #[error("error canonicalizing destination directory `{dir}`")]
1511 DestDirCanonicalization {
1512 dir: Utf8PathBuf,
1514
1515 #[source]
1517 error: std::io::Error,
1518 },
1519
1520 #[error("destination `{0}` already exists")]
1522 DestinationExists(Utf8PathBuf),
1523
1524 #[error("error reading archive")]
1526 Read(#[source] ArchiveReadError),
1527
1528 #[error("error deserializing Rust build metadata")]
1530 RustBuildMeta(#[from] RustBuildMetaParseError),
1531
1532 #[error("error writing file `{path}` to disk")]
1534 WriteFile {
1535 path: Utf8PathBuf,
1537
1538 #[source]
1540 error: std::io::Error,
1541 },
1542
1543 #[error("error reporting extract status")]
1545 ReporterIo(std::io::Error),
1546}
1547
1548#[derive(Debug, Error)]
1550#[non_exhaustive]
1551pub enum WriteEventError {
1552 #[error("error writing to output")]
1554 Io(#[source] std::io::Error),
1555
1556 #[error("error operating on path {file}")]
1558 Fs {
1559 file: Utf8PathBuf,
1561
1562 #[source]
1564 error: std::io::Error,
1565 },
1566
1567 #[error("error writing JUnit output to {file}")]
1569 Junit {
1570 file: Utf8PathBuf,
1572
1573 #[source]
1575 error: quick_junit::SerializeError,
1576 },
1577}
1578
1579#[derive(Debug, Error)]
1582#[non_exhaustive]
1583pub enum CargoConfigError {
1584 #[error("failed to retrieve current directory")]
1586 GetCurrentDir(#[source] std::io::Error),
1587
1588 #[error("current directory is invalid UTF-8")]
1590 CurrentDirInvalidUtf8(#[source] FromPathBufError),
1591
1592 #[error("failed to parse --config argument `{config_str}` as TOML")]
1594 CliConfigParseError {
1595 config_str: String,
1597
1598 #[source]
1600 error: toml_edit::TomlError,
1601 },
1602
1603 #[error("failed to deserialize --config argument `{config_str}` as TOML")]
1605 CliConfigDeError {
1606 config_str: String,
1608
1609 #[source]
1611 error: toml_edit::de::Error,
1612 },
1613
1614 #[error(
1616 "invalid format for --config argument `{config_str}` (should be a dotted key expression)"
1617 )]
1618 InvalidCliConfig {
1619 config_str: String,
1621
1622 #[source]
1624 reason: InvalidCargoCliConfigReason,
1625 },
1626
1627 #[error("non-UTF-8 path encountered")]
1629 NonUtf8Path(#[source] FromPathBufError),
1630
1631 #[error("failed to retrieve the Cargo home directory")]
1633 GetCargoHome(#[source] std::io::Error),
1634
1635 #[error("failed to canonicalize path `{path}")]
1637 FailedPathCanonicalization {
1638 path: Utf8PathBuf,
1640
1641 #[source]
1643 error: std::io::Error,
1644 },
1645
1646 #[error("failed to read config at `{path}`")]
1648 ConfigReadError {
1649 path: Utf8PathBuf,
1651
1652 #[source]
1654 error: std::io::Error,
1655 },
1656
1657 #[error(transparent)]
1659 ConfigParseError(#[from] Box<CargoConfigParseError>),
1660}
1661
1662#[derive(Debug, Error)]
1666#[error("failed to parse config at `{path}`")]
1667pub struct CargoConfigParseError {
1668 pub path: Utf8PathBuf,
1670
1671 #[source]
1673 pub error: toml::de::Error,
1674}
1675
1676#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]
1680#[non_exhaustive]
1681pub enum InvalidCargoCliConfigReason {
1682 #[error("was not a TOML dotted key expression (such as `build.jobs = 2`)")]
1684 NotDottedKv,
1685
1686 #[error("includes non-whitespace decoration")]
1688 IncludesNonWhitespaceDecoration,
1689
1690 #[error("sets a value to an inline table, which is not accepted")]
1692 SetsValueToInlineTable,
1693
1694 #[error("sets a value to an array of tables, which is not accepted")]
1696 SetsValueToArrayOfTables,
1697
1698 #[error("doesn't provide a value")]
1700 DoesntProvideValue,
1701}
1702
1703#[derive(Debug, Error)]
1705pub enum HostPlatformDetectError {
1706 #[error(
1709 "error spawning `rustc -vV`, and detecting the build \
1710 target failed as well\n\
1711 - rustc spawn error: {}\n\
1712 - build target error: {}\n",
1713 DisplayErrorChain::new_with_initial_indent(" ", error),
1714 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1715 )]
1716 RustcVvSpawnError {
1717 error: std::io::Error,
1719
1720 build_target_error: Box<target_spec::Error>,
1722 },
1723
1724 #[error(
1727 "`rustc -vV` failed with {}, and detecting the \
1728 build target failed as well\n\
1729 - `rustc -vV` stdout:\n{}\n\
1730 - `rustc -vV` stderr:\n{}\n\
1731 - build target error:\n{}\n",
1732 status,
1733 DisplayIndented { item: String::from_utf8_lossy(stdout), indent: " " },
1734 DisplayIndented { item: String::from_utf8_lossy(stderr), indent: " " },
1735 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1736 )]
1737 RustcVvFailed {
1738 status: ExitStatus,
1740
1741 stdout: Vec<u8>,
1743
1744 stderr: Vec<u8>,
1746
1747 build_target_error: Box<target_spec::Error>,
1749 },
1750
1751 #[error(
1754 "parsing `rustc -vV` output failed, and detecting the build target \
1755 failed as well\n\
1756 - host platform error:\n{}\n\
1757 - build target error:\n{}\n",
1758 DisplayErrorChain::new_with_initial_indent(" ", host_platform_error),
1759 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1760 )]
1761 HostPlatformParseError {
1762 host_platform_error: Box<target_spec::Error>,
1764
1765 build_target_error: Box<target_spec::Error>,
1767 },
1768
1769 #[error("test-only code, so `rustc -vV` was not called; failed to detect build target")]
1772 BuildTargetError {
1773 #[source]
1775 build_target_error: Box<target_spec::Error>,
1776 },
1777}
1778
1779#[derive(Debug, Error)]
1781pub enum TargetTripleError {
1782 #[error(
1784 "environment variable '{}' contained non-UTF-8 data",
1785 TargetTriple::CARGO_BUILD_TARGET_ENV
1786 )]
1787 InvalidEnvironmentVar,
1788
1789 #[error("error deserializing target triple from {source}")]
1791 TargetSpecError {
1792 source: TargetTripleSource,
1794
1795 #[source]
1797 error: target_spec::Error,
1798 },
1799
1800 #[error("target path `{path}` is not a valid file")]
1802 TargetPathReadError {
1803 source: TargetTripleSource,
1805
1806 path: Utf8PathBuf,
1808
1809 #[source]
1811 error: std::io::Error,
1812 },
1813
1814 #[error(
1816 "for custom platform obtained from {source}, \
1817 failed to create temporary directory for custom platform"
1818 )]
1819 CustomPlatformTempDirError {
1820 source: TargetTripleSource,
1822
1823 #[source]
1825 error: std::io::Error,
1826 },
1827
1828 #[error(
1830 "for custom platform obtained from {source}, \
1831 failed to write JSON to temporary path `{path}`"
1832 )]
1833 CustomPlatformWriteError {
1834 source: TargetTripleSource,
1836
1837 path: Utf8PathBuf,
1839
1840 #[source]
1842 error: std::io::Error,
1843 },
1844
1845 #[error(
1847 "for custom platform obtained from {source}, \
1848 failed to close temporary directory `{dir_path}`"
1849 )]
1850 CustomPlatformCloseError {
1851 source: TargetTripleSource,
1853
1854 dir_path: Utf8PathBuf,
1856
1857 #[source]
1859 error: std::io::Error,
1860 },
1861}
1862
1863impl TargetTripleError {
1864 pub fn source_report(&self) -> Option<miette::Report> {
1869 match self {
1870 Self::TargetSpecError { error, .. } => {
1871 Some(miette::Report::new_boxed(error.clone().into_diagnostic()))
1872 }
1873 TargetTripleError::InvalidEnvironmentVar
1875 | TargetTripleError::TargetPathReadError { .. }
1876 | TargetTripleError::CustomPlatformTempDirError { .. }
1877 | TargetTripleError::CustomPlatformWriteError { .. }
1878 | TargetTripleError::CustomPlatformCloseError { .. } => None,
1879 }
1880 }
1881}
1882
1883#[derive(Debug, Error)]
1885pub enum TargetRunnerError {
1886 #[error("environment variable '{0}' contained non-UTF-8 data")]
1888 InvalidEnvironmentVar(String),
1889
1890 #[error("runner '{key}' = '{value}' did not contain a runner binary")]
1893 BinaryNotSpecified {
1894 key: PlatformRunnerSource,
1896
1897 value: String,
1899 },
1900}
1901
1902#[derive(Debug, Error)]
1904#[error("error setting up signal handler")]
1905pub struct SignalHandlerSetupError(#[from] std::io::Error);
1906
1907#[derive(Debug, Error)]
1909pub enum ShowTestGroupsError {
1910 #[error(
1912 "unknown test groups specified: {}\n(known groups: {})",
1913 unknown_groups.iter().join(", "),
1914 known_groups.iter().join(", "),
1915 )]
1916 UnknownGroups {
1917 unknown_groups: BTreeSet<TestGroup>,
1919
1920 known_groups: BTreeSet<TestGroup>,
1922 },
1923}
1924
1925#[derive(Debug, Error, PartialEq, Eq, Hash)]
1927pub enum InheritsError {
1928 #[error("the {} profile should not inherit from other profiles", .0)]
1930 DefaultProfileInheritance(String),
1931 #[error("profile {} inherits from an unknown profile {}", .0, .1)]
1933 UnknownInheritance(String, String),
1934 #[error("a self referential inheritance is detected from profile: {}", .0)]
1936 SelfReferentialInheritance(String),
1937 #[error("inheritance cycle detected in profile configuration from: {}", .0.iter().map(|scc| {
1939 format!("[{}]", scc.iter().join(", "))
1940 }).join(", "))]
1941 InheritanceCycle(Vec<Vec<String>>),
1942}
1943
1944#[cfg(feature = "self-update")]
1945mod self_update_errors {
1946 use super::*;
1947 use crate::update::PrereleaseKind;
1948 use mukti_metadata::ReleaseStatus;
1949 use semver::{Version, VersionReq};
1950
1951 #[derive(Debug, Error)]
1955 #[non_exhaustive]
1956 pub enum UpdateError {
1957 #[error("failed to read release metadata from `{path}`")]
1959 ReadLocalMetadata {
1960 path: Utf8PathBuf,
1962
1963 #[source]
1965 error: std::io::Error,
1966 },
1967
1968 #[error("self-update failed")]
1970 SelfUpdate(#[source] self_update::errors::Error),
1971
1972 #[error("deserializing release metadata failed")]
1974 ReleaseMetadataDe(#[source] serde_json::Error),
1975
1976 #[error("version `{version}` not found (known versions: {})", known_versions(.known))]
1978 VersionNotFound {
1979 version: Version,
1981
1982 known: Vec<(Version, ReleaseStatus)>,
1984 },
1985
1986 #[error("no version found matching requirement `{req}`")]
1988 NoMatchForVersionReq {
1989 req: VersionReq,
1991 },
1992
1993 #[error("no stable version found")]
1995 NoStableVersion,
1996
1997 #[error("no version found matching {} channel", kind.description())]
1999 NoVersionForPrereleaseKind {
2000 kind: PrereleaseKind,
2002 },
2003
2004 #[error("project {not_found} not found in release metadata (known projects: {})", known.join(", "))]
2006 MuktiProjectNotFound {
2007 not_found: String,
2009
2010 known: Vec<String>,
2012 },
2013
2014 #[error(
2016 "for version {version}, no release information found for target `{triple}` \
2017 (known targets: {})",
2018 known_triples.iter().join(", ")
2019 )]
2020 NoTargetData {
2021 version: Version,
2023
2024 triple: String,
2026
2027 known_triples: BTreeSet<String>,
2029 },
2030
2031 #[error("the current executable's path could not be determined")]
2033 CurrentExe(#[source] std::io::Error),
2034
2035 #[error("temporary directory could not be created at `{location}`")]
2037 TempDirCreate {
2038 location: Utf8PathBuf,
2040
2041 #[source]
2043 error: std::io::Error,
2044 },
2045
2046 #[error("temporary archive could not be created at `{archive_path}`")]
2048 TempArchiveCreate {
2049 archive_path: Utf8PathBuf,
2051
2052 #[source]
2054 error: std::io::Error,
2055 },
2056
2057 #[error("error writing to temporary archive at `{archive_path}`")]
2059 TempArchiveWrite {
2060 archive_path: Utf8PathBuf,
2062
2063 #[source]
2065 error: std::io::Error,
2066 },
2067
2068 #[error("error reading from temporary archive at `{archive_path}`")]
2070 TempArchiveRead {
2071 archive_path: Utf8PathBuf,
2073
2074 #[source]
2076 error: std::io::Error,
2077 },
2078
2079 #[error("SHA-256 checksum mismatch: expected: {expected}, actual: {actual}")]
2081 ChecksumMismatch {
2082 expected: String,
2084
2085 actual: String,
2087 },
2088
2089 #[error("error renaming `{source}` to `{dest}`")]
2091 FsRename {
2092 source: Utf8PathBuf,
2094
2095 dest: Utf8PathBuf,
2097
2098 #[source]
2100 error: std::io::Error,
2101 },
2102
2103 #[error("cargo-nextest binary updated, but error running `cargo nextest self setup`")]
2105 SelfSetup(#[source] std::io::Error),
2106 }
2107
2108 fn known_versions(versions: &[(Version, ReleaseStatus)]) -> String {
2109 use std::fmt::Write;
2110
2111 const DISPLAY_COUNT: usize = 4;
2113
2114 let display_versions: Vec<_> = versions
2115 .iter()
2116 .filter(|(v, status)| v.pre.is_empty() && *status == ReleaseStatus::Active)
2117 .map(|(v, _)| v.to_string())
2118 .take(DISPLAY_COUNT)
2119 .collect();
2120 let mut display_str = display_versions.join(", ");
2121 if versions.len() > display_versions.len() {
2122 write!(
2123 display_str,
2124 " and {} others",
2125 versions.len() - display_versions.len()
2126 )
2127 .unwrap();
2128 }
2129
2130 display_str
2131 }
2132
2133 #[derive(Debug, Error)]
2135 pub enum UpdateVersionParseError {
2136 #[error("version string is empty")]
2138 EmptyString,
2139
2140 #[error(
2142 "`{input}` is not a valid semver requirement\n\
2143 (hint: see https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html for the correct format)"
2144 )]
2145 InvalidVersionReq {
2146 input: String,
2148
2149 #[source]
2151 error: semver::Error,
2152 },
2153
2154 #[error("`{input}` is not a valid semver{}", extra_semver_output(.input))]
2156 InvalidVersion {
2157 input: String,
2159
2160 #[source]
2162 error: semver::Error,
2163 },
2164 }
2165
2166 fn extra_semver_output(input: &str) -> String {
2167 if input.parse::<VersionReq>().is_ok() {
2170 format!(
2171 "\n(if you want to specify a semver range, add an explicit qualifier, like ^{input})"
2172 )
2173 } else {
2174 "".to_owned()
2175 }
2176 }
2177}
2178
2179#[cfg(feature = "self-update")]
2180pub use self_update_errors::*;
2181
2182#[cfg(test)]
2183mod tests {
2184 use super::*;
2185
2186 #[test]
2187 fn display_error_chain() {
2188 let err1 = StringError::new("err1", None);
2189
2190 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err1)), @"err1");
2191
2192 let err2 = StringError::new("err2", Some(err1));
2193 let err3 = StringError::new("err3\nerr3 line 2", Some(err2));
2194
2195 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err3)), @r"
2196 err3
2197 err3 line 2
2198 caused by:
2199 - err2
2200 - err1
2201 ");
2202 }
2203
2204 #[test]
2205 fn display_error_list() {
2206 let err1 = StringError::new("err1", None);
2207
2208 let error_list =
2209 ErrorList::<StringError>::new("waiting on the water to boil", vec![err1.clone()])
2210 .expect(">= 1 error");
2211 insta::assert_snapshot!(format!("{}", error_list), @"err1");
2212 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @"err1");
2213
2214 let err2 = StringError::new("err2", Some(err1));
2215 let err3 = StringError::new("err3", Some(err2));
2216
2217 let error_list =
2218 ErrorList::<StringError>::new("waiting on flowers to bloom", vec![err3.clone()])
2219 .expect(">= 1 error");
2220 insta::assert_snapshot!(format!("{}", error_list), @"err3");
2221 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @r"
2222 err3
2223 caused by:
2224 - err2
2225 - err1
2226 ");
2227
2228 let err4 = StringError::new("err4", None);
2229 let err5 = StringError::new("err5", Some(err4));
2230 let err6 = StringError::new("err6\nerr6 line 2", Some(err5));
2231
2232 let error_list = ErrorList::<StringError>::new(
2233 "waiting for the heat death of the universe",
2234 vec![err3, err6],
2235 )
2236 .expect(">= 1 error");
2237
2238 insta::assert_snapshot!(format!("{}", error_list), @r"
2239 2 errors occurred waiting for the heat death of the universe:
2240 * err3
2241 caused by:
2242 - err2
2243 - err1
2244 * err6
2245 err6 line 2
2246 caused by:
2247 - err5
2248 - err4
2249 ");
2250 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @r"
2251 2 errors occurred waiting for the heat death of the universe:
2252 * err3
2253 caused by:
2254 - err2
2255 - err1
2256 * err6
2257 err6 line 2
2258 caused by:
2259 - err5
2260 - err4
2261 ");
2262 }
2263
2264 #[derive(Clone, Debug, Error)]
2265 struct StringError {
2266 message: String,
2267 #[source]
2268 source: Option<Box<StringError>>,
2269 }
2270
2271 impl StringError {
2272 fn new(message: impl Into<String>, source: Option<StringError>) -> Self {
2273 Self {
2274 message: message.into(),
2275 source: source.map(Box::new),
2276 }
2277 }
2278 }
2279
2280 impl fmt::Display for StringError {
2281 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2282 write!(f, "{}", self.message)
2283 }
2284 }
2285}