1use crate::gen_cargo_nextest_run_cmd::RunKindDeps;
7use flowey::node::prelude::*;
8use std::collections::BTreeMap;
9use std::path::Path;
10
11pub fn nextest_junit_path(
13 config_file: &Path,
14 nextest_profile: &str,
15) -> anyhow::Result<Option<PathBuf>> {
16 let nextest_toml = fs_err::read_to_string(config_file)?
17 .parse::<toml_edit::DocumentMut>()
18 .context("failed to parse nextest.toml")?;
19
20 let path = Some(&nextest_toml)
21 .and_then(|i| i.get("profile"))
22 .and_then(|i| i.get(nextest_profile))
23 .and_then(|i| i.get("junit"))
24 .and_then(|i| i.get("path"));
25
26 if let Some(path) = path {
27 Ok(Some(
28 path.as_str().context("malformed nextest.toml")?.into(),
29 ))
30 } else {
31 Ok(None)
32 }
33}
34
35#[derive(Serialize, Deserialize)]
36pub struct TestResults {
37 pub all_tests_passed: bool,
38 pub junit_xml: Option<PathBuf>,
40}
41
42pub mod build_params {
44 use crate::run_cargo_build::CargoBuildProfile;
45 use crate::run_cargo_build::CargoFeatureSet;
46 use flowey::node::prelude::*;
47 use std::collections::BTreeMap;
48
49 #[derive(Serialize, Deserialize)]
51 pub enum TestPackages {
52 Workspace {
54 exclude: Vec<String>,
56 },
57 Crates {
59 crates: Vec<String>,
61 },
62 }
63
64 #[derive(Serialize, Deserialize, Clone)]
65 pub struct NextestBuildParams<C = VarNotClaimed> {
66 pub packages: ReadVar<TestPackages, C>,
68 pub features: CargoFeatureSet,
70 pub no_default_features: bool,
72 pub target: target_lexicon::Triple,
74 pub profile: CargoBuildProfile,
76 pub extra_env: ReadVar<BTreeMap<String, String>, C>,
78 }
79}
80
81#[derive(Serialize, Deserialize)]
83pub enum NextestRunKind {
84 BuildAndRun(build_params::NextestBuildParams),
86 RunFromArchive {
88 archive_file: ReadVar<PathBuf>,
89 target: Option<target_lexicon::Triple>,
90 nextest_bin: Option<ReadVar<PathBuf>>,
91 },
92}
93
94#[derive(Serialize, Deserialize)]
95pub struct Run {
96 pub friendly_name: String,
98 pub run_kind: NextestRunKind,
100 pub working_dir: ReadVar<PathBuf>,
102 pub config_file: ReadVar<PathBuf>,
104 pub tool_config_files: Vec<(String, ReadVar<PathBuf>)>,
106 pub nextest_profile: String,
109 pub nextest_filter_expr: Option<String>,
111 pub run_ignored: bool,
113 pub with_rlimit_unlimited_core_size: bool,
115 pub extra_env: Option<ReadVar<BTreeMap<String, String>>>,
117 pub pre_run_deps: Vec<ReadVar<SideEffect>>,
121 pub results: WriteVar<TestResults>,
123}
124
125flowey_config! {
126 pub struct Config {
128 pub fail_fast: Option<bool>,
131 pub terminate_job_on_fail: Option<bool>,
134 }
135}
136
137flowey_request! {
138 pub enum Request {
139 Run(Run),
140 }
141}
142
143new_flow_node_with_config!(struct Node);
144
145impl FlowNodeWithConfig for Node {
146 type Request = Request;
147 type Config = Config;
148
149 fn imports(ctx: &mut ImportCtx<'_>) {
150 ctx.import::<crate::cfg_cargo_common_flags::Node>();
151 ctx.import::<crate::download_cargo_nextest::Node>();
152 ctx.import::<crate::install_cargo_nextest::Node>();
153 ctx.import::<crate::install_rust::Node>();
154 ctx.import::<crate::gen_cargo_nextest_run_cmd::Node>();
155 }
156
157 fn emit(
158 config: Config,
159 requests: Vec<Self::Request>,
160 ctx: &mut NodeCtx<'_>,
161 ) -> anyhow::Result<()> {
162 let mut run = Vec::new();
163
164 for req in requests {
165 match req {
166 Request::Run(v) => run.push(v),
167 }
168 }
169
170 let fail_fast = config.fail_fast;
171 let terminate_job_on_fail = config.terminate_job_on_fail.unwrap_or(false);
172
173 for Run {
174 friendly_name,
175 run_kind,
176 working_dir,
177 config_file,
178 tool_config_files,
179 nextest_profile,
180 extra_env,
181 with_rlimit_unlimited_core_size,
182 nextest_filter_expr,
183 run_ignored,
184 pre_run_deps,
185 results,
186 } in run
187 {
188 let run_kind_deps = match run_kind {
189 NextestRunKind::BuildAndRun(params) => {
190 let cargo_flags = ctx.reqv(crate::cfg_cargo_common_flags::Request::GetFlags);
191
192 let nextest_installed = ctx.reqv(crate::install_cargo_nextest::Request);
193
194 let rust_toolchain = ctx.reqv(crate::install_rust::Request::GetRustupToolchain);
195
196 ctx.req(crate::install_rust::Request::InstallTargetTriple(
197 params.target.clone(),
198 ));
199
200 RunKindDeps::BuildAndRun {
201 params,
202 nextest_installed,
203 rust_toolchain,
204 cargo_flags,
205 }
206 }
207 NextestRunKind::RunFromArchive {
208 archive_file,
209 target,
210 nextest_bin,
211 } => {
212 let target = target.unwrap_or(target_lexicon::Triple::host());
213
214 let nextest_bin = nextest_bin.unwrap_or_else(|| {
215 ctx.reqv(|v| crate::download_cargo_nextest::Request::Get(target.clone(), v))
216 });
217
218 RunKindDeps::RunFromArchive {
219 archive_file,
220 nextest_bin,
221 target,
222 }
223 }
224 };
225
226 let cmd = ctx.reqv(|v| crate::gen_cargo_nextest_run_cmd::Request {
227 run_kind_deps,
228 working_dir: working_dir.clone(),
229 config_file: config_file.clone(),
230 tool_config_files,
231 nextest_profile: nextest_profile.clone(),
232 nextest_filter_expr,
233 run_ignored,
234 fail_fast,
235 extra_env,
236 extra_commands: None,
237 portable: false,
238 command: v,
239 });
240
241 let (all_tests_passed_read, all_tests_passed_write) = ctx.new_var();
242 let (junit_xml_read, junit_xml_write) = ctx.new_var();
243
244 ctx.emit_rust_step(format!("run '{friendly_name}' nextest tests"), |ctx| {
245 pre_run_deps.claim(ctx);
246
247 let working_dir = working_dir.claim(ctx);
248 let config_file = config_file.claim(ctx);
249 let all_tests_passed_var = all_tests_passed_write.claim(ctx);
250 let junit_xml_write = junit_xml_write.claim(ctx);
251 let cmd = cmd.claim(ctx);
252
253 move |rt| {
254 let working_dir = rt.read(working_dir);
255 let config_file = rt.read(config_file);
256 let cmd = rt.read(cmd);
257
258 let junit_path = nextest_junit_path(&config_file, &nextest_profile)?;
261
262 #[cfg(unix)]
281 let old_core_rlimits = if with_rlimit_unlimited_core_size
282 && matches!(rt.platform(), FlowPlatform::Linux(_))
283 {
284 let limits = rlimit::getrlimit(rlimit::Resource::CORE)?;
285 rlimit::setrlimit(
286 rlimit::Resource::CORE,
287 rlimit::INFINITY,
288 rlimit::INFINITY,
289 )?;
290 Some(limits)
291 } else {
292 None
293 };
294
295 #[cfg(not(unix))]
296 let _ = with_rlimit_unlimited_core_size;
297
298 log::info!("{cmd}");
299
300 assert_eq!(cmd.commands.len(), 1);
309 let mut command = std::process::Command::new(&cmd.commands[0].0);
310 command
311 .args(&cmd.commands[0].1)
312 .envs(&cmd.env)
313 .current_dir(&working_dir);
314
315 let mut child = command.spawn().with_context(|| {
316 format!("failed to spawn '{}'", cmd.commands[0].0.to_string_lossy())
317 })?;
318
319 let status = child.wait()?;
320
321 #[cfg(unix)]
322 if let Some((soft, hard)) = old_core_rlimits {
323 rlimit::setrlimit(rlimit::Resource::CORE, soft, hard)?;
324 }
325
326 let all_tests_passed = match (status.success(), status.code()) {
327 (true, _) => true,
328 (false, Some(100)) => false,
330 (false, _) => anyhow::bail!("failed to run nextest"),
332 };
333
334 rt.write(all_tests_passed_var, &all_tests_passed);
335
336 if !all_tests_passed {
337 log::warn!("encountered at least one test failure!");
338
339 if terminate_job_on_fail {
340 anyhow::bail!("terminating job (TerminateJobOnFail = true)")
341 } else {
342 if matches!(rt.backend(), FlowBackend::Ado) {
345 eprintln!("##vso[task.complete result=SucceededWithIssues;]")
346 } else {
347 log::warn!("encountered at least one test failure");
348 }
349 }
350 }
351
352 let junit_xml = if let Some(junit_path) = junit_path {
353 let emitted_xml = working_dir
354 .join("target")
355 .join("nextest")
356 .join(&nextest_profile)
357 .join(junit_path);
358 let final_xml = std::env::current_dir()?.join("junit.xml");
359 fs_err::copy(emitted_xml, &final_xml)?;
361 Some(final_xml.absolute()?)
362 } else {
363 None
364 };
365
366 rt.write(junit_xml_write, &junit_xml);
367
368 Ok(())
369 }
370 });
371
372 ctx.emit_minor_rust_step("write results", |ctx| {
373 let all_tests_passed = all_tests_passed_read.claim(ctx);
374 let junit_xml = junit_xml_read.claim(ctx);
375 let results = results.claim(ctx);
376
377 move |rt| {
378 let all_tests_passed = rt.read(all_tests_passed);
379 let junit_xml = rt.read(junit_xml);
380
381 rt.write(
382 results,
383 &TestResults {
384 all_tests_passed,
385 junit_xml,
386 },
387 );
388 }
389 });
390 }
391
392 Ok(())
393 }
394}
395
396impl build_params::NextestBuildParams {
398 pub fn claim(self, ctx: &mut StepCtx<'_>) -> build_params::NextestBuildParams<VarClaimed> {
399 let build_params::NextestBuildParams {
400 packages,
401 features,
402 no_default_features,
403 target,
404 profile,
405 extra_env,
406 } = self;
407
408 build_params::NextestBuildParams {
409 packages: packages.claim(ctx),
410 features,
411 no_default_features,
412 target,
413 profile,
414 extra_env: extra_env.claim(ctx),
415 }
416 }
417}