nextest_runner/config/
test_threads.rs1use super::get_num_cpus;
5use crate::errors::TestThreadsParseError;
6use serde::Deserialize;
7use std::{cmp::Ordering, fmt, str::FromStr};
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum TestThreads {
12 Count(usize),
14
15 NumCpus,
17}
18
19impl TestThreads {
20 pub fn compute(self) -> usize {
22 match self {
23 Self::Count(threads) => threads,
24 Self::NumCpus => get_num_cpus(),
25 }
26 }
27}
28
29impl FromStr for TestThreads {
30 type Err = TestThreadsParseError;
31
32 fn from_str(s: &str) -> Result<Self, Self::Err> {
33 if s == "num-cpus" {
34 return Ok(Self::NumCpus);
35 }
36
37 match s.parse::<isize>() {
38 Err(e) => Err(TestThreadsParseError::new(format!(
39 "Error: {e} parsing {s}"
40 ))),
41 Ok(0) => Err(TestThreadsParseError::new("jobs may not be 0")),
42 Ok(j) if j < 0 => Ok(TestThreads::Count(
43 (get_num_cpus() as isize + j).max(1) as usize
44 )),
45 Ok(j) => Ok(TestThreads::Count(j as usize)),
46 }
47 }
48}
49
50impl fmt::Display for TestThreads {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 Self::Count(threads) => write!(f, "{threads}"),
54 Self::NumCpus => write!(f, "num-cpus"),
55 }
56 }
57}
58
59impl<'de> Deserialize<'de> for TestThreads {
60 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
61 where
62 D: serde::Deserializer<'de>,
63 {
64 struct V;
65
66 impl serde::de::Visitor<'_> for V {
67 type Value = TestThreads;
68
69 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
70 write!(formatter, "an integer or the string \"num-cpus\"")
71 }
72
73 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
74 where
75 E: serde::de::Error,
76 {
77 if v == "num-cpus" {
78 Ok(TestThreads::NumCpus)
79 } else {
80 Err(serde::de::Error::invalid_value(
81 serde::de::Unexpected::Str(v),
82 &self,
83 ))
84 }
85 }
86
87 fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
89 where
90 E: serde::de::Error,
91 {
92 match v.cmp(&0) {
93 Ordering::Greater => Ok(TestThreads::Count(v as usize)),
94 Ordering::Less => Ok(TestThreads::Count(
95 (get_num_cpus() as i64 + v).max(1) as usize
96 )),
97 Ordering::Equal => Err(serde::de::Error::invalid_value(
98 serde::de::Unexpected::Signed(v),
99 &self,
100 )),
101 }
102 }
103 }
104
105 deserializer.deserialize_any(V)
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::config::{NextestConfig, test_helpers::*};
113 use camino_tempfile::tempdir;
114 use indoc::indoc;
115 use nextest_filtering::ParseContext;
116 use test_case::test_case;
117
118 #[test_case(
119 indoc! {r#"
120 [profile.custom]
121 test-threads = -1
122 "#},
123 Some(get_num_cpus() - 1)
124
125 ; "negative"
126 )]
127 #[test_case(
128 indoc! {r#"
129 [profile.custom]
130 test-threads = 2
131 "#},
132 Some(2)
133
134 ; "positive"
135 )]
136 #[test_case(
137 indoc! {r#"
138 [profile.custom]
139 test-threads = 0
140 "#},
141 None
142
143 ; "zero"
144 )]
145 #[test_case(
146 indoc! {r#"
147 [profile.custom]
148 test-threads = "num-cpus"
149 "#},
150 Some(get_num_cpus())
151
152 ; "num-cpus"
153 )]
154 fn parse_test_threads(config_contents: &str, n_threads: Option<usize>) {
155 let workspace_dir = tempdir().unwrap();
156
157 let graph = temp_workspace(&workspace_dir, config_contents);
158
159 let pcx = ParseContext::new(&graph);
160 let config = NextestConfig::from_sources(
161 graph.workspace().root(),
162 &pcx,
163 None,
164 [],
165 &Default::default(),
166 );
167 match n_threads {
168 None => assert!(config.is_err()),
169 Some(n) => assert_eq!(
170 config
171 .unwrap()
172 .profile("custom")
173 .unwrap()
174 .apply_build_platforms(&build_platforms())
175 .custom_profile()
176 .unwrap()
177 .test_threads()
178 .unwrap()
179 .compute(),
180 n,
181 ),
182 }
183 }
184}