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

formatting - remove trailing space

formatting for rustfmt CI
This commit is contained in:
duncan 2025-01-22 14:27:27 +00:00
parent 1795a85be3
commit 2f5a7a619d
9 changed files with 186 additions and 143 deletions

View file

@ -224,6 +224,7 @@ pub enum TargetKind {
Example, Example,
Test, Test,
Bench, Bench,
/// Cargo calls this kind `custom-build`
BuildScript, BuildScript,
Other, Other,
} }
@ -252,6 +253,22 @@ impl TargetKind {
pub fn is_proc_macro(self) -> bool { pub fn is_proc_macro(self) -> bool {
matches!(self, TargetKind::Lib { is_proc_macro: true }) matches!(self, TargetKind::Lib { is_proc_macro: true })
} }
/// If this is a valid cargo target, returns the name cargo uses in command line arguments
/// and output, otherwise None.
/// https://docs.rs/cargo_metadata/latest/cargo_metadata/enum.TargetKind.html
pub fn as_cargo_target(self) -> Option<&'static str> {
match self {
TargetKind::Bin => Some("bin"),
TargetKind::Lib { is_proc_macro: true } => Some("proc-macro"),
TargetKind::Lib { is_proc_macro: false } => Some("lib"),
TargetKind::Example => Some("example"),
TargetKind::Test => Some("test"),
TargetKind::Bench => Some("bench"),
TargetKind::BuildScript => Some("custom-build"),
TargetKind::Other => None,
}
}
} }
#[derive(Default, Clone, Debug, PartialEq, Eq)] #[derive(Default, Clone, Debug, PartialEq, Eq)]

View file

