supercov-engine 0.0.47

Rust instrumentation, evidence, attribution, and query engine for Supercov
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
//! `cargo nextest run` for the owned Rust frontend.
//!
//! nextest already runs every test in a process of its own and honours
//! Cargo's target runner, so nothing about its scheduling, retries or output
//! has to be reproduced: nextest builds the instrumented workspace, lists it
//! and runs it exactly as the user asked, and this program is configured as
//! the target runner. Every attempt nextest launches passes through here,
//! gets an evidence directory of its own, and is recorded together with the
//! exact identity nextest exposes to runners (`NEXTEST_TEST_NAME`,
//! `NEXTEST_ATTEMPT`, `NEXTEST_BINARY_ID`, ...). The listing nextest produced
//! beforehand names every selected test, so a test that ran unrecorded, or
//! was listed but never ran, fails the run rather than thinning the report.
//!
//! A runner the user already configured for the target keeps running the
//! binaries: this program wraps it rather than replacing it.

use std::{
    collections::{BTreeMap, BTreeSet},
    ffi::OsString,
    fs,
    io::Write,
    path::{Path, PathBuf},
    process::Command,
};

use nextest_metadata::TestListSummary;
use serde::{Deserialize, Serialize};

use crate::{
    coverage_report::{ExecutionScope, RawTestResult, TestProvenance},
    rust_cargo_configuration::{RustCargoResolvedRunner, resolve_cargo_runner_plan},
    rust_compiler_orchestration::{CargoMetadataOutput, cargo_metadata_arguments},
    rust_project::PreparedRustProject,
    rust_runner_attempt::{
        RustRunnerInvocationIdentity, classify_rust_runner_environment,
        parse_nextest_version_output, validate_nextest_version,
    },
    rust_test_runner::{
        CargoTestInvocation, RustTestRunnerError, capped_rustflags, instrumented_stack_environment,
        io_error, nextest_list_invocation, nextest_version_arguments, relative_source, snapshot,
    },
};

/// The first argument that makes this program nextest's target runner.
pub const RUNNER_MODE_ARGUMENT: &str = "__nextest-runner";
const EVIDENCE_DIR_ENV: &str = "SUPERCOV_RUST_EVIDENCE_DIR";
const PLAN_FILE: &str = "runner-plan.json";
const ATTEMPTS_DIRECTORY: &str = "attempts";

/// What the runner needs to know per target: the runner the user configured,
/// if any, which this program wraps.
#[derive(Debug, Serialize, Deserialize)]
struct RunnerPlan {
    targets: Vec<RunnerTarget>,
}

#[derive(Debug, Serialize, Deserialize)]
struct RunnerTarget {
    target: String,
    underlying: Option<RustCargoResolvedRunner>,
}

/// One attempt nextest launched, as the runner recorded it.
#[derive(Debug, Serialize, Deserialize)]
struct AttemptRecord {
    binary_id: String,
    test_name: String,
    attempt: usize,
    total_attempts: usize,
    runner_attempt_id: String,
    exit_code: i32,
    evidence: PathBuf,
}

/// A test binary nextest listed, joined to the Cargo target it was built from.
#[derive(Debug, Clone)]
struct NextestArtifact {
    executable: PathBuf,
    binary_name: String,
    kind: String,
    source: String,
}

pub struct NextestOutcome {
    pub results: Vec<RawTestResult>,
    pub artifact_files: Vec<PathBuf>,
    pub exit_code: i32,
}

// -------------------------------------------------------------- the engine

