Skip to content

Commit 0c1b483

Browse files
authored
Merge pull request #19005 from duncanawoods/18955---fix-running-tests-for-packages-with-multiple-targets
fix testing packages with multiple targets
2 parents ef60b78 + 2f5a7a6 commit 0c1b483

File tree

9 files changed

+186
-143
lines changed

9 files changed

+186
-143
lines changed

crates/project-model/src/cargo_workspace.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ pub enum TargetKind {
226226
Example,
227227
Test,
228228
Bench,
229+
/// Cargo calls this kind `custom-build`
229230
BuildScript,
230231
Other,
231232
}
@@ -254,6 +255,22 @@ impl TargetKind {
254255
pub fn is_proc_macro(self) -> bool {
255256
matches!(self, TargetKind::Lib { is_proc_macro: true })
256257
}
258+
259+
/// If this is a valid cargo target, returns the name cargo uses in command line arguments
260+
/// and output, otherwise None.
261+
/// https://docs.rs/cargo_metadata/latest/cargo_metadata/enum.TargetKind.html
262+
pub fn as_cargo_target(self) -> Option<&'static str> {
263+
match self {
264+
TargetKind::Bin => Some("bin"),
265+
TargetKind::Lib { is_proc_macro: true } => Some("proc-macro"),
266+
TargetKind::Lib { is_proc_macro: false } => Some("lib"),
267+
TargetKind::Example => Some("example"),
268+
TargetKind::Test => Some("test"),
269+
TargetKind::Bench => Some("bench"),
270+
TargetKind::BuildScript => Some("custom-build"),
271+
TargetKind::Other => None,
272+
}
273+
}
257274
}
258275

259276
#[derive(Default, Clone, Debug, PartialEq, Eq)]

crates/rust-analyzer/src/command.rs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,33 @@ use crossbeam_channel::Sender;
1313
use process_wrap::std::{StdChildWrapper, StdCommandWrap};
1414
use stdx::process::streaming_output;
1515

16-
/// Cargo output is structured as a one JSON per line. This trait abstracts parsing one line of
17-
/// cargo output into a Rust data type.
18-
pub(crate) trait ParseFromLine: Sized + Send + 'static {
19-
fn from_line(line: &str, error: &mut String) -> Option<Self>;
20-
fn from_eof() -> Option<Self>;
16+
/// Cargo output is structured as one JSON per line. This trait abstracts parsing one line of
17+
/// cargo output into a Rust data type
18+
pub(crate) trait CargoParser<T>: Send + 'static {
19+
fn from_line(&self, line: &str, error: &mut String) -> Option<T>;
20+
fn from_eof(&self) -> Option<T>;
2121
}
2222

2323
struct CargoActor<T> {
24+
parser: Box<dyn CargoParser<T>>,
2425
sender: Sender<T>,
2526
stdout: ChildStdout,
2627
stderr: ChildStderr,
2728
}
2829