@ -13,24 +13,33 @@ use crossbeam_channel::Sender;
use process_wrap::std::{StdChildWrapper, StdCommandWrap}; use process_wrap::std::{StdChildWrapper, StdCommandWrap};
use stdx::process::streaming_output; use stdx::process::streaming_output;
/// Cargo output is structured as a one JSON per line. This trait abstracts parsing one line of /// Cargo output is structured as one JSON per line. This trait abstracts parsing one line of
/// cargo output into a Rust data type. /// cargo output into a Rust data type
pub(crate) trait ParseFromLine: Sized + Send + 'static { pub(crate) trait CargoParser<T>: Send + 'static {
fn from_line(line: &str, error: &mut String) -> Option<Self>; fn from_line(&self, line: &str, error: &mut String) -> Option<T>;
fn from_eof() -> Option<Self>; fn from_eof(&self) -> Option<T>;
} }
struct CargoActor<T> { struct CargoActor<T> {
parser: Box<dyn CargoParser<T>>,
sender: Sender<T>, sender: Sender<T>,
stdout: ChildStdout, stdout: ChildStdout,
stderr: ChildStderr, stderr: ChildStderr,
} }
impl<T: ParseFromLine> CargoActor<T> { impl<T: Sized + Send + 'static> CargoActor<T> {
fn new(sender: Sender<T>, stdout: ChildStdout, stderr: ChildStderr) -> Self { fn new(
CargoActor { sender, stdout, stderr } parser: impl CargoParser<T>,
sender: Sender<T>,
stdout: ChildStdout,
stderr: ChildStderr,
) -> Self {
let parser = Box::new(parser);
CargoActor { parser, sender, stdout, stderr }
} }
}
impl<T: Sized + Send + 'static> CargoActor<T> {
fn run(self) -> io::Result<(bool, String)> { fn run(self) -> io::Result<(bool, String)> {
// We manually read a line at a time, instead of using serde's // We manually read a line at a time, instead of using serde's
// stream deserializers, because the deserializer cannot recover // stream deserializers, because the deserializer cannot recover
@ -47,7 +56,7 @@ impl<T: ParseFromLine> CargoActor<T> {
let mut read_at_least_one_stderr_message = false; let mut read_at_least_one_stderr_message = false;
let process_line = |line: &str, error: &mut String| { let process_line = |line: &str, error: &mut String| {
// Try to deserialize a message from Cargo or Rustc. // Try to deserialize a message from Cargo or Rustc.
if let Some(t) = T::from_line(line, error) { if let Some(t) = self.parser.from_line(line, error) {
self.sender.send(t).unwrap(); self.sender.send(t).unwrap();
true true
} else { } else {
@ -68,7 +77,7 @@ impl<T: ParseFromLine> CargoActor<T> {
} }
}, },
&mut || { &mut || {
if let Some(t) = T::from_eof() { if let Some(t) = self.parser.from_eof() {
self.sender.send(t).unwrap(); self.sender.send(t).unwrap();
} }
}, },
@ -116,8 +125,12 @@ impl<T> fmt::Debug for CommandHandle<T> {
} }
} }
impl<T: ParseFromLine> CommandHandle<T> { impl<T: Sized + Send + 'static> CommandHandle<T> {
pub(crate) fn spawn(mut command: Command, sender: Sender<T>) -> std::io::Result<Self> { pub(crate) fn spawn(
mut command: Command,
parser: impl CargoParser<T>,
sender: Sender<T>,
) -> std::io::Result<Self> {
command.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null()); command.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null());
let program = command.get_program().into(); let program = command.get_program().into();
@ -134,7 +147,7 @@ impl<T: ParseFromLine> CommandHandle<T> {
let stdout = child.0.stdout().take().unwrap(); let stdout = child.0.stdout().take().unwrap();
let stderr = child.0.stderr().take().unwrap(); let stderr = child.0.stderr().take().unwrap();
let actor = CargoActor::<T>::new(sender, stdout, stderr); let actor = CargoActor::<T>::new(parser, sender, stdout, stderr);
let thread = stdx::thread::Builder::new(stdx::thread::ThreadIntent::Worker) let thread = stdx::thread::Builder::new(stdx::thread::ThreadIntent::Worker)
.name("CommandHandle".to_owned()) .name("CommandHandle".to_owned())
.spawn(move || actor.run()) .spawn(move || actor.run())

View file

@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use tracing::{info_span, span::EnteredSpan}; use tracing::{info_span, span::EnteredSpan};
use crate::command::{CommandHandle, ParseFromLine}; use crate::command::{CargoParser, CommandHandle};
pub(crate) const ARG_PLACEHOLDER: &str = "{arg}"; pub(crate) const ARG_PLACEHOLDER: &str = "{arg}";
@ -66,7 +66,7 @@ impl DiscoverCommand {
cmd.args(args); cmd.args(args);
Ok(DiscoverHandle { Ok(DiscoverHandle {
_handle: CommandHandle::spawn(cmd, self.sender.clone())?, _handle: CommandHandle::spawn(cmd, DiscoverProjectParser, self.sender.clone())?,
span: info_span!("discover_command").entered(), span: info_span!("discover_command").entered(),
}) })
} }
@ -115,8 +115,10 @@ impl DiscoverProjectMessage {
} }
} }
impl ParseFromLine for DiscoverProjectMessage { struct DiscoverProjectParser;
fn from_line(line: &str, _error: &mut String) -> Option<Self> {
impl CargoParser<DiscoverProjectMessage> for DiscoverProjectParser {
fn from_line(&self, line: &str, _error: &mut String) -> Option<DiscoverProjectMessage> {
// can the line even be deserialized as JSON? // can the line even be deserialized as JSON?
let Ok(data) = serde_json::from_str::<Value>(line) else { let Ok(data) = serde_json::from_str::<Value>(line) else {
let err = DiscoverProjectData::Error { error: line.to_owned(), source: None }; let err = DiscoverProjectData::Error { error: line.to_owned(), source: None };
@ -131,7 +133,7 @@ impl ParseFromLine for DiscoverProjectMessage {
Some(msg) Some(msg)
} }
fn from_eof() -> Option<Self> { fn from_eof(&self) -> Option<DiscoverProjectMessage> {
None None
} }
} }

View file

@ -17,7 +17,7 @@ pub(crate) use cargo_metadata::diagnostic::{
use toolchain::Tool; use toolchain::Tool;
use triomphe::Arc; use triomphe::Arc;
use crate::command::{CommandHandle, ParseFromLine}; use crate::command::{CargoParser, CommandHandle};
#[derive(Clone, Debug, Default, PartialEq, Eq)] #[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) enum InvocationStrategy { pub(crate) enum InvocationStrategy {
@ -324,7 +324,7 @@ impl FlycheckActor {
tracing::debug!(?command, "will restart flycheck"); tracing::debug!(?command, "will restart flycheck");
let (sender, receiver) = unbounded(); let (sender, receiver) = unbounded();
match CommandHandle::spawn(command, sender) { match CommandHandle::spawn(command, CargoCheckParser, sender) {
Ok(command_handle) => { Ok(command_handle) => {
tracing::debug!(command = formatted_command, "did restart flycheck"); tracing::debug!(command = formatted_command, "did restart flycheck");
self.command_handle = Some(command_handle); self.command_handle = Some(command_handle);
@ -550,8 +550,10 @@ enum CargoCheckMessage {
Diagnostic { diagnostic: Diagnostic, package_id: Option<Arc<PackageId>> }, Diagnostic { diagnostic: Diagnostic, package_id: Option<Arc<PackageId>> },
} }
impl ParseFromLine for CargoCheckMessage { struct CargoCheckParser;
fn from_line(line: &str, error: &mut String) -> Option<Self> {
impl CargoParser<CargoCheckMessage> for CargoCheckParser {
fn from_line(&self, line: &str, error: &mut String) -> Option<CargoCheckMessage> {
let mut deserializer = serde_json::Deserializer::from_str(line); let mut deserializer = serde_json::Deserializer::from_str(line);
deserializer.disable_recursion_limit(); deserializer.disable_recursion_limit();
if let Ok(message) = JsonMessage::deserialize(&mut deserializer) { if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
@ -580,7 +582,7 @@ impl ParseFromLine for CargoCheckMessage {
None None
} }
fn from_eof() -> Option<Self> { fn from_eof(&self) -> Option<CargoCheckMessage> {
None None
} }
} }

View file

@ -1,25 +0,0 @@
//! Currently cargo does not emit crate name in the `cargo test --format=json`, which needs to be changed. This
//! module contains a way to recover crate names in a very hacky and wrong way.
// FIXME(hack_recover_crate_name): Remove this module.
use std::sync::{Mutex, MutexGuard, OnceLock};
use ide_db::FxHashMap;
static STORAGE: OnceLock<Mutex<FxHashMap<String, String>>> = OnceLock::new();
fn get_storage() -> MutexGuard<'static, FxHashMap<String, String>> {
STORAGE.get_or_init(|| Mutex::new(FxHashMap::default())).lock().unwrap()
}
pub(crate) fn insert_name(name_with_crate: String) {
let Some((_, name_without_crate)) = name_with_crate.split_once("::") else {
return;
};
get_storage().insert(name_without_crate.to_owned(), name_with_crate);
}
pub(crate) fn lookup_name(name_without_crate: String) -> Option<String> {
get_storage().get(&name_without_crate).cloned()
}

View file

@ -36,7 +36,6 @@ use crate::{
config::{Config, RustfmtConfig, WorkspaceSymbolConfig}, config::{Config, RustfmtConfig, WorkspaceSymbolConfig},
diagnostics::convert_diagnostic, diagnostics::convert_diagnostic,
global_state::{FetchWorkspaceRequest, GlobalState, GlobalStateSnapshot}, global_state::{FetchWorkspaceRequest, GlobalState, GlobalStateSnapshot},
hack_recover_crate_name,
line_index::LineEndings, line_index::LineEndings,
lsp::{ lsp::{
ext::{ ext::{
@ -196,20 +195,48 @@ pub(crate) fn handle_view_item_tree(
Ok(res) Ok(res)
} }
// cargo test requires the real package name which might contain hyphens but // cargo test requires:
// the test identifier passed to this function is the namespace form where hyphens // - the package name - the root of the test identifier supplied to this handler can be
// are replaced with underscores so we have to reverse this and find the real package name // a package or a target inside a package.
fn find_package_name(namespace_root: &str, cargo: &CargoWorkspace) -> Option<String> { // - the target name - if the test identifier is a target, it's needed in addition to the
// package name to run the right test
// - real names - the test identifier uses the namespace form where hyphens are replaced with
// underscores. cargo test requires the real name.
// - the target kind e.g. bin or lib
fn find_test_target(namespace_root: &str, cargo: &CargoWorkspace) -> Option<TestTarget> {
cargo.packages().find_map(|p| { cargo.packages().find_map(|p| {
let package_name = &cargo[p].name; let package_name = &cargo[p].name;
if package_name.replace('-', "_") == namespace_root { for target in cargo[p].targets.iter() {
Some(package_name.clone()) let target_name = &cargo[*target].name;
} else { if target_name.replace('-', "_") == namespace_root {
None return Some(TestTarget {
package: package_name.clone(),
target: target_name.clone(),
kind: cargo[*target].kind,
});
}
} }
None
}) })
} }
fn get_all_targets(cargo: &CargoWorkspace) -> Vec<TestTarget> {
cargo
.packages()
.flat_map(|p| {
let package_name = &cargo[p].name;
cargo[p].targets.iter().map(|target| {
let target_name = &cargo[*target].name;
TestTarget {
package: package_name.clone(),
target: target_name.clone(),
kind: cargo[*target].kind,
}
})
})
.collect()
}
pub(crate) fn handle_run_test( pub(crate) fn handle_run_test(
state: &mut GlobalState, state: &mut GlobalState,
params: lsp_ext::RunTestParams, params: lsp_ext::RunTestParams,
@ -217,53 +244,41 @@ pub(crate) fn handle_run_test(
if let Some(_session) = state.test_run_session.take() { if let Some(_session) = state.test_run_session.take() {
state.send_notification::<lsp_ext::EndRunTest>(()); state.send_notification::<lsp_ext::EndRunTest>(());
} }
// We detect the lowest common ancestor of all included tests, and
// run it. We ignore excluded tests for now, the client will handle
// it for us.
let lca = match params.include {
Some(tests) => tests
.into_iter()
.reduce(|x, y| {
let mut common_prefix = "".to_owned();
for (xc, yc) in x.chars().zip(y.chars()) {
if xc != yc {
break;
}
common_prefix.push(xc);
}
common_prefix
})
.unwrap_or_default(),
None => "".to_owned(),
};
let (namespace_root, test_path) = if lca.is_empty() {
(None, None)
} else if let Some((namespace_root, path)) = lca.split_once("::") {
(Some(namespace_root), Some(path))
} else {
(Some(lca.as_str()), None)
};
let mut handles = vec![]; let mut handles = vec![];
for ws in &*state.workspaces { for ws in &*state.workspaces {
if let ProjectWorkspaceKind::Cargo { cargo, .. } = &ws.kind { if let ProjectWorkspaceKind::Cargo { cargo, .. } = &ws.kind {
let test_target = if let Some(namespace_root) = namespace_root { // need to deduplicate `include` to avoid redundant test runs
if let Some(package_name) = find_package_name(namespace_root, cargo) { let tests = match params.include {
TestTarget::Package(package_name) Some(ref include) => include
} else { .iter()
TestTarget::Workspace .unique()
} .filter_map(|test| {
} else { let (root, remainder) = match test.split_once("::") {
TestTarget::Workspace Some((root, remainder)) => (root.to_owned(), Some(remainder)),
None => (test.clone(), None),
};
if let Some(target) = find_test_target(&root, cargo) {
Some((target, remainder))
} else {
tracing::error!("Test target not found for: {test}");
None
}
})
.collect_vec(),
None => get_all_targets(cargo).into_iter().map(|target| (target, None)).collect(),
}; };
let handle = CargoTestHandle::new( for (target, path) in tests {
test_path, let handle = CargoTestHandle::new(
state.config.cargo_test_options(None), path,
cargo.workspace_root(), state.config.cargo_test_options(None),
test_target, cargo.workspace_root(),
state.test_run_sender.clone(), target,
)?; state.test_run_sender.clone(),
handles.push(handle); )?;
handles.push(handle);
}
} }
} }
// Each process send finished signal twice, once for stdout and once for stderr // Each process send finished signal twice, once for stdout and once for stderr
@ -287,9 +302,7 @@ pub(crate) fn handle_discover_test(
} }
None => (snap.analysis.discover_test_roots()?, None), None => (snap.analysis.discover_test_roots()?, None),
}; };
for t in &tests {
hack_recover_crate_name::insert_name(t.id.clone());
}
Ok(lsp_ext::DiscoverTestResults { Ok(lsp_ext::DiscoverTestResults {
tests: tests tests: tests
.into_iter() .into_iter()

View file

@ -15,7 +15,6 @@ mod command;
mod diagnostics; mod diagnostics;
mod discover; mod discover;
mod flycheck; mod flycheck;
mod hack_recover_crate_name;
mod line_index; mod line_index;
mod main_loop; mod main_loop;
mod mem_docs; mod mem_docs;

View file

@ -26,7 +26,6 @@ use crate::{
file_id_to_url, url_to_file_id, FetchBuildDataResponse, FetchWorkspaceRequest, file_id_to_url, url_to_file_id, FetchBuildDataResponse, FetchWorkspaceRequest,
FetchWorkspaceResponse, GlobalState, FetchWorkspaceResponse, GlobalState,
}, },
hack_recover_crate_name,
handlers::{ handlers::{
dispatch::{NotificationDispatcher, RequestDispatcher}, dispatch::{NotificationDispatcher, RequestDispatcher},
request::empty_diagnostic_report, request::empty_diagnostic_report,
@ -37,7 +36,7 @@ use crate::{
}, },
lsp_ext, lsp_ext,
reload::{BuildDataProgress, ProcMacroProgress, ProjectWorkspaceProgress}, reload::{BuildDataProgress, ProcMacroProgress, ProjectWorkspaceProgress},
test_runner::{CargoTestMessage, TestState}, test_runner::{CargoTestMessage, CargoTestOutput, TestState},
}; };
pub fn main_loop(config: Config, connection: Connection) -> anyhow::Result<()> { pub fn main_loop(config: Config, connection: Connection) -> anyhow::Result<()> {
@ -659,9 +658,7 @@ impl GlobalState {
.filter_map(|f| snapshot.analysis.discover_tests_in_file(f).ok()) .filter_map(|f| snapshot.analysis.discover_tests_in_file(f).ok())
.flatten() .flatten()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for t in &tests {
hack_recover_crate_name::insert_name(t.id.clone());
}
Task::DiscoverTest(lsp_ext::DiscoverTestResults { Task::DiscoverTest(lsp_ext::DiscoverTestResults {
tests: tests tests: tests
.into_iter() .into_iter()
@ -958,30 +955,29 @@ impl GlobalState {
} }
fn handle_cargo_test_msg(&mut self, message: CargoTestMessage) { fn handle_cargo_test_msg(&mut self, message: CargoTestMessage) {
match message { match message.output {
CargoTestMessage::Test { name, state } => { CargoTestOutput::Test { name, state } => {
let state = match state { let state = match state {
TestState::Started => lsp_ext::TestState::Started, TestState::Started => lsp_ext::TestState::Started,
TestState::Ignored => lsp_ext::TestState::Skipped, TestState::Ignored => lsp_ext::TestState::Skipped,
TestState::Ok => lsp_ext::TestState::Passed, TestState::Ok => lsp_ext::TestState::Passed,
TestState::Failed { stdout } => lsp_ext::TestState::Failed { message: stdout }, TestState::Failed { stdout } => lsp_ext::TestState::Failed { message: stdout },
}; };
let Some(test_id) = hack_recover_crate_name::lookup_name(name) else { let test_id = format!("{}::{name}", message.target.target);
return;
};
self.send_notification::<lsp_ext::ChangeTestState>( self.send_notification::<lsp_ext::ChangeTestState>(
lsp_ext::ChangeTestStateParams { test_id, state }, lsp_ext::ChangeTestStateParams { test_id, state },
); );
} }
CargoTestMessage::Suite => (), CargoTestOutput::Suite => (),
CargoTestMessage::Finished => { CargoTestOutput::Finished => {
self.test_run_remaining_jobs = self.test_run_remaining_jobs.saturating_sub(1); self.test_run_remaining_jobs = self.test_run_remaining_jobs.saturating_sub(1);
if self.test_run_remaining_jobs == 0 { if self.test_run_remaining_jobs == 0 {
self.send_notification::<lsp_ext::EndRunTest>(()); self.send_notification::<lsp_ext::EndRunTest>(());
self.test_run_session = None; self.test_run_session = None;
} }
} }
CargoTestMessage::Custom { text } => { CargoTestOutput::Custom { text } => {
self.send_notification::<lsp_ext::AppendOutputToRunTest>(text); self.send_notification::<lsp_ext::AppendOutputToRunTest>(text);
} }
} }

View file

@ -3,12 +3,13 @@
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
use paths::AbsPath; use paths::AbsPath;
use project_model::TargetKind;
use serde::Deserialize as _; use serde::Deserialize as _;
use serde_derive::Deserialize; use serde_derive::Deserialize;
use toolchain::Tool; use toolchain::Tool;
use crate::{ use crate::{
command::{CommandHandle, ParseFromLine}, command::{CargoParser, CommandHandle},
flycheck::CargoOptions, flycheck::CargoOptions,
}; };
@ -25,9 +26,15 @@ pub(crate) enum TestState {
}, },
} }
#[derive(Debug)]
pub(crate) struct CargoTestMessage {
pub target: TestTarget,
pub output: CargoTestOutput,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
pub(crate) enum CargoTestMessage { pub(crate) enum CargoTestOutput {
Test { Test {
name: String, name: String,
#[serde(flatten)] #[serde(flatten)]
@ -40,19 +47,33 @@ pub(crate) enum CargoTestMessage {
}, },
} }
impl ParseFromLine for CargoTestMessage { pub(crate) struct CargoTestOutputParser {
fn from_line(line: &str, _: &mut String) -> Option<Self> { pub target: TestTarget,
}
impl CargoTestOutputParser {
pub(crate) fn new(test_target: &TestTarget) -> Self {
Self { target: test_target.clone() }
}
}
impl CargoParser<CargoTestMessage> for CargoTestOutputParser {
fn from_line(&self, line: &str, _error: &mut String) -> Option<CargoTestMessage> {
let mut deserializer = serde_json::Deserializer::from_str(line); let mut deserializer = serde_json::Deserializer::from_str(line);
deserializer.disable_recursion_limit(); deserializer.disable_recursion_limit();
if let Ok(message) = CargoTestMessage::deserialize(&mut deserializer) {
return Some(message);
}
Some(CargoTestMessage::Custom { text: line.to_owned() }) Some(CargoTestMessage {
target: self.target.clone(),
output: if let Ok(message) = CargoTestOutput::deserialize(&mut deserializer) {
message
} else {
CargoTestOutput::Custom { text: line.to_owned() }
},
})
} }
fn from_eof() -> Option<Self> { fn from_eof(&self) -> Option<CargoTestMessage> {
Some(CargoTestMessage::Finished) Some(CargoTestMessage { target: self.target.clone(), output: CargoTestOutput::Finished })
} }
} }
@ -62,14 +83,14 @@ pub(crate) struct CargoTestHandle {
} }
// Example of a cargo test command: // Example of a cargo test command:
// cargo test --workspace --no-fail-fast -- -Z unstable-options --format=json //
// or // cargo test --package my-package --bin my_bin --no-fail-fast -- module::func -Z unstable-options --format=json
// cargo test --package my-package --no-fail-fast -- module::func -Z unstable-options --format=json
#[derive(Debug)] #[derive(Debug, Clone)]
pub(crate) enum TestTarget { pub(crate) struct TestTarget {
Workspace, pub package: String,
Package(String), pub target: String,
pub kind: TargetKind,
} }
impl CargoTestHandle { impl CargoTestHandle {
@ -84,15 +105,18 @@ impl CargoTestHandle {
cmd.env("RUSTC_BOOTSTRAP", "1"); cmd.env("RUSTC_BOOTSTRAP", "1");
cmd.arg("test"); cmd.arg("test");
match &test_target { cmd.arg("--package");
TestTarget::Package(package) => { cmd.arg(&test_target.package);
cmd.arg("--package");
cmd.arg(package); if let TargetKind::Lib { .. } = test_target.kind {
} // no name required with lib because there can only be one lib target per package
TestTarget::Workspace => { cmd.arg("--lib");
cmd.arg("--workspace"); } else if let Some(cargo_target) = test_target.kind.as_cargo_target() {
} cmd.arg(format!("--{cargo_target}"));
}; cmd.arg(&test_target.target);
} else {
tracing::warn!("Running test for unknown cargo target {:?}", test_target.kind);
}
// --no-fail-fast is needed to ensure that all requested tests will run // --no-fail-fast is needed to ensure that all requested tests will run
cmd.arg("--no-fail-fast"); cmd.arg("--no-fail-fast");
@ -110,6 +134,8 @@ impl CargoTestHandle {
cmd.arg(extra_arg); cmd.arg(extra_arg);
} }
Ok(Self { _handle: CommandHandle::spawn(cmd, sender)? }) Ok(Self {
_handle: CommandHandle::spawn(cmd, CargoTestOutputParser::new(&test_target), sender)?,
})
} }
} }