/// Run the user's `cargo nextest run` against the instrumented workspace with
/// this program as the target runner, and read back every attempt.
pub(crate) fn run_nextest(
    project: &PreparedRustProject,
    invocation: &CargoTestInvocation,
    root: &Path,
    run_id: &str,
    diagnostics: &mut dyn Write,
) -> Result<NextestOutcome, RustTestRunnerError> {
    fs::create_dir_all(root.join(ATTEMPTS_DIRECTORY)).map_err(io_error)?;
    let program = std::env::current_exe()
        .and_then(fs::canonicalize)
        .map_err(io_error)?;
    let cargo = |arguments: &[String]| {
        let mut command = Command::new(&invocation.program);
        command
            .args(arguments)
            .current_dir(&project.workspace_root)
            .env("CARGO_TARGET_DIR", &project.target_directory)
            .env("RUSTFLAGS", capped_rustflags());
        command
    };

    // The identity contract is pinned to the nextest versions it was verified
    // against; anything else fails before a test runs.
    let version = cargo(&nextest_version_arguments(invocation)?)
        .output()
        .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
    if !version.status.success() {
        return Err(RustTestRunnerError::CargoFailed(combined_output(
            &version.stdout,
            &version.stderr,
        )));
    }
    let version = parse_nextest_version_output(&version.stdout)
        .map_err(|error| RustTestRunnerError::Context(error.to_string()))?;
    validate_nextest_version(&version).map_err(|error| {
        RustTestRunnerError::UnsupportedCommand(format!("cargo-nextest {version}: {error}"))
    })?;

    // The target runner configuration: one entry per target nextest may run
    // for, each wrapping whatever runner the user had configured.
    let plan =
        resolve_cargo_runner_plan(&project.workspace_root, &project.workspace_root, invocation)
            .map_err(|error| RustTestRunnerError::Context(error.to_string()))?;
    let runner_plan = RunnerPlan {
        targets: plan
            .targets
            .iter()
            .map(|target| RunnerTarget {
                target: target.target.clone(),
                underlying: target
                    .underlying_runner
                    .as_ref()
                    .map(|runner| runner.resolve(&project.workspace_root)),
            })
            .collect(),
    };
    fs::write(root.join(PLAN_FILE), serde_json::to_vec(&runner_plan)?).map_err(io_error)?;
    let runner_configuration = runner_configuration_arguments(&program, root, &runner_plan)?;

    // The listing: every test nextest selected, per binary, before any runs.
    let listing = nextest_list_invocation(invocation)?;
    let mut list_arguments = listing.arguments;
    list_arguments.extend(runner_configuration.iter().cloned());
    if !listing.runner_arguments.is_empty() {
        list_arguments.push("--".into());
        list_arguments.extend(listing.runner_arguments);
    }
    let listed = cargo(&list_arguments)
        .output()
        .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
    if !listed.status.success() {
        return Err(RustTestRunnerError::CargoFailed(combined_output(
            &listed.stdout,
            &listed.stderr,
        )));
    }
    let catalog =
        TestListSummary::parse_json(String::from_utf8_lossy(&listed.stdout)).map_err(|error| {
            RustTestRunnerError::CargoJson(format!("invalid nextest test listing: {error}"))
        })?;
    let metadata = cargo(
        &cargo_metadata_arguments(invocation)
            .map_err(|error| RustTestRunnerError::Context(error.to_string()))?,
    )
    .output()
    .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
    if !metadata.status.success() {
        return Err(RustTestRunnerError::CargoFailed(combined_output(
            &metadata.stdout,
            &metadata.stderr,
        )));
    }
    let metadata: CargoMetadataOutput =
        serde_json::from_slice(&metadata.stdout).map_err(|error| {
            RustTestRunnerError::CargoJson(format!("invalid Cargo metadata: {error}"))
        })?;
    let artifacts = nextest_artifacts(project, &catalog, &metadata)?;

    // The run itself, streaming nextest's own output.
    let command = invocation.command_position().ok_or_else(|| {
        RustTestRunnerError::UnsupportedCommand(
            "the expanded Cargo invocation lost its nextest run subcommand".into(),
        )
    })?;
    let mut run_arguments = invocation.arguments[..command + 2].to_vec();
    run_arguments.extend(runner_configuration.iter().cloned());
    run_arguments.extend(invocation.arguments[command + 2..].iter().cloned());
    if !invocation.runner_arguments.is_empty() {
        run_arguments.push("--".into());
        run_arguments.extend(invocation.runner_arguments.iter().cloned());
    }
    let status = crate::child_signal_guard::status(&mut cargo(&run_arguments))
        .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
    let exit_code = status.code().unwrap_or(1);

    // Every attempt the runner recorded, checked against the listing.
    let mut selected = BTreeSet::new();
    for (binary_id, suite) in &catalog.rust_suites {
        for (name, case) in &suite.test_cases {
            if case.filter_match.is_match() {
                selected.insert((binary_id.to_string(), name.to_string()));
            }
        }
    }
    let mut records = read_attempts(&root.join(ATTEMPTS_DIRECTORY))?;
    records.sort_by(|left, right| {
        (&left.binary_id, &left.test_name, left.attempt).cmp(&(
            &right.binary_id,
            &right.test_name,
            right.attempt,
        ))
    });
    let mut seen = BTreeSet::new();
    let mut ran = BTreeSet::new();
    let mut results = Vec::with_capacity(records.len());
    for record in records {
        let key = (record.binary_id.clone(), record.test_name.clone());
        if !selected.contains(&key) {
            return Err(RustTestRunnerError::Context(format!(
                "nextest ran a test its listing did not select: {} {}",
                record.binary_id, record.test_name
            )));
        }
        if !seen.insert((key.clone(), record.attempt)) {
            return Err(RustTestRunnerError::Context(format!(
                "nextest attempt {} of {} {} was recorded twice",
                record.attempt, record.binary_id, record.test_name
            )));
        }
        ran.insert(key);
        let artifact = artifacts.get(&record.binary_id).ok_or_else(|| {
            RustTestRunnerError::Context(format!(
                "nextest ran a binary its listing lacks: {}",
                record.binary_id
            ))
        })?;
        let test_id = format!("{}::{}", artifact.source, record.test_name);
        let passed = record.exit_code == 0;
        if !passed {
            writeln!(
                diagnostics,
                "[supercov] Rust test failed on attempt {}: {test_id}",
                record.attempt
            )
            .map_err(io_error)?;
        }
        let attempt_id = format!("{run_id}:{}:{}", record.runner_attempt_id, record.attempt);
        let evidence = snapshot(&project.manifest, &record.evidence, &attempt_id)?;
        results.push(RawTestResult {
            test_id: Some(test_id.clone()),
            scope: Some(ExecutionScope {
                version: 1,
                run_id: run_id.into(),
                worker_id: format!("nextest-{}", record.binary_id),
                test_id: test_id.clone(),
                test_key: test_id.clone(),
                retry: record.attempt - 1,
                attempt_id: attempt_id.clone(),
            }),
            test: test_id,
            test_file: Some(artifact.source.clone()),
            title: Some(record.test_name.clone()),
            retry: Some(record.attempt - 1),
            status: Some(if passed { "passed" } else { "failed" }.into()),
            expected_status: Some("passed".into()),
            flaky: passed && record.attempt > 1,
            provenance: TestProvenance {
                runner: "nextest".into(),
                kind: artifact.kind.clone(),
                project: Some(artifact.binary_name.clone()),
                source: "supercov-owned-process-per-test".into(),
            },
            role: "test".into(),
            phases: evidence.phases,
            runtime: vec![evidence.snapshot],
            browser: Vec::new(),
            server: Vec::new(),
        });
    }
    // nextest stops early on a failure when asked to; then a listed test that
    // never ran is expected, and the exit code already says so.
    if exit_code == 0 {
        let missing = selected
            .difference(&ran)
            .map(|(binary_id, name)| format!("{binary_id} {name}"))
            .collect::<Vec<_>>();
        if !missing.is_empty() {
            return Err(RustTestRunnerError::Context(format!(
                "nextest listed {} test(s) it never ran through the runner: {}",
                missing.len(),
                missing.join(", ")
            )));
        }
    }
    Ok(NextestOutcome {
        results,
        artifact_files: artifacts
            .into_values()
            .map(|artifact| artifact.executable)
            .collect(),
        exit_code,
    })
}