29-
impl<T: ParseFromLine> CargoActor<T> {
30-
fn new(sender: Sender<T>, stdout: ChildStdout, stderr: ChildStderr) -> Self {
31-
CargoActor { sender, stdout, stderr }
30+
impl<T: Sized + Send + 'static> CargoActor<T> {
31+
fn new(
32+
parser: impl CargoParser<T>,
33+
sender: Sender<T>,
34+
stdout: ChildStdout,
35+
stderr: ChildStderr,
36+
) -> Self {
37+
let parser = Box::new(parser);
38+
CargoActor { parser, sender, stdout, stderr }
3239
}
40+
}
3341

42+
impl<T: Sized + Send + 'static> CargoActor<T> {
3443
fn run(self) -> io::Result<(bool, String)> {
3544
// We manually read a line at a time, instead of using serde's
3645
// stream deserializers, because the deserializer cannot recover
@@ -47,7 +56,7 @@ impl<T: ParseFromLine> CargoActor<T> {
4756
let mut read_at_least_one_stderr_message = false;
4857
let process_line = |line: &str, error: &mut String| {
4958
// Try to deserialize a message from Cargo or Rustc.
50-
if let Some(t) = T::from_line(line, error) {
59+
if let Some(t) = self.parser.from_line(line, error) {
5160
self.sender.send(t).unwrap();
5261
true
5362
} else {
@@ -68,7 +77,7 @@ impl<T: ParseFromLine> CargoActor<T> {
6877
}
6978
},
7079
&mut || {
71-
if let Some(t) = T::from_eof() {
80+
if let Some(t) = self.parser.from_eof() {
7281
self.sender.send(t).unwrap();
7382
}
7483
},
@@ -116,8 +125,12 @@ impl<T> fmt::Debug for CommandHandle<T> {
116125
}
117126
}
118127

119-
impl<T: ParseFromLine> CommandHandle<T> {
120-
pub(crate) fn spawn(mut command: Command, sender: Sender<T>) -> std::io::Result<Self> {
128+
impl<T: Sized + Send + 'static> CommandHandle<T> {
129+
pub(crate) fn spawn(
130+
mut command: Command,
131+
parser: impl CargoParser<T>,
132+
sender: Sender<T>,
133+
) -> std::io::Result<Self> {
121134
command.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null());
122135

123136
let program = command.get_program().into();
@@ -134,7 +147,7 @@ impl<T: ParseFromLine> CommandHandle<T> {
134147
let stdout = child.0.stdout().take().unwrap();
135148
let stderr = child.0.stderr().take().unwrap();
136149

137-
let actor = CargoActor::<T>::new(sender, stdout, stderr);
150+
let actor = CargoActor::<T>::new(parser, sender, stdout, stderr);
138151
let thread = stdx::thread::Builder::new(stdx::thread::ThreadIntent::Worker)
139152
.name("CommandHandle".to_owned())
140153
.spawn(move || actor.run())

crates/rust-analyzer/src/discover.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
99
use serde_json::Value;
1010
use tracing::{info_span, span::EnteredSpan};
1111

12-
use crate::command::{CommandHandle, ParseFromLine};
12+
use crate::command::{CargoParser, CommandHandle};
1313

1414
pub(crate) const ARG_PLACEHOLDER: &str = "{arg}";
1515

@@ -66,7 +66,7 @@ impl DiscoverCommand {
6666
cmd.args(args);
6767

6868
Ok(DiscoverHandle {
69-
_handle: CommandHandle::spawn(cmd, self.sender.clone())?,
69+
_handle: CommandHandle::spawn(cmd, DiscoverProjectParser, self.sender.clone())?,
7070
span: info_span!("discover_command").entered(),
7171
})
7272
}
@@ -115,8 +115,10 @@ impl DiscoverProjectMessage {
115115
}
116116
}
117117

118-
impl ParseFromLine for DiscoverProjectMessage {
119-
fn from_line(line: &str, _error: &mut String) -> Option<Self> {
118+
struct DiscoverProjectParser;
119+
120+
impl CargoParser<DiscoverProjectMessage> for DiscoverProjectParser {
121+
fn from_line(&self, line: &str, _error: &mut String) -> Option<DiscoverProjectMessage> {
120122
// can the line even be deserialized as JSON?
121123
let Ok(data) = serde_json::from_str::<Value>(line) else {
122124
let err = DiscoverProjectData::Error { error: line.to_owned(), source: None };
@@ -131,7 +133,7 @@ impl ParseFromLine for DiscoverProjectMessage {
131133
Some(msg)
132134
}
133135

134-
fn from_eof() -> Option<Self> {
136+
fn from_eof(&self) -> Option<DiscoverProjectMessage> {
135137
None
136138
}
137139
}

crates/rust-analyzer/src/flycheck.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub(crate) use cargo_metadata::diagnostic::{
1717
use toolchain::Tool;
1818
use triomphe::Arc;
1919

20-
use crate::command::{CommandHandle, ParseFromLine};
20+
use crate::command::{CargoParser, CommandHandle};
2121

2222
#[derive(Clone, Debug, Default, PartialEq, Eq)]
2323
pub(crate) enum InvocationStrategy {
@@ -329,7 +329,7 @@ impl FlycheckActor {
329329

330330
tracing::debug!(?command, "will restart flycheck");
331331
let (sender, receiver) = unbounded();
332-
match CommandHandle::spawn(command, sender) {
332+
match CommandHandle::spawn(command, CargoCheckParser, sender) {
333333
Ok(command_handle) => {
334334
tracing::debug!(command = formatted_command, "did restart flycheck");
335335
self.command_handle = Some(command_handle);
@@ -558,8 +558,10 @@ enum CargoCheckMessage {
558558
Diagnostic { diagnostic: Diagnostic, package_id: Option<Arc<PackageId>> },
559559
}
560560

561-
impl ParseFromLine for CargoCheckMessage {
562-
fn from_line(line: &str, error: &mut String) -> Option<Self> {
561+
struct CargoCheckParser;
562+
563+
impl CargoParser<CargoCheckMessage> for CargoCheckParser {
564+
fn from_line(&self, line: &str, error: &mut String) -> Option<CargoCheckMessage> {
563565
let mut deserializer = serde_json::Deserializer::from_str(line);
564566
deserializer.disable_recursion_limit();
565567
if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
@@ -588,7 +590,7 @@ impl ParseFromLine for CargoCheckMessage {
588590
None
589591
}
590592

591-
fn from_eof() -> Option<Self> {
593+
fn from_eof(&self) -> Option<CargoCheckMessage> {
592594
None
593595
}
594596
}

crates/rust-analyzer/src/hack_recover_crate_name.rs

Lines changed: 0 additions & 25 deletions
This file was deleted.

crates/rust-analyzer/src/handlers/request.rs

Lines changed: 67 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ use crate::{
3535
config::{Config, RustfmtConfig, WorkspaceSymbolConfig},
3636
diagnostics::convert_diagnostic,
3737
global_state::{FetchWorkspaceRequest, GlobalState, GlobalStateSnapshot},
38-
hack_recover_crate_name,
3938
line_index::LineEndings,
4039
lsp::{
4140
LspError, completion_item_hash,
@@ -195,74 +194,90 @@ pub(crate) fn handle_view_item_tree(
195194
Ok(res)
196195
}
197196

198-
// cargo test requires the real package name which might contain hyphens but
199-
// the test identifier passed to this function is the namespace form where hyphens
200-
// are replaced with underscores so we have to reverse this and find the real package name
201-
fn find_package_name(namespace_root: &str, cargo: &CargoWorkspace) -> Option<String> {
197+
// cargo test requires:
198+
// - the package name - the root of the test identifier supplied to this handler can be
199+
// a package or a target inside a package.
200+
// - the target name - if the test identifier is a target, it's needed in addition to the
201+
// package name to run the right test
202+
// - real names - the test identifier uses the namespace form where hyphens are replaced with
203+
// underscores. cargo test requires the real name.
204+
// - the target kind e.g. bin or lib
205+
fn find_test_target(namespace_root: &str, cargo: &CargoWorkspace) -> Option<TestTarget> {
202206
cargo.packages().find_map(|p| {
203207
let package_name = &cargo[p].name;
204-
if package_name.replace('-', "_") == namespace_root {
205-
Some(package_name.clone())
206-
} else {
207-
None
208+
for target in cargo[p].targets.iter() {
209+
let target_name = &cargo[*target].name;
210+
if target_name.replace('-', "_") == namespace_root {
211+
return Some(TestTarget {
212+
package: package_name.clone(),
213+
target: target_name.clone(),
214+
kind: cargo[*target].kind,
215+
});
216+
}
208217
}
218+
None
209219
})
210220
}
211221

222+
fn get_all_targets(cargo: &CargoWorkspace) -> Vec<TestTarget> {
223+
cargo
224+
.packages()
225+
.flat_map(|p| {
226+
let package_name = &cargo[p].name;
227+
cargo[p].targets.iter().map(|target| {
228+
let target_name = &cargo[*target].name;
229+
TestTarget {
230+
package: package_name.clone(),
231+
target: target_name.clone(),
232+
kind: cargo[*target].kind,
233+
}
234+
})
235+
})
236+
.collect()
237+
}
238+
212239
pub(crate) fn handle_run_test(
213240
state: &mut GlobalState,
214241
params: lsp_ext::RunTestParams,
215242
) -> anyhow::Result<()> {
216243
if let Some(_session) = state.test_run_session.take() {
217244
state.send_notification::<lsp_ext::EndRunTest>(());
218245
}
219-
// We detect the lowest common ancestor of all included tests, and
220-
// run it. We ignore excluded tests for now, the client will handle
221-
// it for us.
222-
let lca = match params.include {
223-
Some(tests) => tests
224-
.into_iter()
225-
.reduce(|x, y| {
226-
let mut common_prefix = "".to_owned();
227-
for (xc, yc) in x.chars().zip(y.chars()) {
228-
if xc != yc {
229-
break;
230-
}
231-
common_prefix.push(xc);
232-
}
233-
common_prefix
234-
})
235-
.unwrap_or_default(),
236-
None => "".to_owned(),
237-
};
238-
let (namespace_root, test_path) = if lca.is_empty() {
239-
(None, None)
240-
} else if let Some((namespace_root, path)) = lca.split_once("::") {
241-
(Some(namespace_root), Some(path))
242-
} else {
243-
(Some(lca.as_str()), None)
244-
};
246+
245247
let mut handles = vec![];
246248
for ws in &*state.workspaces {
247249
if let ProjectWorkspaceKind::Cargo { cargo, .. } = &ws.kind {
248-
let test_target = if let Some(namespace_root) = namespace_root {
249-
if let Some(package_name) = find_package_name(namespace_root, cargo) {
250-
TestTarget::Package(package_name)
251-
} else {
252-
TestTarget::Workspace
253-
}
254-
} else {
255-
TestTarget::Workspace
250+
// need to deduplicate `include` to avoid redundant test runs
251+
let tests = match params.include {
252+
Some(ref include) => include
253+
.iter()
254+
.unique()
255+
.filter_map(|test| {
256+
let (root, remainder) = match test.split_once("::") {
257+
Some((root, remainder)) => (root.to_owned(), Some(remainder)),
258+
None => (test.clone(), None),
259+
};
260+
if let Some(target) = find_test_target(&root, cargo) {
261+
Some((target, remainder))
262+
} else {
263+
tracing::error!("Test target not found for: {test}");
264+
None
265+
}
266+
})
267+
.collect_vec(),
268+
None => get_all_targets(cargo).into_iter().map(|target| (target, None)).collect(),
256269
};
257270

258-
let handle = CargoTestHandle::new(
259-
test_path,
260-
state.config.cargo_test_options(None),
261-
cargo.workspace_root(),
262-
test_target,
263-
state.test_run_sender.clone(),
264-
)?;
265-
handles.push(handle);
271+
for (target, path) in tests {
272+
let handle = CargoTestHandle::new(
273+
path,
274+
state.config.cargo_test_options(None),
275+
cargo.workspace_root(),
276+
target,
277+
state.test_run_sender.clone(),
278+
)?;
279+
handles.push(handle);
280+
}
266281
}
267282
}
268283
// Each process send finished signal twice, once for stdout and once for stderr
@@ -286,9 +301,7 @@ pub(crate) fn handle_discover_test(
286301
}
287302
None => (snap.analysis.discover_test_roots()?, None),
288303
};
289-
for t in &tests {
290-
hack_recover_crate_name::insert_name(t.id.clone());
291-
}
304+
292305
Ok(lsp_ext::DiscoverTestResults {
293306
tests: tests
294307
.into_iter()

crates/rust-analyzer/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ mod command;
2424
mod diagnostics;
2525
mod discover;
2626
mod flycheck;
27-
mod hack_recover_crate_name;
2827
mod line_index;
2928
mod main_loop;
3029
mod mem_docs;

0 commit comments

Comments
 (0)