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