1use crate::CommandError;
5use camino::{Utf8Path, Utf8PathBuf};
6use serde::{Deserialize, Serialize};
7use smol_str::SmolStr;
8use std::{
9 borrow::Cow,
10 cmp::Ordering,
11 collections::{BTreeMap, BTreeSet},
12 fmt::{self, Write as _},
13 path::PathBuf,
14 process::Command,
15};
16use target_spec::summaries::PlatformSummary;
17
18#[derive(Clone, Debug, Default)]
20pub struct ListCommand {
21 cargo_path: Option<Box<Utf8Path>>,
22 manifest_path: Option<Box<Utf8Path>>,
23 current_dir: Option<Box<Utf8Path>>,
24 args: Vec<Box<str>>,
25}
26
27impl ListCommand {
28 pub fn new() -> Self {
32 Self::default()
33 }
34
35 pub fn cargo_path(&mut self, path: impl Into<Utf8PathBuf>) -> &mut Self {
38 self.cargo_path = Some(path.into().into());
39 self
40 }
41
42 pub fn manifest_path(&mut self, path: impl Into<Utf8PathBuf>) -> &mut Self {
44 self.manifest_path = Some(path.into().into());
45 self
46 }
47
48 pub fn current_dir(&mut self, path: impl Into<Utf8PathBuf>) -> &mut Self {
50 self.current_dir = Some(path.into().into());
51 self
52 }
53
54 pub fn add_arg(&mut self, arg: impl Into<String>) -> &mut Self {
56 self.args.push(arg.into().into());
57 self
58 }
59
60 pub fn add_args(&mut self, args: impl IntoIterator<Item = impl Into<String>>) -> &mut Self {
62 for arg in args {
63 self.add_arg(arg.into());
64 }
65 self
66 }
67
68 pub fn cargo_command(&self) -> Command {
70 let cargo_path: PathBuf = self.cargo_path.as_ref().map_or_else(
71 || std::env::var_os("CARGO").map_or("cargo".into(), PathBuf::from),
72 |path| PathBuf::from(path.as_std_path()),
73 );
74
75 let mut command = Command::new(cargo_path);
76 if let Some(path) = &self.manifest_path.as_deref() {
77 command.args(["--manifest-path", path.as_str()]);
78 }
79 if let Some(current_dir) = &self.current_dir.as_deref() {
80 command.current_dir(current_dir);
81 }
82
83 command.args(["nextest", "list", "--message-format=json"]);
84
85 command.args(self.args.iter().map(|s| s.as_ref()));
86 command
87 }
88
89 pub fn exec(&self) -> Result<TestListSummary, CommandError> {
91 let mut command = self.cargo_command();
92 let output = command.output().map_err(CommandError::Exec)?;
93
94 if !output.status.success() {
95 let exit_code = output.status.code();
97 let stderr = output.stderr;
98 return Err(CommandError::CommandFailed { exit_code, stderr });
99 }
100
101 serde_json::from_slice(&output.stdout).map_err(CommandError::Json)
103 }
104
105 pub fn exec_binaries_only(&self) -> Result<BinaryListSummary, CommandError> {
108 let mut command = self.cargo_command();
109 command.arg("--list-type=binaries-only");
110 let output = command.output().map_err(CommandError::Exec)?;
111
112 if !output.status.success() {
113 let exit_code = output.status.code();
115 let stderr = output.stderr;
116 return Err(CommandError::CommandFailed { exit_code, stderr });
117 }
118
119 serde_json::from_slice(&output.stdout).map_err(CommandError::Json)
121 }
122}
123
124#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
126#[serde(rename_all = "kebab-case")]
127#[non_exhaustive]
128pub struct TestListSummary {
129 pub rust_build_meta: RustBuildMetaSummary,
131
132 pub test_count: usize,
134
135 pub rust_suites: BTreeMap<RustBinaryId, RustTestSuiteSummary>,
138}
139
140impl TestListSummary {
141 pub fn new(rust_build_meta: RustBuildMetaSummary) -> Self {
143 Self {
144 rust_build_meta,
145 test_count: 0,
146 rust_suites: BTreeMap::new(),
147 }
148 }
149 pub fn parse_json(json: impl AsRef<str>) -> Result<Self, serde_json::Error> {
151 serde_json::from_str(json.as_ref())
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
157#[serde(rename_all = "kebab-case")]
158pub enum BuildPlatform {
159 Target,
161
162 Host,
164}
165
166impl fmt::Display for BuildPlatform {
167 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
168 match self {
169 Self::Target => write!(f, "target"),
170 Self::Host => write!(f, "host"),
171 }
172 }
173}
174
175#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
179#[serde(rename_all = "kebab-case")]
180pub struct RustTestBinarySummary {
181 pub binary_id: RustBinaryId,
183
184 pub binary_name: String,
186
187 pub package_id: String,
191
192 pub kind: RustTestBinaryKind,
194
195 pub binary_path: Utf8PathBuf,
197
198 pub build_platform: BuildPlatform,
201}
202
203#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
208#[serde(transparent)]
209pub struct RustTestBinaryKind(pub Cow<'static, str>);
210
211impl RustTestBinaryKind {
212 #[inline]
214 pub fn new(kind: impl Into<Cow<'static, str>>) -> Self {
215 Self(kind.into())
216 }
217
218 #[inline]
220 pub const fn new_const(kind: &'static str) -> Self {
221 Self(Cow::Borrowed(kind))
222 }
223
224 pub fn as_str(&self) -> &str {
226 &self.0
227 }
228
229 pub const LIB: Self = Self::new_const("lib");
231
232 pub const TEST: Self = Self::new_const("test");
234
235 pub const BENCH: Self = Self::new_const("bench");
237
238 pub const BIN: Self = Self::new_const("bin");
240
241 pub const EXAMPLE: Self = Self::new_const("example");
243
244 pub const PROC_MACRO: Self = Self::new_const("proc-macro");
246}
247
248impl fmt::Display for RustTestBinaryKind {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 write!(f, "{}", self.0)
251 }
252}
253
254#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
256#[serde(rename_all = "kebab-case")]
257pub struct BinaryListSummary {
258 pub rust_build_meta: RustBuildMetaSummary,
260
261 pub rust_binaries: BTreeMap<RustBinaryId, RustTestBinarySummary>,
263}
264
265#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
269#[serde(transparent)]
270pub struct RustBinaryId(SmolStr);
271
272impl fmt::Display for RustBinaryId {
273 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 f.write_str(&self.0)
275 }
276}
277
278impl RustBinaryId {
279 #[inline]
281 pub fn new(id: &str) -> Self {
282 Self(id.into())
283 }
284
285 pub fn from_parts(package_name: &str, kind: &RustTestBinaryKind, target_name: &str) -> Self {
329 let mut id = package_name.to_owned();
330 if kind == &RustTestBinaryKind::LIB || kind == &RustTestBinaryKind::PROC_MACRO {
332 } else if kind == &RustTestBinaryKind::TEST {
334 id.push_str("::");
337 id.push_str(target_name);
338 } else {
339 write!(id, "::{kind}/{target_name}").unwrap();
343 }
344
345 Self(id.into())
346 }
347
348 #[inline]
350 pub fn as_str(&self) -> &str {
351 &self.0
352 }
353
354 #[inline]
356 pub fn len(&self) -> usize {
357 self.0.len()
358 }
359
360 #[inline]
362 pub fn is_empty(&self) -> bool {
363 self.0.is_empty()
364 }
365
366 #[inline]
368 pub fn components(&self) -> RustBinaryIdComponents<'_> {
369 RustBinaryIdComponents::new(self)
370 }
371}
372
373impl<S> From<S> for RustBinaryId
374where
375 S: AsRef<str>,
376{
377 #[inline]
378 fn from(s: S) -> Self {
379 Self(s.as_ref().into())
380 }
381}
382
383impl Ord for RustBinaryId {
384 fn cmp(&self, other: &RustBinaryId) -> Ordering {
385 self.components().cmp(&other.components())
390 }
391}
392
393impl PartialOrd for RustBinaryId {
394 fn partial_cmp(&self, other: &RustBinaryId) -> Option<Ordering> {
395 Some(self.cmp(other))
396 }
397}
398
399#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
405pub struct RustBinaryIdComponents<'a> {
406 pub package_name: &'a str,
408
409 pub binary_name_and_kind: RustBinaryIdNameAndKind<'a>,
411}
412
413impl<'a> RustBinaryIdComponents<'a> {
414 fn new(id: &'a RustBinaryId) -> Self {
415 let mut parts = id.as_str().splitn(2, "::");
416
417 let package_name = parts
418 .next()
419 .expect("splitn(2) returns at least 1 component");
420 let binary_name_and_kind = if let Some(suffix) = parts.next() {
421 let mut parts = suffix.splitn(2, '/');
422
423 let part1 = parts
424 .next()
425 .expect("splitn(2) returns at least 1 component");
426 if let Some(binary_name) = parts.next() {
427 RustBinaryIdNameAndKind::NameAndKind {
428 kind: part1,
429 binary_name,
430 }
431 } else {
432 RustBinaryIdNameAndKind::NameOnly { binary_name: part1 }
433 }
434 } else {
435 RustBinaryIdNameAndKind::None
436 };
437
438 Self {
439 package_name,
440 binary_name_and_kind,
441 }
442 }
443}
444
445#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
449pub enum RustBinaryIdNameAndKind<'a> {
450 None,
452
453 NameOnly {
455 binary_name: &'a str,
457 },
458
459 NameAndKind {
461 kind: &'a str,
463
464 binary_name: &'a str,
466 },
467}
468
469#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
471#[serde(rename_all = "kebab-case")]
472pub struct RustBuildMetaSummary {
473 pub target_directory: Utf8PathBuf,
475
476 pub base_output_directories: BTreeSet<Utf8PathBuf>,
478
479 pub non_test_binaries: BTreeMap<String, BTreeSet<RustNonTestBinarySummary>>,
481
482 #[serde(default)]
487 pub build_script_out_dirs: BTreeMap<String, Utf8PathBuf>,
488
489 pub linked_paths: BTreeSet<Utf8PathBuf>,
491
492 #[serde(default)]
496 pub platforms: Option<BuildPlatformsSummary>,
497
498 #[serde(default)]
502 pub target_platforms: Vec<PlatformSummary>,
503
504 #[serde(default)]
509 pub target_platform: Option<String>,
510}
511
512#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
515#[serde(rename_all = "kebab-case")]
516pub struct RustNonTestBinarySummary {
517 pub name: String,
519
520 pub kind: RustNonTestBinaryKind,
522
523 pub path: Utf8PathBuf,
525}
526
527#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
529#[serde(rename_all = "kebab-case")]
530pub struct BuildPlatformsSummary {
531 pub host: HostPlatformSummary,
533
534 pub targets: Vec<TargetPlatformSummary>,
538}
539
540#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
542#[serde(rename_all = "kebab-case")]
543pub struct HostPlatformSummary {
544 pub platform: PlatformSummary,
546
547 pub libdir: PlatformLibdirSummary,
549}
550
551#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
553#[serde(rename_all = "kebab-case")]
554pub struct TargetPlatformSummary {
555 pub platform: PlatformSummary,
557
558 pub libdir: PlatformLibdirSummary,
562}
563
564#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
566#[serde(tag = "status", rename_all = "kebab-case")]
567pub enum PlatformLibdirSummary {
568 Available {
570 path: Utf8PathBuf,
572 },
573
574 Unavailable {
576 reason: PlatformLibdirUnavailable,
578 },
579}
580
581#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
587pub struct PlatformLibdirUnavailable(pub Cow<'static, str>);
588
589impl PlatformLibdirUnavailable {
590 pub const RUSTC_FAILED: Self = Self::new_const("rustc-failed");
592
593 pub const RUSTC_OUTPUT_ERROR: Self = Self::new_const("rustc-output-error");
596
597 pub const OLD_SUMMARY: Self = Self::new_const("old-summary");
600
601 pub const NOT_IN_ARCHIVE: Self = Self::new_const("not-in-archive");
604
605 pub const fn new_const(reason: &'static str) -> Self {
607 Self(Cow::Borrowed(reason))
608 }
609
610 pub fn new(reason: impl Into<Cow<'static, str>>) -> Self {
612 Self(reason.into())
613 }
614
615 pub fn as_str(&self) -> &str {
617 &self.0
618 }
619}
620
621#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
626#[serde(transparent)]
627pub struct RustNonTestBinaryKind(pub Cow<'static, str>);
628
629impl RustNonTestBinaryKind {
630 #[inline]
632 pub fn new(kind: impl Into<Cow<'static, str>>) -> Self {
633 Self(kind.into())
634 }
635
636 #[inline]
638 pub const fn new_const(kind: &'static str) -> Self {
639 Self(Cow::Borrowed(kind))
640 }
641
642 pub fn as_str(&self) -> &str {
644 &self.0
645 }
646
647 pub const DYLIB: Self = Self::new_const("dylib");
650
651 pub const BIN_EXE: Self = Self::new_const("bin-exe");
653}
654
655impl fmt::Display for RustNonTestBinaryKind {
656 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657 write!(f, "{}", self.0)
658 }
659}
660
661#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
665#[serde(rename_all = "kebab-case")]
666pub struct RustTestSuiteSummary {
667 pub package_name: String,
669
670 #[serde(flatten)]
672 pub binary: RustTestBinarySummary,
673
674 pub cwd: Utf8PathBuf,
676
677 #[serde(default = "listed_status")]
682 pub status: RustTestSuiteStatusSummary,
683
684 #[serde(rename = "testcases")]
686 pub test_cases: BTreeMap<String, RustTestCaseSummary>,
687}
688
689fn listed_status() -> RustTestSuiteStatusSummary {
690 RustTestSuiteStatusSummary::LISTED
691}
692
693#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
697#[serde(transparent)]
698pub struct RustTestSuiteStatusSummary(pub Cow<'static, str>);
699
700impl RustTestSuiteStatusSummary {
701 #[inline]
703 pub fn new(kind: impl Into<Cow<'static, str>>) -> Self {
704 Self(kind.into())
705 }
706
707 #[inline]
709 pub const fn new_const(kind: &'static str) -> Self {
710 Self(Cow::Borrowed(kind))
711 }
712
713 pub fn as_str(&self) -> &str {
715 &self.0
716 }
717
718 pub const LISTED: Self = Self::new_const("listed");
721
722 pub const SKIPPED: Self = Self::new_const("skipped");
727
728 pub const SKIPPED_DEFAULT_FILTER: Self = Self::new_const("skipped-default-filter");
732}
733
734#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
738#[serde(rename_all = "kebab-case")]
739pub struct RustTestCaseSummary {
740 pub ignored: bool,
744
745 pub filter_match: FilterMatch,
749}
750
751#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
753#[serde(rename_all = "kebab-case", tag = "status")]
754pub enum FilterMatch {
755 Matches,
757
758 Mismatch {
760 reason: MismatchReason,
762 },
763}
764
765impl FilterMatch {
766 pub fn is_match(&self) -> bool {
768 matches!(self, FilterMatch::Matches)
769 }
770}
771
772#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
774#[serde(rename_all = "kebab-case")]
775#[non_exhaustive]
776pub enum MismatchReason {
777 Ignored,
779
780 String,
782
783 Expression,
785
786 Partition,
788
789 DefaultFilter,
793}
794
795impl fmt::Display for MismatchReason {
796 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
797 match self {
798 MismatchReason::Ignored => write!(f, "does not match the run-ignored option"),
799 MismatchReason::String => write!(f, "does not match the provided string filters"),
800 MismatchReason::Expression => {
801 write!(f, "does not match the provided expression filters")
802 }
803 MismatchReason::Partition => write!(f, "is in a different partition"),
804 MismatchReason::DefaultFilter => {
805 write!(f, "is filtered out by the profile's default-filter")
806 }
807 }
808 }
809}
810
811#[cfg(test)]
812mod tests {
813 use super::*;
814 use test_case::test_case;
815
816 #[test_case(r#"{
817 "target-directory": "/foo",
818 "base-output-directories": [],
819 "non-test-binaries": {},
820 "linked-paths": []
821 }"#, RustBuildMetaSummary {
822 target_directory: "/foo".into(),
823 base_output_directories: BTreeSet::new(),
824 non_test_binaries: BTreeMap::new(),
825 build_script_out_dirs: BTreeMap::new(),
826 linked_paths: BTreeSet::new(),
827 target_platform: None,
828 target_platforms: vec![],
829 platforms: None,
830 }; "no target platform")]
831 #[test_case(r#"{
832 "target-directory": "/foo",
833 "base-output-directories": [],
834 "non-test-binaries": {},
835 "linked-paths": [],
836 "target-platform": "x86_64-unknown-linux-gnu"
837 }"#, RustBuildMetaSummary {
838 target_directory: "/foo".into(),
839 base_output_directories: BTreeSet::new(),
840 non_test_binaries: BTreeMap::new(),
841 build_script_out_dirs: BTreeMap::new(),
842 linked_paths: BTreeSet::new(),
843 target_platform: Some("x86_64-unknown-linux-gnu".to_owned()),
844 target_platforms: vec![],
845 platforms: None,
846 }; "single target platform specified")]
847 fn test_deserialize_old_rust_build_meta(input: &str, expected: RustBuildMetaSummary) {
848 let build_meta: RustBuildMetaSummary =
849 serde_json::from_str(input).expect("input deserialized correctly");
850 assert_eq!(
851 build_meta, expected,
852 "deserialized input matched expected output"
853 );
854 }
855
856 #[test]
857 fn test_binary_id_ord() {
858 let empty = RustBinaryId::new("");
859 let foo = RustBinaryId::new("foo");
860 let bar = RustBinaryId::new("bar");
861 let foo_name1 = RustBinaryId::new("foo::name1");
862 let foo_name2 = RustBinaryId::new("foo::name2");
863 let bar_name = RustBinaryId::new("bar::name");
864 let foo_bin_name1 = RustBinaryId::new("foo::bin/name1");
865 let foo_bin_name2 = RustBinaryId::new("foo::bin/name2");
866 let bar_bin_name = RustBinaryId::new("bar::bin/name");
867 let foo_proc_macro_name = RustBinaryId::new("foo::proc_macro/name");
868 let bar_proc_macro_name = RustBinaryId::new("bar::proc_macro/name");
869
870 let sorted_ids = [
872 empty,
873 bar,
874 bar_name,
875 bar_bin_name,
876 bar_proc_macro_name,
877 foo,
878 foo_name1,
879 foo_name2,
880 foo_bin_name1,
881 foo_bin_name2,
882 foo_proc_macro_name,
883 ];
884
885 for (i, id) in sorted_ids.iter().enumerate() {
886 for (j, other_id) in sorted_ids.iter().enumerate() {
887 let expected = i.cmp(&j);
888 assert_eq!(
889 id.cmp(other_id),
890 expected,
891 "comparing {id:?} to {other_id:?} gave {expected:?}"
892 );
893 }
894 }
895 }
896}