/// `--config target.<triple>.runner=[...]` for every target of the plan: this
/// program in runner mode, told where to record and which target it serves.
fn runner_configuration_arguments(
    program: &Path,
    root: &Path,
    plan: &RunnerPlan,
) -> Result<Vec<String>, RustTestRunnerError> {
    let json = |value: &str| serde_json::to_string(value).map_err(RustTestRunnerError::Json);
    let program = json(program.to_str().ok_or_else(|| {
        RustTestRunnerError::Context("the Supercov executable path is not UTF-8".into())
    })?)?;
    let root = json(root.to_str().ok_or_else(|| {
        RustTestRunnerError::Context("the nextest evidence root is not UTF-8".into())
    })?)?;
    let marker = json(RUNNER_MODE_ARGUMENT)?;
    let mut arguments = Vec::with_capacity(plan.targets.len() * 2);
    let mut seen = BTreeSet::new();
    for target in &plan.targets {
        if !seen.insert(target.target.as_str()) {
            return Err(RustTestRunnerError::Context(format!(
                "the Cargo runner plan names target {} twice",
                target.target
            )));
        }
        let triple = json(&target.target)?;
        arguments.push("--config".into());
        arguments.push(format!(
            "target.{triple}.runner=[{program},{marker},{root},{triple}]"
        ));
    }
    if arguments.is_empty() {
        return Err(RustTestRunnerError::Context(
            "the Cargo runner plan selected no targets".into(),
        ));
    }
    Ok(arguments)
}

