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(crate) fn iter(&self) -> impl Iterator<Item = &T> {
398 self.inner.iter()
399 }
400}
401
402impl<T: std::error::Error> fmt::Display for ErrorList<T> {
403 fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result {
404 if self.inner.len() == 1 {
406 return write!(f, "{}", self.inner[0]);
407 }
408
409 writeln!(
411 f,
412 "{} errors occurred {}:",
413 self.inner.len(),
414 self.description,
415 )?;
416 for error in &self.inner {
417 let mut indent = indented(f).with_str(" ").skip_initial();
418 writeln!(indent, "* {}", DisplayErrorChain::new(error))?;
419 f = indent.into_inner();
420 }
421 Ok(())
422 }
423}
424
425impl<T: std::error::Error> std::error::Error for ErrorList<T> {
426 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
427 if self.inner.len() == 1 {
428 self.inner[0].source()
429 } else {
430 None
433 }
434 }
435}
436
437pub(crate) struct DisplayErrorChain<E> {
442 error: E,
443 initial_indent: &'static str,
444}
445
446impl<E: std::error::Error> DisplayErrorChain<E> {
447 pub(crate) fn new(error: E) -> Self {
448 Self {
449 error,
450 initial_indent: "",
451 }
452 }
453
454 pub(crate) fn new_with_initial_indent(initial_indent: &'static str, error: E) -> Self {
455 Self {
456 error,
457 initial_indent,
458 }
459 }
460}
461
462impl<E> fmt::Display for DisplayErrorChain<E>
463where
464 E: std::error::Error,
465{
466 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
467 let mut writer = indented(f).with_str(self.initial_indent);
468 write!(writer, "{}", self.error)?;
469
470 let Some(mut cause) = self.error.source() else {
471 return Ok(());
472 };
473
474 write!(writer, "\n caused by:")?;
475
476 loop {
477 writeln!(writer)?;
478 let mut indent = indented(&mut writer).with_str(" ").skip_initial();
480 write!(indent, " - {cause}")?;
481
482 let Some(next_cause) = cause.source() else {
483 break Ok(());
484 };
485
486 cause = next_cause;
487 }
488 }
489}
490
491#[derive(Clone, Debug, Error)]
493pub enum ChildError {
494 #[error(transparent)]
496 Fd(#[from] ChildFdError),
497
498 #[error(transparent)]
500 SetupScriptOutput(#[from] SetupScriptOutputError),
501}
502
503#[derive(Clone, Debug, Error)]
505pub enum ChildFdError {
506 #[error("error reading standard output")]
508 ReadStdout(#[source] Arc<std::io::Error>),
509
510 #[error("error reading standard error")]
512 ReadStderr(#[source] Arc<std::io::Error>),
513
514 #[error("error reading combined stream")]
516 ReadCombined(#[source] Arc<std::io::Error>),
517
518 #[error("error waiting for child process to exit")]
520 Wait(#[source] Arc<std::io::Error>),
521}
522
523#[derive(Clone, Debug, Eq, PartialEq)]
525#[non_exhaustive]
526pub struct UnknownTestGroupError {
527 pub profile_name: String,
529
530 pub name: TestGroup,
532}
533
534#[derive(Clone, Debug, Eq, PartialEq)]
537pub struct ProfileUnknownScriptError {
538 pub profile_name: String,
540
541 pub name: ScriptId,
543}
544
545#[derive(Clone, Debug, Eq, PartialEq)]
548pub struct ProfileWrongConfigScriptTypeError {
549 pub profile_name: String,
551
552 pub name: ScriptId,
554
555 pub attempted: ProfileScriptType,
557
558 pub actual: ScriptType,
560}
561
562#[derive(Clone, Debug, Eq, PartialEq)]
565pub struct ProfileListScriptUsesRunFiltersError {
566 pub profile_name: String,
568
569 pub name: ScriptId,
571
572 pub script_type: ProfileScriptType,
574
575 pub filters: BTreeSet<String>,
577}
578
579#[derive(Clone, Debug, Default)]
581pub struct ProfileScriptErrors {
582 pub unknown_scripts: Vec<ProfileUnknownScriptError>,
584
585 pub wrong_script_types: Vec<ProfileWrongConfigScriptTypeError>,
587
588 pub list_scripts_using_run_filters: Vec<ProfileListScriptUsesRunFiltersError>,
590}
591
592impl ProfileScriptErrors {
593 pub fn is_empty(&self) -> bool {
595 self.unknown_scripts.is_empty()
596 && self.wrong_script_types.is_empty()
597 && self.list_scripts_using_run_filters.is_empty()
598 }
599}
600
601#[derive(Clone, Debug, Error)]
603#[error("profile `{profile}` not found (known profiles: {})", .all_profiles.join(", "))]
604pub struct ProfileNotFound {
605 profile: String,
606 all_profiles: Vec<String>,
607}
608
609impl ProfileNotFound {
610 pub(crate) fn new(
611 profile: impl Into<String>,
612 all_profiles: impl IntoIterator<Item = impl Into<String>>,
613 ) -> Self {
614 let mut all_profiles: Vec<_> = all_profiles.into_iter().map(|s| s.into()).collect();
615 all_profiles.sort_unstable();
616 Self {
617 profile: profile.into(),
618 all_profiles,
619 }
620 }
621}
622
623#[derive(Clone, Debug, Error, Eq, PartialEq)]
625pub enum InvalidIdentifier {
626 #[error("identifier is empty")]
628 Empty,
629
630 #[error("invalid identifier `{0}`")]
632 InvalidXid(SmolStr),
633
634 #[error("tool identifier not of the form \"@tool:tool-name:identifier\": `{0}`")]
636 ToolIdentifierInvalidFormat(SmolStr),
637
638 #[error("tool identifier has empty component: `{0}`")]
640 ToolComponentEmpty(SmolStr),
641
642 #[error("invalid tool identifier `{0}`")]
644 ToolIdentifierInvalidXid(SmolStr),
645}
646
647#[derive(Clone, Debug, Error, Eq, PartialEq)]
649pub enum InvalidToolName {
650 #[error("tool name is empty")]
652 Empty,
653
654 #[error("invalid tool name `{0}`")]
656 InvalidXid(SmolStr),
657
658 #[error("tool name cannot start with \"@tool\": `{0}`")]
660 StartsWithToolPrefix(SmolStr),
661}
662
663#[derive(Clone, Debug, Error)]
665#[error("invalid custom test group name: {0}")]
666pub struct InvalidCustomTestGroupName(pub InvalidIdentifier);
667
668#[derive(Clone, Debug, Error)]
670#[error("invalid configuration script name: {0}")]
671pub struct InvalidConfigScriptName(pub InvalidIdentifier);
672
673#[derive(Clone, Debug, Error, PartialEq, Eq)]
675pub enum ToolConfigFileParseError {
676 #[error(
677 "tool-config-file has invalid format: {input}\n(hint: tool configs must be in the format <tool-name>:<path>)"
678 )]
679 InvalidFormat {
681 input: String,
683 },
684
685 #[error("tool-config-file has invalid tool name: {input}")]
687 InvalidToolName {
688 input: String,
690
691 #[source]
693 error: InvalidToolName,
694 },
695
696 #[error("tool-config-file has empty config file path: {input}")]
698 EmptyConfigFile {
699 input: String,
701 },
702
703 #[error("tool-config-file is not an absolute path: {config_file}")]
705 ConfigFileNotAbsolute {
706 config_file: Utf8PathBuf,
708 },
709}
710
711#[derive(Debug, Error)]
713#[non_exhaustive]
714pub enum UserConfigError {
715 #[error("failed to read user config at {path}")]
717 Read {
718 path: Utf8PathBuf,
720 #[source]
722 error: std::io::Error,
723 },
724
725 #[error("failed to parse user config at {path}")]
727 Parse {
728 path: Utf8PathBuf,
730 #[source]
732 error: toml::de::Error,
733 },
734
735 #[error("user config path contains non-UTF-8 characters")]
737 NonUtf8Path {
738 #[source]
740 error: FromPathBufError,
741 },
742
743 #[error(
745 "for user config at {path}, failed to compile platform spec in [[overrides]] at index {index}"
746 )]
747 OverridePlatformSpec {
748 path: Utf8PathBuf,
750 index: usize,
752 #[source]
754 error: target_spec::Error,
755 },
756}
757
758#[derive(Clone, Debug, Error)]
760#[error("unrecognized value for max-fail: {reason}")]
761pub struct MaxFailParseError {
762 pub reason: String,
764}
765
766impl MaxFailParseError {
767 pub(crate) fn new(reason: impl Into<String>) -> Self {
768 Self {
769 reason: reason.into(),
770 }
771 }
772}
773
774#[derive(Clone, Debug, Error)]
776#[error(
777 "unrecognized value for stress-count: {input}\n\
778 (hint: expected either a positive integer or \"infinite\")"
779)]
780pub struct StressCountParseError {
781 pub input: String,
783}
784
785impl StressCountParseError {
786 pub(crate) fn new(input: impl Into<String>) -> Self {
787 Self {
788 input: input.into(),
789 }
790 }
791}
792
793#[derive(Clone, Debug, Error)]
795#[non_exhaustive]
796pub enum DebuggerCommandParseError {
797 #[error(transparent)]
799 ShellWordsParse(shell_words::ParseError),
800
801 #[error("debugger command cannot be empty")]
803 EmptyCommand,
804}
805
806#[derive(Clone, Debug, Error)]
808#[non_exhaustive]
809pub enum TracerCommandParseError {
810 #[error(transparent)]
812 ShellWordsParse(shell_words::ParseError),
813
814 #[error("tracer command cannot be empty")]
816 EmptyCommand,
817}
818
819#[derive(Clone, Debug, Error)]
821#[error(
822 "unrecognized value for test-threads: {input}\n(hint: expected either an integer or \"num-cpus\")"
823)]
824pub struct TestThreadsParseError {
825 pub input: String,
827}
828
829impl TestThreadsParseError {
830 pub(crate) fn new(input: impl Into<String>) -> Self {
831 Self {
832 input: input.into(),
833 }
834 }
835}
836
837#[derive(Clone, Debug, Error)]
840pub struct PartitionerBuilderParseError {
841 expected_format: Option<&'static str>,
842 message: Cow<'static, str>,
843}
844
845impl PartitionerBuilderParseError {
846 pub(crate) fn new(
847 expected_format: Option<&'static str>,
848 message: impl Into<Cow<'static, str>>,
849 ) -> Self {
850 Self {
851 expected_format,
852 message: message.into(),
853 }
854 }
855}
856
857impl fmt::Display for PartitionerBuilderParseError {
858 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
859 match self.expected_format {
860 Some(format) => {
861 write!(
862 f,
863 "partition must be in the format \"{}\":\n{}",
864 format, self.message
865 )
866 }
867 None => write!(f, "{}", self.message),
868 }
869 }
870}
871
872#[derive(Clone, Debug, Error)]
875pub enum TestFilterBuilderError {
876 #[error("error constructing test filters")]
878 Construct {
879 #[from]
881 error: aho_corasick::BuildError,
882 },
883}
884
885#[derive(Debug, Error)]
887pub enum PathMapperConstructError {
888 #[error("{kind} `{input}` failed to canonicalize")]
890 Canonicalization {
891 kind: PathMapperConstructKind,
893
894 input: Utf8PathBuf,
896
897 #[source]
899 err: std::io::Error,
900 },
901 #[error("{kind} `{input}` canonicalized to a non-UTF-8 path")]
903 NonUtf8Path {
904 kind: PathMapperConstructKind,
906
907 input: Utf8PathBuf,
909
910 #[source]
912 err: FromPathBufError,
913 },
914 #[error("{kind} `{canonicalized_path}` is not a directory")]
916 NotADirectory {
917 kind: PathMapperConstructKind,
919
920 input: Utf8PathBuf,
922
923 canonicalized_path: Utf8PathBuf,
925 },
926}
927
928impl PathMapperConstructError {
929 pub fn kind(&self) -> PathMapperConstructKind {
931 match self {
932 Self::Canonicalization { kind, .. }
933 | Self::NonUtf8Path { kind, .. }
934 | Self::NotADirectory { kind, .. } => *kind,
935 }
936 }
937
938 pub fn input(&self) -> &Utf8Path {
940 match self {
941 Self::Canonicalization { input, .. }
942 | Self::NonUtf8Path { input, .. }
943 | Self::NotADirectory { input, .. } => input,
944 }
945 }
946}
947
948#[derive(Copy, Clone, Debug, PartialEq, Eq)]
953pub enum PathMapperConstructKind {
954 WorkspaceRoot,
956
957 TargetDir,
959}
960
961impl fmt::Display for PathMapperConstructKind {
962 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
963 match self {
964 Self::WorkspaceRoot => write!(f, "remapped workspace root"),
965 Self::TargetDir => write!(f, "remapped target directory"),
966 }
967 }
968}
969
970#[derive(Debug, Error)]
972pub enum RustBuildMetaParseError {
973 #[error("error deserializing platform from build metadata")]
975 PlatformDeserializeError(#[from] target_spec::Error),
976
977 #[error("the host platform could not be determined")]
979 DetectBuildTargetError(#[source] target_spec::Error),
980
981 #[error("unsupported features in the build metadata: {message}")]
983 Unsupported {
984 message: String,
986 },
987}
988
989#[derive(Clone, Debug, thiserror::Error)]
992#[error("invalid format version: {input}")]
993pub struct FormatVersionError {
994 pub input: String,
996 #[source]
998 pub error: FormatVersionErrorInner,
999}
1000
1001#[derive(Clone, Debug, thiserror::Error)]
1003pub enum FormatVersionErrorInner {
1004 #[error("expected format version in form of `{expected}`")]
1006 InvalidFormat {
1007 expected: &'static str,
1009 },
1010 #[error("version component `{which}` could not be parsed as an integer")]
1012 InvalidInteger {
1013 which: &'static str,
1015 #[source]
1017 err: std::num::ParseIntError,
1018 },
1019 #[error("version component `{which}` value {value} is out of range {range:?}")]
1021 InvalidValue {
1022 which: &'static str,
1024 value: u8,
1026 range: std::ops::Range<u8>,
1028 },
1029}
1030
1031#[derive(Debug, Error)]
1034#[non_exhaustive]
1035pub enum FromMessagesError {
1036 #[error("error reading Cargo JSON messages")]
1038 ReadMessages(#[source] std::io::Error),
1039
1040 #[error("error querying package graph")]
1042 PackageGraph(#[source] guppy::Error),
1043
1044 #[error("missing kind for target {binary_name} in package {package_name}")]
1046 MissingTargetKind {
1047 package_name: String,
1049 binary_name: String,
1051 },
1052}
1053
1054#[derive(Debug, Error)]
1056#[non_exhaustive]
1057pub enum CreateTestListError {
1058 #[error(
1060 "for `{binary_id}`, current directory `{cwd}` is not a directory\n\
1061 (hint: ensure project source is available at this location)"
1062 )]
1063 CwdIsNotDir {
1064 binary_id: RustBinaryId,
1066
1067 cwd: Utf8PathBuf,
1069 },
1070
1071 #[error(
1073 "for `{binary_id}`, running command `{}` failed to execute",
1074 shell_words::join(command)
1075 )]
1076 CommandExecFail {
1077 binary_id: RustBinaryId,
1079
1080 command: Vec<String>,
1082
1083 #[source]
1085 error: std::io::Error,
1086 },
1087
1088 #[error(
1090 "for `{binary_id}`, command `{}` {}\n--- stdout:\n{}\n--- stderr:\n{}\n---",
1091 shell_words::join(command),
1092 display_exited_with(*exit_status),
1093 String::from_utf8_lossy(stdout),
1094 String::from_utf8_lossy(stderr),
1095 )]
1096 CommandFail {
1097 binary_id: RustBinaryId,
1099
1100 command: Vec<String>,
1102
1103 exit_status: ExitStatus,
1105
1106 stdout: Vec<u8>,
1108
1109 stderr: Vec<u8>,
1111 },
1112
1113 #[error(
1115 "for `{binary_id}`, command `{}` produced non-UTF-8 output:\n--- stdout:\n{}\n--- stderr:\n{}\n---",
1116 shell_words::join(command),
1117 String::from_utf8_lossy(stdout),
1118 String::from_utf8_lossy(stderr)
1119 )]
1120 CommandNonUtf8 {
1121 binary_id: RustBinaryId,
1123
1124 command: Vec<String>,
1126
1127 stdout: Vec<u8>,
1129
1130 stderr: Vec<u8>,
1132 },
1133
1134 #[error("for `{binary_id}`, {message}\nfull output:\n{full_output}")]
1136 ParseLine {
1137 binary_id: RustBinaryId,
1139
1140 message: Cow<'static, str>,
1142
1143 full_output: String,
1145 },
1146
1147 #[error(
1149 "error joining dynamic library paths for {}: [{}]",
1150 dylib_path_envvar(),
1151 itertools::join(.new_paths, ", ")
1152 )]
1153 DylibJoinPaths {
1154 new_paths: Vec<Utf8PathBuf>,
1156
1157 #[source]
1159 error: JoinPathsError,
1160 },
1161
1162 #[error("error creating Tokio runtime")]
1164 TokioRuntimeCreate(#[source] std::io::Error),
1165}
1166
1167impl CreateTestListError {
1168 pub(crate) fn parse_line(
1169 binary_id: RustBinaryId,
1170 message: impl Into<Cow<'static, str>>,
1171 full_output: impl Into<String>,
1172 ) -> Self {
1173 Self::ParseLine {
1174 binary_id,
1175 message: message.into(),
1176 full_output: full_output.into(),
1177 }
1178 }
1179
1180 pub(crate) fn dylib_join_paths(new_paths: Vec<Utf8PathBuf>, error: JoinPathsError) -> Self {
1181 Self::DylibJoinPaths { new_paths, error }
1182 }
1183}
1184
1185#[derive(Debug, Error)]
1187#[non_exhaustive]
1188pub enum WriteTestListError {
1189 #[error("error writing to output")]
1191 Io(#[source] std::io::Error),
1192
1193 #[error("error serializing to JSON")]
1195 Json(#[source] serde_json::Error),
1196}
1197
1198#[derive(Debug, Error)]
1202pub enum ConfigureHandleInheritanceError {
1203 #[cfg(windows)]
1205 #[error("error configuring handle inheritance")]
1206 WindowsError(#[from] std::io::Error),
1207}
1208
1209#[derive(Debug, Error)]
1211#[non_exhaustive]
1212pub enum TestRunnerBuildError {
1213 #[error("error creating Tokio runtime")]
1215 TokioRuntimeCreate(#[source] std::io::Error),
1216
1217 #[error("error setting up signals")]
1219 SignalHandlerSetupError(#[from] SignalHandlerSetupError),
1220}
1221
1222#[derive(Debug, Error)]
1224pub struct TestRunnerExecuteErrors<E> {
1225 pub report_error: Option<E>,
1227
1228 pub join_errors: Vec<tokio::task::JoinError>,
1231}
1232
1233impl<E: std::error::Error> fmt::Display for TestRunnerExecuteErrors<E> {
1234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1235 if let Some(report_error) = &self.report_error {
1236 write!(f, "error reporting results: {report_error}")?;
1237 }
1238
1239 if !self.join_errors.is_empty() {
1240 if self.report_error.is_some() {
1241 write!(f, "; ")?;
1242 }
1243
1244 write!(f, "errors joining tasks: ")?;
1245
1246 for (i, join_error) in self.join_errors.iter().enumerate() {
1247 if i > 0 {
1248 write!(f, ", ")?;
1249 }
1250
1251 write!(f, "{join_error}")?;
1252 }
1253 }
1254
1255 Ok(())
1256 }
1257}
1258
1259#[derive(Debug, Error)]
1263#[error(
1264 "could not detect archive format from file name `{file_name}` (supported extensions: {})",
1265 supported_extensions()
1266)]
1267pub struct UnknownArchiveFormat {
1268 pub file_name: String,
1270}
1271
1272fn supported_extensions() -> String {
1273 ArchiveFormat::SUPPORTED_FORMATS
1274 .iter()
1275 .map(|(extension, _)| *extension)
1276 .join(", ")
1277}
1278
1279#[derive(Debug, Error)]
1281#[non_exhaustive]
1282pub enum ArchiveCreateError {
1283 #[error("error creating binary list")]
1285 CreateBinaryList(#[source] WriteTestListError),
1286
1287 #[error("extra path `{}` not found", .redactor.redact_path(path))]
1289 MissingExtraPath {
1290 path: Utf8PathBuf,
1292
1293 redactor: Redactor,
1298 },
1299
1300 #[error("while archiving {step}, error writing {} `{path}` to archive", kind_str(*.is_dir))]
1302 InputFileRead {
1303 step: ArchiveStep,
1305
1306 path: Utf8PathBuf,
1308
1309 is_dir: Option<bool>,
1311
1312 #[source]
1314 error: std::io::Error,
1315 },
1316
1317 #[error("error reading directory entry from `{path}")]
1319 DirEntryRead {
1320 path: Utf8PathBuf,
1322
1323 #[source]
1325 error: std::io::Error,
1326 },
1327
1328 #[error("error writing to archive")]
1330 OutputArchiveIo(#[source] std::io::Error),
1331
1332 #[error("error reporting archive status")]
1334 ReporterIo(#[source] std::io::Error),
1335}
1336
1337fn kind_str(is_dir: Option<bool>) -> &'static str {
1338 match is_dir {
1339 Some(true) => "directory",
1340 Some(false) => "file",
1341 None => "path",
1342 }
1343}
1344
1345#[derive(Debug, Error)]
1347pub enum MetadataMaterializeError {
1348 #[error("I/O error reading metadata file `{path}`")]
1350 Read {
1351 path: Utf8PathBuf,
1353
1354 #[source]
1356 error: std::io::Error,
1357 },
1358
1359 #[error("error deserializing metadata file `{path}`")]
1361 Deserialize {
1362 path: Utf8PathBuf,
1364
1365 #[source]
1367 error: serde_json::Error,
1368 },
1369
1370 #[error("error parsing Rust build metadata from `{path}`")]
1372 RustBuildMeta {
1373 path: Utf8PathBuf,
1375
1376 #[source]
1378 error: RustBuildMetaParseError,
1379 },
1380
1381 #[error("error building package graph from `{path}`")]
1383 PackageGraphConstruct {
1384 path: Utf8PathBuf,
1386
1387 #[source]
1389 error: guppy::Error,
1390 },
1391}
1392
1393#[derive(Debug, Error)]
1397#[non_exhaustive]
1398pub enum ArchiveReadError {
1399 #[error("I/O error reading archive")]
1401 Io(#[source] std::io::Error),
1402
1403 #[error("path in archive `{}` wasn't valid UTF-8", String::from_utf8_lossy(.0))]
1405 NonUtf8Path(Vec<u8>),
1406
1407 #[error("path in archive `{0}` doesn't start with `target/`")]
1409 NoTargetPrefix(Utf8PathBuf),
1410
1411 #[error("path in archive `{path}` contains an invalid component `{component}`")]
1413 InvalidComponent {
1414 path: Utf8PathBuf,
1416
1417 component: String,
1419 },
1420
1421 #[error("corrupted archive: checksum read error for path `{path}`")]
1423 ChecksumRead {
1424 path: Utf8PathBuf,
1426
1427 #[source]
1429 error: std::io::Error,
1430 },
1431
1432 #[error("corrupted archive: invalid checksum for path `{path}`")]
1434 InvalidChecksum {
1435 path: Utf8PathBuf,
1437
1438 expected: u32,
1440
1441 actual: u32,
1443 },
1444
1445 #[error("metadata file `{0}` not found in archive")]
1447 MetadataFileNotFound(&'static Utf8Path),
1448
1449 #[error("error deserializing metadata file `{path}` in archive")]
1451 MetadataDeserializeError {
1452 path: &'static Utf8Path,
1454
1455 #[source]
1457 error: serde_json::Error,
1458 },
1459
1460 #[error("error building package graph from `{path}` in archive")]
1462 PackageGraphConstructError {
1463 path: &'static Utf8Path,
1465
1466 #[source]
1468 error: guppy::Error,
1469 },
1470}
1471
1472#[derive(Debug, Error)]
1476#[non_exhaustive]
1477pub enum ArchiveExtractError {
1478 #[error("error creating temporary directory")]
1480 TempDirCreate(#[source] std::io::Error),
1481
1482 #[error("error canonicalizing destination directory `{dir}`")]
1484 DestDirCanonicalization {
1485 dir: Utf8PathBuf,
1487
1488 #[source]
1490 error: std::io::Error,
1491 },
1492
1493 #[error("destination `{0}` already exists")]
1495 DestinationExists(Utf8PathBuf),
1496
1497 #[error("error reading archive")]
1499 Read(#[source] ArchiveReadError),
1500
1501 #[error("error deserializing Rust build metadata")]
1503 RustBuildMeta(#[from] RustBuildMetaParseError),
1504
1505 #[error("error writing file `{path}` to disk")]
1507 WriteFile {
1508 path: Utf8PathBuf,
1510
1511 #[source]
1513 error: std::io::Error,
1514 },
1515
1516 #[error("error reporting extract status")]
1518 ReporterIo(std::io::Error),
1519}
1520
1521#[derive(Debug, Error)]
1523#[non_exhaustive]
1524pub enum WriteEventError {
1525 #[error("error writing to output")]
1527 Io(#[source] std::io::Error),
1528
1529 #[error("error operating on path {file}")]
1531 Fs {
1532 file: Utf8PathBuf,
1534
1535 #[source]
1537 error: std::io::Error,
1538 },
1539
1540 #[error("error writing JUnit output to {file}")]
1542 Junit {
1543 file: Utf8PathBuf,
1545
1546 #[source]
1548 error: quick_junit::SerializeError,
1549 },
1550}
1551
1552#[derive(Debug, Error)]
1555#[non_exhaustive]
1556pub enum CargoConfigError {
1557 #[error("failed to retrieve current directory")]
1559 GetCurrentDir(#[source] std::io::Error),
1560
1561 #[error("current directory is invalid UTF-8")]
1563 CurrentDirInvalidUtf8(#[source] FromPathBufError),
1564
1565 #[error("failed to parse --config argument `{config_str}` as TOML")]
1567 CliConfigParseError {
1568 config_str: String,
1570
1571 #[source]
1573 error: toml_edit::TomlError,
1574 },
1575
1576 #[error("failed to deserialize --config argument `{config_str}` as TOML")]
1578 CliConfigDeError {
1579 config_str: String,
1581
1582 #[source]
1584 error: toml_edit::de::Error,
1585 },
1586
1587 #[error(
1589 "invalid format for --config argument `{config_str}` (should be a dotted key expression)"
1590 )]
1591 InvalidCliConfig {
1592 config_str: String,
1594
1595 #[source]
1597 reason: InvalidCargoCliConfigReason,
1598 },
1599
1600 #[error("non-UTF-8 path encountered")]
1602 NonUtf8Path(#[source] FromPathBufError),
1603
1604 #[error("failed to retrieve the Cargo home directory")]
1606 GetCargoHome(#[source] std::io::Error),
1607
1608 #[error("failed to canonicalize path `{path}")]
1610 FailedPathCanonicalization {
1611 path: Utf8PathBuf,
1613
1614 #[source]
1616 error: std::io::Error,
1617 },
1618
1619 #[error("failed to read config at `{path}`")]
1621 ConfigReadError {
1622 path: Utf8PathBuf,
1624
1625 #[source]
1627 error: std::io::Error,
1628 },
1629
1630 #[error(transparent)]
1632 ConfigParseError(#[from] Box<CargoConfigParseError>),
1633}
1634
1635#[derive(Debug, Error)]
1639#[error("failed to parse config at `{path}`")]
1640pub struct CargoConfigParseError {
1641 pub path: Utf8PathBuf,
1643
1644 #[source]
1646 pub error: toml::de::Error,
1647}
1648
1649#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]
1653#[non_exhaustive]
1654pub enum InvalidCargoCliConfigReason {
1655 #[error("was not a TOML dotted key expression (such as `build.jobs = 2`)")]
1657 NotDottedKv,
1658
1659 #[error("includes non-whitespace decoration")]
1661 IncludesNonWhitespaceDecoration,
1662
1663 #[error("sets a value to an inline table, which is not accepted")]
1665 SetsValueToInlineTable,
1666
1667 #[error("sets a value to an array of tables, which is not accepted")]
1669 SetsValueToArrayOfTables,
1670
1671 #[error("doesn't provide a value")]
1673 DoesntProvideValue,
1674}
1675
1676#[derive(Debug, Error)]
1678pub enum HostPlatformDetectError {
1679 #[error(
1682 "error spawning `rustc -vV`, and detecting the build \
1683 target failed as well\n\
1684 - rustc spawn error: {}\n\
1685 - build target error: {}\n",
1686 DisplayErrorChain::new_with_initial_indent(" ", error),
1687 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1688 )]
1689 RustcVvSpawnError {
1690 error: std::io::Error,
1692
1693 build_target_error: Box<target_spec::Error>,
1695 },
1696
1697 #[error(
1700 "`rustc -vV` failed with {}, and detecting the \
1701 build target failed as well\n\
1702 - `rustc -vV` stdout:\n{}\n\
1703 - `rustc -vV` stderr:\n{}\n\
1704 - build target error:\n{}\n",
1705 status,
1706 DisplayIndented { item: String::from_utf8_lossy(stdout), indent: " " },
1707 DisplayIndented { item: String::from_utf8_lossy(stderr), indent: " " },
1708 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1709 )]
1710 RustcVvFailed {
1711 status: ExitStatus,
1713
1714 stdout: Vec<u8>,
1716
1717 stderr: Vec<u8>,
1719
1720 build_target_error: Box<target_spec::Error>,
1722 },
1723
1724 #[error(
1727 "parsing `rustc -vV` output failed, and detecting the build target \
1728 failed as well\n\
1729 - host platform error:\n{}\n\
1730 - build target error:\n{}\n",
1731 DisplayErrorChain::new_with_initial_indent(" ", host_platform_error),
1732 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1733 )]
1734 HostPlatformParseError {
1735 host_platform_error: Box<target_spec::Error>,
1737
1738 build_target_error: Box<target_spec::Error>,
1740 },
1741
1742 #[error("test-only code, so `rustc -vV` was not called; failed to detect build target")]
1745 BuildTargetError {
1746 #[source]
1748 build_target_error: Box<target_spec::Error>,
1749 },
1750}
1751
1752#[derive(Debug, Error)]
1754pub enum TargetTripleError {
1755 #[error(
1757 "environment variable '{}' contained non-UTF-8 data",
1758 TargetTriple::CARGO_BUILD_TARGET_ENV
1759 )]
1760 InvalidEnvironmentVar,
1761
1762 #[error("error deserializing target triple from {source}")]
1764 TargetSpecError {
1765 source: TargetTripleSource,
1767
1768 #[source]
1770 error: target_spec::Error,
1771 },
1772
1773 #[error("target path `{path}` is not a valid file")]
1775 TargetPathReadError {
1776 source: TargetTripleSource,
1778
1779 path: Utf8PathBuf,
1781
1782 #[source]
1784 error: std::io::Error,
1785 },
1786
1787 #[error(
1789 "for custom platform obtained from {source}, \
1790 failed to create temporary directory for custom platform"
1791 )]
1792 CustomPlatformTempDirError {
1793 source: TargetTripleSource,
1795
1796 #[source]
1798 error: std::io::Error,
1799 },
1800
1801 #[error(
1803 "for custom platform obtained from {source}, \
1804 failed to write JSON to temporary path `{path}`"
1805 )]
1806 CustomPlatformWriteError {
1807 source: TargetTripleSource,
1809
1810 path: Utf8PathBuf,
1812
1813 #[source]
1815 error: std::io::Error,
1816 },
1817
1818 #[error(
1820 "for custom platform obtained from {source}, \
1821 failed to close temporary directory `{dir_path}`"
1822 )]
1823 CustomPlatformCloseError {
1824 source: TargetTripleSource,
1826
1827 dir_path: Utf8PathBuf,
1829
1830 #[source]
1832 error: std::io::Error,
1833 },
1834}
1835
1836impl TargetTripleError {
1837 pub fn source_report(&self) -> Option<miette::Report> {
1842 match self {
1843 Self::TargetSpecError { error, .. } => {
1844 Some(miette::Report::new_boxed(error.clone().into_diagnostic()))
1845 }
1846 TargetTripleError::InvalidEnvironmentVar
1848 | TargetTripleError::TargetPathReadError { .. }
1849 | TargetTripleError::CustomPlatformTempDirError { .. }
1850 | TargetTripleError::CustomPlatformWriteError { .. }
1851 | TargetTripleError::CustomPlatformCloseError { .. } => None,
1852 }
1853 }
1854}
1855
1856#[derive(Debug, Error)]
1858pub enum TargetRunnerError {
1859 #[error("environment variable '{0}' contained non-UTF-8 data")]
1861 InvalidEnvironmentVar(String),
1862
1863 #[error("runner '{key}' = '{value}' did not contain a runner binary")]
1866 BinaryNotSpecified {
1867 key: PlatformRunnerSource,
1869
1870 value: String,
1872 },
1873}
1874
1875#[derive(Debug, Error)]
1877#[error("error setting up signal handler")]
1878pub struct SignalHandlerSetupError(#[from] std::io::Error);
1879
1880#[derive(Debug, Error)]
1882pub enum ShowTestGroupsError {
1883 #[error(
1885 "unknown test groups specified: {}\n(known groups: {})",
1886 unknown_groups.iter().join(", "),
1887 known_groups.iter().join(", "),
1888 )]
1889 UnknownGroups {
1890 unknown_groups: BTreeSet<TestGroup>,
1892
1893 known_groups: BTreeSet<TestGroup>,
1895 },
1896}
1897
1898#[derive(Debug, Error, PartialEq, Eq, Hash)]
1900pub enum InheritsError {
1901 #[error("the {} profile should not inherit from other profiles", .0)]
1903 DefaultProfileInheritance(String),
1904 #[error("profile {} inherits from an unknown profile {}", .0, .1)]
1906 UnknownInheritance(String, String),
1907 #[error("a self referential inheritance is detected from profile: {}", .0)]
1909 SelfReferentialInheritance(String),
1910 #[error("inheritance cycle detected in profile configuration from: {}", .0.iter().map(|scc| {
1912 format!("[{}]", scc.iter().join(", "))
1913 }).join(", "))]
1914 InheritanceCycle(Vec<Vec<String>>),
1915}
1916
1917#[cfg(feature = "self-update")]
1918mod self_update_errors {
1919 use super::*;
1920 use crate::update::PrereleaseKind;
1921 use mukti_metadata::ReleaseStatus;
1922 use semver::{Version, VersionReq};
1923
1924 #[derive(Debug, Error)]
1928 #[non_exhaustive]
1929 pub enum UpdateError {
1930 #[error("failed to read release metadata from `{path}`")]
1932 ReadLocalMetadata {
1933 path: Utf8PathBuf,
1935
1936 #[source]
1938 error: std::io::Error,
1939 },
1940
1941 #[error("self-update failed")]
1943 SelfUpdate(#[source] self_update::errors::Error),
1944
1945 #[error("deserializing release metadata failed")]
1947 ReleaseMetadataDe(#[source] serde_json::Error),
1948
1949 #[error("version `{version}` not found (known versions: {})", known_versions(.known))]
1951 VersionNotFound {
1952 version: Version,
1954
1955 known: Vec<(Version, ReleaseStatus)>,
1957 },
1958
1959 #[error("no version found matching requirement `{req}`")]
1961 NoMatchForVersionReq {
1962 req: VersionReq,
1964 },
1965
1966 #[error("no stable version found")]
1968 NoStableVersion,
1969
1970 #[error("no version found matching {} channel", kind.description())]
1972 NoVersionForPrereleaseKind {
1973 kind: PrereleaseKind,
1975 },
1976
1977 #[error("project {not_found} not found in release metadata (known projects: {})", known.join(", "))]
1979 MuktiProjectNotFound {
1980 not_found: String,
1982
1983 known: Vec<String>,
1985 },
1986
1987 #[error(
1989 "for version {version}, no release information found for target `{triple}` \
1990 (known targets: {})",
1991 known_triples.iter().join(", ")
1992 )]
1993 NoTargetData {
1994 version: Version,
1996
1997 triple: String,
1999
2000 known_triples: BTreeSet<String>,
2002 },
2003
2004 #[error("the current executable's path could not be determined")]
2006 CurrentExe(#[source] std::io::Error),
2007
2008 #[error("temporary directory could not be created at `{location}`")]
2010 TempDirCreate {
2011 location: Utf8PathBuf,
2013
2014 #[source]
2016 error: std::io::Error,
2017 },
2018
2019 #[error("temporary archive could not be created at `{archive_path}`")]
2021 TempArchiveCreate {
2022 archive_path: Utf8PathBuf,
2024
2025 #[source]
2027 error: std::io::Error,
2028 },
2029
2030 #[error("error writing to temporary archive at `{archive_path}`")]
2032 TempArchiveWrite {
2033 archive_path: Utf8PathBuf,
2035
2036 #[source]
2038 error: std::io::Error,
2039 },
2040
2041 #[error("error reading from temporary archive at `{archive_path}`")]
2043 TempArchiveRead {
2044 archive_path: Utf8PathBuf,
2046
2047 #[source]
2049 error: std::io::Error,
2050 },
2051
2052 #[error("SHA-256 checksum mismatch: expected: {expected}, actual: {actual}")]
2054 ChecksumMismatch {
2055 expected: String,
2057
2058 actual: String,
2060 },
2061
2062 #[error("error renaming `{source}` to `{dest}`")]
2064 FsRename {
2065 source: Utf8PathBuf,
2067
2068 dest: Utf8PathBuf,
2070
2071 #[source]
2073 error: std::io::Error,
2074 },
2075
2076 #[error("cargo-nextest binary updated, but error running `cargo nextest self setup`")]
2078 SelfSetup(#[source] std::io::Error),
2079 }
2080
2081 fn known_versions(versions: &[(Version, ReleaseStatus)]) -> String {
2082 use std::fmt::Write;
2083
2084 const DISPLAY_COUNT: usize = 4;
2086
2087 let display_versions: Vec<_> = versions
2088 .iter()
2089 .filter(|(v, status)| v.pre.is_empty() && *status == ReleaseStatus::Active)
2090 .map(|(v, _)| v.to_string())
2091 .take(DISPLAY_COUNT)
2092 .collect();
2093 let mut display_str = display_versions.join(", ");
2094 if versions.len() > display_versions.len() {
2095 write!(
2096 display_str,
2097 " and {} others",
2098 versions.len() - display_versions.len()
2099 )
2100 .unwrap();
2101 }
2102
2103 display_str
2104 }
2105
2106 #[derive(Debug, Error)]
2108 pub enum UpdateVersionParseError {
2109 #[error("version string is empty")]
2111 EmptyString,
2112
2113 #[error(
2115 "`{input}` is not a valid semver requirement\n\
2116 (hint: see https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html for the correct format)"
2117 )]
2118 InvalidVersionReq {
2119 input: String,
2121
2122 #[source]
2124 error: semver::Error,
2125 },
2126
2127 #[error("`{input}` is not a valid semver{}", extra_semver_output(.input))]
2129 InvalidVersion {
2130 input: String,
2132
2133 #[source]
2135 error: semver::Error,
2136 },
2137 }
2138
2139 fn extra_semver_output(input: &str) -> String {
2140 if input.parse::<VersionReq>().is_ok() {
2143 format!(
2144 "\n(if you want to specify a semver range, add an explicit qualifier, like ^{input})"
2145 )
2146 } else {
2147 "".to_owned()
2148 }
2149 }
2150}
2151
2152#[cfg(feature = "self-update")]
2153pub use self_update_errors::*;
2154
2155#[cfg(test)]
2156mod tests {
2157 use super::*;
2158
2159 #[test]
2160 fn display_error_chain() {
2161 let err1 = StringError::new("err1", None);
2162
2163 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err1)), @"err1");
2164
2165 let err2 = StringError::new("err2", Some(err1));
2166 let err3 = StringError::new("err3\nerr3 line 2", Some(err2));
2167
2168 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err3)), @r"
2169 err3
2170 err3 line 2
2171 caused by:
2172 - err2
2173 - err1
2174 ");
2175 }
2176
2177 #[test]
2178 fn display_error_list() {
2179 let err1 = StringError::new("err1", None);
2180
2181 let error_list =
2182 ErrorList::<StringError>::new("waiting on the water to boil", vec![err1.clone()])
2183 .expect(">= 1 error");
2184 insta::assert_snapshot!(format!("{}", error_list), @"err1");
2185 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @"err1");
2186
2187 let err2 = StringError::new("err2", Some(err1));
2188 let err3 = StringError::new("err3", Some(err2));
2189
2190 let error_list =
2191 ErrorList::<StringError>::new("waiting on flowers to bloom", vec![err3.clone()])
2192 .expect(">= 1 error");
2193 insta::assert_snapshot!(format!("{}", error_list), @"err3");
2194 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @r"
2195 err3
2196 caused by:
2197 - err2
2198 - err1
2199 ");
2200
2201 let err4 = StringError::new("err4", None);
2202 let err5 = StringError::new("err5", Some(err4));
2203 let err6 = StringError::new("err6\nerr6 line 2", Some(err5));
2204
2205 let error_list = ErrorList::<StringError>::new(
2206 "waiting for the heat death of the universe",
2207 vec![err3, err6],
2208 )
2209 .expect(">= 1 error");
2210
2211 insta::assert_snapshot!(format!("{}", error_list), @r"
2212 2 errors occurred waiting for the heat death of the universe:
2213 * err3
2214 caused by:
2215 - err2
2216 - err1
2217 * err6
2218 err6 line 2
2219 caused by:
2220 - err5
2221 - err4
2222 ");
2223 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @r"
2224 2 errors occurred waiting for the heat death of the universe:
2225 * err3
2226 caused by:
2227 - err2
2228 - err1
2229 * err6
2230 err6 line 2
2231 caused by:
2232 - err5
2233 - err4
2234 ");
2235 }
2236
2237 #[derive(Clone, Debug, Error)]
2238 struct StringError {
2239 message: String,
2240 #[source]
2241 source: Option<Box<StringError>>,
2242 }
2243
2244 impl StringError {
2245 fn new(message: impl Into<String>, source: Option<StringError>) -> Self {
2246 Self {
2247 message: message.into(),
2248 source: source.map(Box::new),
2249 }
2250 }
2251 }
2252
2253 impl fmt::Display for StringError {
2254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2255 write!(f, "{}", self.message)
2256 }
2257 }
2258}