Skip to main content

nextest_runner/config/elements/
threads_required.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::config::core::get_num_cpus;
5use serde::Deserialize;
6use std::{cmp::Ordering, fmt};
7
8/// Type for the threads-required config key.
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum ThreadsRequired {
11    /// Take up "slots" equal to the number of threads.
12    Count(usize),
13
14    /// Take up as many slots as the number of CPUs.
15    NumCpus,
16
17    /// Take up as many slots as the number of test threads specified.
18    NumTestThreads,
19}
20
21impl ThreadsRequired {
22    /// Gets the actual number of test threads computed at runtime.
23    pub fn compute(self, test_threads: usize) -> usize {
24        match self {
25            Self::Count(threads) => threads,
26            Self::NumCpus => get_num_cpus(),
27            Self::NumTestThreads => test_threads,
28        }
29    }
30}
31
32impl<'de> Deserialize<'de> for ThreadsRequired {
33    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
34    where
35        D: serde::Deserializer<'de>,
36    {
37        struct V;
38
39        impl serde::de::Visitor<'_> for V {
40            type Value = ThreadsRequired;
41
42            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
43                write!(
44                    formatter,
45                    "an integer, the string \"num-cpus\" or the string \"num-test-threads\""
46                )
47            }
48
49            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
50            where
51                E: serde::de::Error,
52            {
53                if v == "num-cpus" {
54                    Ok(ThreadsRequired::NumCpus)
55                } else if v == "num-test-threads" {
56                    Ok(ThreadsRequired::NumTestThreads)
57                } else {
58                    Err(serde::de::Error::invalid_value(
59                        serde::de::Unexpected::Str(v),
60                        &self,
61                    ))
62                }
63            }
64
65            // Note that TOML uses i64, not u64.
66            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
67            where
68                E: serde::de::Error,
69            {
70                match v.cmp(&0) {
71                    Ordering::Greater => Ok(ThreadsRequired::Count(v as usize)),
72                    // TODO: we don't currently support negative numbers here because it's not clear
73                    // whether num-cpus or num-test-threads is better. It would probably be better
74                    // to support a small expression syntax with +, -, * and /.
75                    //
76                    // I (Rain) checked out a number of the expression syntax crates and found that they
77                    // either support too much or too little. We want just this minimal set of operators,
78                    // plus. Probably worth just forking https://docs.rs/mexe or working with upstream
79                    // to add support for operators.
80                    Ordering::Equal | Ordering::Less => Err(serde::de::Error::invalid_value(
81                        serde::de::Unexpected::Signed(v),
82                        &self,
83                    )),
84                }
85            }
86        }
87
88        deserializer.deserialize_any(V)
89    }
90}
91
92#[cfg(feature = "config-schema")]
93impl schemars::JsonSchema for ThreadsRequired {
94    fn schema_name() -> std::borrow::Cow<'static, str> {
95        "ThreadsRequired".into()
96    }
97
98    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
99        schemars::json_schema!({
100            "oneOf": [
101                { "type": "integer", "minimum": 1 },
102                { "type": "string", "enum": ["num-cpus", "num-test-threads"] }
103            ]
104        })
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::config::{core::NextestConfig, utils::test_helpers::*};
112    use camino_tempfile::tempdir;
113    use indoc::indoc;
114    use nextest_filtering::ParseContext;
115    use test_case::test_case;
116
117    #[test_case(
118        indoc! {r#"
119            [profile.custom]
120            threads-required = 2
121        "#},
122        Some(2)
123
124        ; "positive"
125    )]
126    #[test_case(
127        indoc! {r#"
128            [profile.custom]
129            threads-required = 0
130        "#},
131        None
132
133        ; "zero"
134    )]
135    #[test_case(
136        indoc! {r#"
137            [profile.custom]
138            threads-required = -1
139        "#},
140        None
141
142        ; "negative"
143    )]
144    #[test_case(
145        indoc! {r#"
146            [profile.custom]
147            threads-required = "num-cpus"
148        "#},
149        Some(get_num_cpus())
150
151        ; "num-cpus"
152    )]
153    #[test_case(
154        indoc! {r#"
155            [profile.custom]
156            test-threads = 1
157            threads-required = "num-cpus"
158        "#},
159        Some(get_num_cpus())
160
161        ; "num-cpus-with-custom-test-threads"
162    )]
163    #[test_case(
164        indoc! {r#"
165            [profile.custom]
166            threads-required = "num-test-threads"
167        "#},
168        Some(get_num_cpus())
169
170        ; "num-test-threads"
171    )]
172    #[test_case(
173        indoc! {r#"
174            [profile.custom]
175            test-threads = 1
176            threads-required = "num-test-threads"
177        "#},
178        Some(1)
179
180        ; "num-test-threads-with-custom-test-threads"
181    )]
182    fn parse_threads_required(config_contents: &str, threads_required: Option<usize>) {
183        let workspace_dir = tempdir().unwrap();
184
185        let graph = temp_workspace(&workspace_dir, config_contents);
186
187        let pcx = ParseContext::new(&graph);
188        let config = NextestConfig::from_sources(
189            graph.workspace().root(),
190            &pcx,
191            None,
192            [],
193            &Default::default(),
194        );
195        match threads_required {
196            None => assert!(config.is_err()),
197            Some(t) => {
198                let config = config.unwrap();
199                let profile = config
200                    .profile("custom")
201                    .unwrap()
202                    .apply_build_platforms(&build_platforms());
203
204                let test_threads = profile.test_threads().compute();
205                let threads_required = profile.threads_required().compute(test_threads);
206                assert_eq!(threads_required, t)
207            }
208        }
209    }
210}