/// Join nextest's listing to Cargo's metadata: each binary to the target it
/// was built from, and so to the source file its tests are attributed to.
fn nextest_artifacts(
    project: &PreparedRustProject,
    catalog: &TestListSummary,
    metadata: &CargoMetadataOutput,
) -> Result<BTreeMap<String, NextestArtifact>, RustTestRunnerError> {
    let canonical_target = fs::canonicalize(&project.target_directory).map_err(io_error)?;
    let packages = metadata
        .packages
        .iter()
        .map(|package| (package.id.as_str(), package))
        .collect::<BTreeMap<_, _>>();
    let mut artifacts = BTreeMap::new();
    for (binary_id, suite) in &catalog.rust_suites {
        let binary_id = binary_id.to_string();
        if suite.binary.binary_id.to_string() != binary_id {
            return Err(RustTestRunnerError::CargoJson(format!(
                "nextest suite key disagrees with binary identity {binary_id}"
            )));
        }
        let package = packages
            .get(suite.binary.package_id.as_str())
            .ok_or_else(|| {
                RustTestRunnerError::CargoJson(format!(
                    "nextest binary {binary_id} names an unknown Cargo package {}",
                    suite.binary.package_id
                ))
            })?;
        if package.name != suite.package_name {
            return Err(RustTestRunnerError::CargoJson(format!(
                "nextest binary {binary_id} package name disagrees with Cargo metadata"
            )));
        }
        let mut targets = package.targets.iter().filter(|target| {
            target.name == suite.binary.binary_name
                && target
                    .kind
                    .iter()
                    .any(|kind| kind == suite.binary.kind.as_str())
        });
        let target = targets.next().ok_or_else(|| {
            RustTestRunnerError::CargoJson(format!(
                "nextest binary {binary_id} has no exact Cargo metadata target"
            ))
        })?;
        if targets.next().is_some() {
            return Err(RustTestRunnerError::CargoJson(format!(
                "nextest binary {binary_id} ambiguously matches Cargo metadata targets"
            )));
        }
        let executable =
            fs::canonicalize(suite.binary.binary_path.as_std_path()).map_err(io_error)?;
        if !executable.starts_with(&canonical_target)
            || !fs::metadata(&executable).is_ok_and(|metadata| metadata.is_file())
        {
            return Err(RustTestRunnerError::UnsafeArtifact(
                executable.display().to_string(),
            ));
        }
        let source = fs::canonicalize(&target.src_path).map_err(io_error)?;
        artifacts.insert(
            binary_id,
            NextestArtifact {
                executable,
                binary_name: suite.binary.binary_name.clone(),
                kind: if suite.binary.kind.as_str() == "test" {
                    "integration".into()
                } else {
                    "unit".into()
                },
                source: relative_source(&project.workspace_root, &source)?,
            },
        );
    }
    if artifacts.is_empty() {
        return Err(RustTestRunnerError::CargoJson(
            "nextest listed no test binaries".into(),
        ));
    }
    Ok(artifacts)
}

