nextest_runner/config/
test_threads.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use super::get_num_cpus;
5use crate::errors::TestThreadsParseError;
6use serde::Deserialize;
7use std::{cmp::Ordering, fmt, str::FromStr};
8
9/// Type for the test-threads config key.
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum TestThreads {
12    /// Run tests with a specified number of threads.
13    Count(usize),
14
15    /// Run tests with a number of threads equal to the logical CPU count.
16    NumCpus,
17}
18
19impl TestThreads {
20    /// Gets the actual number of test threads computed at runtime.
21    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            // Note that TOML uses i64, not u64.
88            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}