Skip to content

Commit b6be8c6

Browse files
author
duncan
committed
fix testing for packages with multiple targets
fix test running by invoking cargo per package remove hack_recover_crate_name make clippy happy fix testing for packages with multiple targets fix test running by invoking cargo per package remove hack_recover_crate_name make clippy happy fix testing for packages with multiple targets fix bad merge replace TargetKind::fmt with TargetKind::as_cargo_target to clarify intention dedupulicate requested test runs replace ParseFromLine with CargoParser
1 parent 1795a85 commit b6be8c6

File tree

9 files changed

+187
-144
lines changed

9 files changed

+187
-144
lines changed

crates/project-model/src/cargo_workspace.rs

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! See [`CargoWorkspace`].
22
3-
use std::ops;
43
use std::str::from_utf8;
4+
use std::ops;
55

66
use anyhow::Context;
77
use base_db::Env;
@@ -224,6 +224,7 @@ pub enum TargetKind {
224224
Example,
225225
Test,
226226
Bench,
227+
/// Cargo calls this kind `custom-build`
227228
BuildScript,
228229
Other,
229230
}
@@ -252,6 +253,22 @@ impl TargetKind {
252253
pub fn is_proc_macro(self) -> bool {
253254
matches!(self, TargetKind::Lib { is_proc_macro: true })
254255
}
256+
257+
/// If this is a valid cargo target, returns the name cargo uses in command line arguments
258+
/// and output, otherwise None.
259+
/// https://docs.rs/cargo_metadata/latest/cargo_metadata/enum.TargetKind.html
260+
pub fn as_cargo_target(self) -> Option<&'static str> {
261+
match self {
262+
TargetKind::Bin => Some("bin"),
263+
TargetKind::Lib { is_proc_macro: true } => Some("proc-macro"),
264+
TargetKind::Lib { is_proc_macro: false } => Some("lib"),
265+
TargetKind::Example => Some("example"),
266+
TargetKind::Test => Some("test"),
267+
TargetKind::Bench => Some("bench"),
268+
TargetKind::BuildScript => Some("custom-build"),
269+
TargetKind::Other => None,
270+
}
271+
}
255272
}
256273

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

crates/rust-analyzer/src/command.rs

+26-13
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

+7-5
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

+7-5
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 {
@@ -324,7 +324,7 @@ impl FlycheckActor {
324324

325325
tracing::debug!(?command, "will restart flycheck");
326326
let (sender, receiver) = unbounded();
327-
match CommandHandle::spawn(command, sender) {
327+
match CommandHandle::spawn(command, CargoCheckParser, sender) {
328328
Ok(command_handle) => {
329329
tracing::debug!(command = formatted_command, "did restart flycheck");
330330
self.command_handle = Some(command_handle);
@@ -550,8 +550,10 @@ enum CargoCheckMessage {
550550
Diagnostic { diagnostic: Diagnostic, package_id: Option<Arc<PackageId>> },
551551
}
552552

553-
impl ParseFromLine for CargoCheckMessage {
554-
fn from_line(line: &str, error: &mut String) -> Option<Self> {
553+
struct CargoCheckParser;
554+
555+
impl CargoParser<CargoCheckMessage> for CargoCheckParser {
556+
fn from_line(&self, line: &str, error: &mut String) -> Option<CargoCheckMessage> {
555557
let mut deserializer = serde_json::Deserializer::from_str(line);
556558
deserializer.disable_recursion_limit();
557559
if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
@@ -580,7 +582,7 @@ impl ParseFromLine for CargoCheckMessage {
580582
None
581583
}
582584

583-
fn from_eof() -> Option<Self> {
585+
fn from_eof(&self) -> Option<CargoCheckMessage> {
584586
None
585587
}
586588
}

crates/rust-analyzer/src/hack_recover_crate_name.rs

-25
This file was deleted.

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

+67-54
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ use crate::{
3636
config::{Config, RustfmtConfig, WorkspaceSymbolConfig},
3737
diagnostics::convert_diagnostic,
3838
global_state::{FetchWorkspaceRequest, GlobalState, GlobalStateSnapshot},
39-
hack_recover_crate_name,
4039
line_index::LineEndings,
4140
lsp::{
4241
ext::{
@@ -196,74 +195,90 @@ pub(crate) fn handle_view_item_tree(
196195
Ok(res)
197196
}
198197

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

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

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

crates/rust-analyzer/src/lib.rs

-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ mod command;
1515
mod diagnostics;
1616
mod discover;
1717
mod flycheck;
18-
mod hack_recover_crate_name;
1918
mod line_index;
2019
mod main_loop;
2120
mod mem_docs;

0 commit comments

Comments
 (0)