fn read_attempts(directory: &Path) -> Result<Vec<AttemptRecord>, RustTestRunnerError> {
    let mut records = Vec::new();
    for entry in fs::read_dir(directory).map_err(io_error)? {
        let path = entry.map_err(io_error)?.path();
        if path
            .extension()
            .is_some_and(|extension| extension == "json")
        {
            records.push(serde_json::from_slice(&fs::read(&path).map_err(io_error)?)?);
        }
    }
    Ok(records)
}

fn combined_output(stdout: &[u8], stderr: &[u8]) -> String {
    format!(
        "{}{}",
        String::from_utf8_lossy(stderr),
        String::from_utf8_lossy(stdout)
    )
    .trim()
    .to_owned()
}

// -------------------------------------------------------------- the runner

/// This program as nextest's target runner: `__nextest-runner <root> <target>
/// <binary> <arguments...>`. A list pass runs the binary untouched; an attempt
/// runs it under an evidence directory of its own and records the identity
/// nextest supplied.
pub fn nextest_runner(arguments: Vec<OsString>) -> i32 {
    match nextest_runner_inner(arguments) {
        Ok(code) => code,
        Err(error) => {
            eprintln!("[supercov] {error}");
            2
        }
    }
}

fn nextest_runner_inner(arguments: Vec<OsString>) -> Result<i32, RustTestRunnerError> {
    let mut arguments = arguments.into_iter();
    let (Some(root), Some(target), Some(binary)) =
        (arguments.next(), arguments.next(), arguments.next())
    else {
        return Err(RustTestRunnerError::Context(
            "the nextest runner needs an evidence root, a target and a binary".into(),
        ));
    };
    let root = PathBuf::from(root);
    let target = target.into_string().map_err(|_| {
        RustTestRunnerError::Context("the nextest runner target is not UTF-8".into())
    })?;
    let rest = arguments.collect::<Vec<_>>();
    let plan: RunnerPlan =
        serde_json::from_slice(&fs::read(root.join(PLAN_FILE)).map_err(io_error)?)?;
    let planned = plan
        .targets
        .iter()
        .find(|candidate| candidate.target == target)
        .ok_or_else(|| {
            RustTestRunnerError::Context(format!(
                "the nextest runner was invoked for an unplanned target: {target}"
            ))
        })?;
    let mut command = match &planned.underlying {
        Some(underlying) => {
            let mut command = Command::new(&underlying.program);
            command.args(&underlying.arguments).arg(&binary);
            command
        }
        None => Command::new(&binary),
    };
    command.args(&rest);
    let identity = classify_rust_runner_environment()
        .map_err(|error| RustTestRunnerError::Context(error.to_string()))?;
    let attempt = match identity {
        RustRunnerInvocationIdentity::NextestList(_) => {
            let status = command
                .status()
                .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
            return Ok(exit_code(&status));
        }
        RustRunnerInvocationIdentity::CargoSingleAttempt => {
            return Err(RustTestRunnerError::Context(
                "the nextest runner was invoked without nextest's identity".into(),
            ));
        }
        RustRunnerInvocationIdentity::NextestAttempt(attempt) => attempt,
    };
    let attempts = root.join(ATTEMPTS_DIRECTORY);
    let sequence = claim_sequence(&attempts)?;
    let evidence = attempts.join(format!("{sequence:08}"));
    fs::create_dir_all(&evidence).map_err(io_error)?;
    let status = command
        .envs(instrumented_stack_environment())
        .env(EVIDENCE_DIR_ENV, &evidence)
        .status()
        .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
    let code = exit_code(&status);
    let record = AttemptRecord {
        binary_id: attempt.invocation.binary_id,
        test_name: attempt.test_name,
        attempt: attempt.retry + 1,
        total_attempts: attempt.total_attempts,
        runner_attempt_id: attempt.runner_attempt_id,
        exit_code: code,
        evidence,
    };
    fs::write(
        attempts.join(format!("{sequence:08}.json")),
        serde_json::to_vec(&record)?,
    )
    .map_err(io_error)?;
    Ok(code)
}

