rust_analyzer/
test_runner.rs

1//! This module provides the functionality needed to run `cargo test` in a background
2//! thread and report the result of each test in a channel.
3
4use crossbeam_channel::Sender;
5use paths::{AbsPath, Utf8Path};
6use project_model::TargetKind;
7use serde::Deserialize as _;
8use serde_derive::Deserialize;
9use toolchain::Tool;
10
11use crate::{
12    command::{CommandHandle, JsonLinesParser},
13    flycheck::CargoOptions,
14};
15
16#[derive(Debug, Deserialize)]
17#[serde(tag = "event", rename_all = "camelCase")]
18pub(crate) enum TestState {
19    Started,
20    Ok,
21    Ignored,
22    Failed {
23        // the stdout field is not always present depending on cargo test flags
24        #[serde(skip_serializing_if = "String::is_empty", default)]
25        stdout: String,
26    },
27}
28
29#[derive(Debug)]
30pub(crate) struct CargoTestMessage {
31    pub target: TestTarget,
32    pub output: CargoTestOutput,
33}
34
35#[derive(Debug, Deserialize)]
36#[serde(tag = "type", rename_all = "camelCase")]
37pub(crate) enum CargoTestOutput {
38    Test {
39        name: String,
40        #[serde(flatten)]
41        state: TestState,
42    },
43    Suite,
44    Finished,
45    Custom {
46        text: String,
47    },
48}
49
50pub(crate) struct CargoTestOutputParser {
51    pub target: TestTarget,
52}
53
54impl CargoTestOutputParser {
55    pub(crate) fn new(test_target: &TestTarget) -> Self {
56        Self { target: test_target.clone() }
57    }
58}
59
60impl JsonLinesParser<CargoTestMessage> for CargoTestOutputParser {
61    fn from_line(&self, line: &str, _error: &mut String) -> Option<CargoTestMessage> {
62        let mut deserializer = serde_json::Deserializer::from_str(line);
63        deserializer.disable_recursion_limit();
64
65        Some(CargoTestMessage {
66            target: self.target.clone(),
67            output: if let Ok(message) = CargoTestOutput::deserialize(&mut deserializer) {
68                message
69            } else {
70                CargoTestOutput::Custom { text: line.to_owned() }
71            },
72        })
73    }
74
75    fn from_eof(&self) -> Option<CargoTestMessage> {
76        Some(CargoTestMessage { target: self.target.clone(), output: CargoTestOutput::Finished })
77    }
78}
79
80#[derive(Debug)]
81pub(crate) struct CargoTestHandle {
82    _handle: CommandHandle<CargoTestMessage>,
83}
84
85// Example of a cargo test command:
86//
87// cargo test --package my-package --bin my_bin --no-fail-fast -- module::func -Z unstable-options --format=json
88
89#[derive(Debug, Clone)]
90pub(crate) struct TestTarget {
91    pub package: String,
92    pub target: String,
93    pub kind: TargetKind,
94}
95
96impl CargoTestHandle {
97    pub(crate) fn new(
98        path: Option<&str>,
99        options: CargoOptions,
100        root: &AbsPath,
101        ws_target_dir: Option<&Utf8Path>,
102        test_target: TestTarget,
103        sender: Sender<CargoTestMessage>,
104    ) -> std::io::Result<Self> {
105        let mut cmd = toolchain::command(Tool::Cargo.path(), root, &options.extra_env);
106        cmd.env("RUSTC_BOOTSTRAP", "1");
107        cmd.arg("--color=always");
108        cmd.arg("test");
109
110        cmd.arg("--package");
111        cmd.arg(&test_target.package);
112
113        if let TargetKind::Lib { .. } = test_target.kind {
114            // no name required with lib because there can only be one lib target per package
115            cmd.arg("--lib");
116        } else if let Some(cargo_target) = test_target.kind.as_cargo_target() {
117            cmd.arg(format!("--{cargo_target}"));
118            cmd.arg(&test_target.target);
119        } else {
120            tracing::warn!("Running test for unknown cargo target {:?}", test_target.kind);
121        }
122
123        // --no-fail-fast is needed to ensure that all requested tests will run
124        cmd.arg("--no-fail-fast");
125        cmd.arg("--manifest-path");
126        cmd.arg(root.join("Cargo.toml"));
127        options.apply_on_command(&mut cmd, ws_target_dir);
128        cmd.arg("--");
129        if let Some(path) = path {
130            cmd.arg(path);
131        }
132        cmd.args(["-Z", "unstable-options"]);
133        cmd.arg("--format=json");
134
135        for extra_arg in options.extra_test_bin_args {
136            cmd.arg(extra_arg);
137        }
138
139        Ok(Self {
140            _handle: CommandHandle::spawn(
141                cmd,
142                CargoTestOutputParser::new(&test_target),
143                sender,
144                None,
145            )?,
146        })
147    }
148}