/// Attempts run concurrently; each claims the next free sequence number by
/// creating its marker exclusively.
fn claim_sequence(directory: &Path) -> Result<usize, RustTestRunnerError> {
    fs::create_dir_all(directory).map_err(io_error)?;
    for candidate in 0..1_000_000_usize {
        match fs::OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(directory.join(format!("{candidate:08}.claim")))
        {
            Ok(_) => return Ok(candidate),
            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
            Err(error) => return Err(io_error(error)),
        }
    }
    Err(RustTestRunnerError::Io(
        "too many nextest attempt claims".into(),
    ))
}

/// The child's exit code as nextest should see it: the code itself, or the
/// shell convention of 128 plus the signal that ended it.
fn exit_code(status: &std::process::ExitStatus) -> i32 {
    if let Some(code) = status.code() {
        return code;
    }
    #[cfg(unix)]
    {
        use std::os::unix::process::ExitStatusExt;
        if let Some(signal) = status.signal() {
            return 128 + signal;
        }
    }
    1
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn runner_configuration_names_every_target_once() {
        let plan = RunnerPlan {
            targets: vec![
                RunnerTarget {
                    target: "aarch64-apple-darwin".into(),
                    underlying: None,
                },
                RunnerTarget {
                    target: "x86_64-unknown-linux-gnu".into(),
                    underlying: Some(RustCargoResolvedRunner {
                        program: "/opt/qemu".into(),
                        arguments: vec!["--fast".into()],
                    }),
                },
            ],
        };
        let arguments = runner_configuration_arguments(
            Path::new("/opt/super cov/supercov"),
            Path::new("/work/.supercov/rust-evidence/run/nextest"),
            &plan,
        )
        .unwrap();
        assert_eq!(
            arguments,
            [
                "--config",
                "target.\"aarch64-apple-darwin\".runner=[\"/opt/super cov/supercov\",\"__nextest-runner\",\"/work/.supercov/rust-evidence/run/nextest\",\"aarch64-apple-darwin\"]",
                "--config",
                "target.\"x86_64-unknown-linux-gnu\".runner=[\"/opt/super cov/supercov\",\"__nextest-runner\",\"/work/.supercov/rust-evidence/run/nextest\",\"x86_64-unknown-linux-gnu\"]",
            ]
        );
        let duplicated = RunnerPlan {
            targets: vec![
                RunnerTarget {
                    target: "aarch64-apple-darwin".into(),
                    underlying: None,
                },
                RunnerTarget {
                    target: "aarch64-apple-darwin".into(),
                    underlying: None,
                },
            ],
        };
        assert!(
            runner_configuration_arguments(Path::new("/s"), Path::new("/r"), &duplicated).is_err()
        );
    }

    #[test]
    fn attempt_records_round_trip() {
        let record = AttemptRecord {
            binary_id: "fixture::suite".into(),
            test_name: "passes_on_retry".into(),
            attempt: 2,
            total_attempts: 2,
            runner_attempt_id: "run:fixture::suite$passes_on_retry".into(),
            exit_code: 0,
            evidence: "/r/attempts/00000003".into(),
        };
        let decoded: AttemptRecord =
            serde_json::from_slice(&serde_json::to_vec(&record).unwrap()).unwrap();
        assert_eq!(decoded.attempt, 2);
        assert_eq!(decoded.test_name, "passes_on_retry");
        assert_eq!(decoded.evidence, PathBuf::from("/r/attempts/00000003"));
    }
}