Skip to content

Commit b86d6b3

Browse files
committed
Add test explorer
1 parent 4a8d0f7 commit b86d6b3

File tree

16 files changed

+917
-166
lines changed

16 files changed

+917
-166
lines changed

crates/flycheck/src/command.rs

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
//! Utilities for running a cargo command like `cargo check` or `cargo test` in a separate thread and
2+
//! parse its stdout/stderr.
3+
4+
use std::{
5+
ffi::OsString,
6+
fmt, io,
7+
path::PathBuf,
8+
process::{ChildStderr, ChildStdout, Command, Stdio},
9+
};
10+
11+
use command_group::{CommandGroup, GroupChild};
12+
use crossbeam_channel::{unbounded, Receiver, Sender};
13+
use stdx::process::streaming_output;
14+
15+
/// Cargo output is structured as a one JSON per line. This trait abstracts parsing one line of
16+
/// cargo output into a Rust data type.
17+
pub(crate) trait ParseFromLine: Sized + Send + 'static {
18+
fn from_line(line: &str, error: &mut String) -> Option<Self>;
19+
fn from_eof() -> Option<Self>;
20+
}
21+
22+
struct CargoActor<T> {
23+
sender: Sender<T>,
24+
stdout: ChildStdout,
25+
stderr: ChildStderr,
26+
}
27+
28+
impl<T: ParseFromLine> CargoActor<T> {
29+
fn new(sender: Sender<T>, stdout: ChildStdout, stderr: ChildStderr) -> Self {
30+
CargoActor { sender, stdout, stderr }
31+
}
32+
33+
fn run(self) -> io::Result<(bool, String)> {
34+
// We manually read a line at a time, instead of using serde's
35+
// stream deserializers, because the deserializer cannot recover
36+
// from an error, resulting in it getting stuck, because we try to
37+
// be resilient against failures.
38+
//
39+
// Because cargo only outputs one JSON object per line, we can
40+
// simply skip a line if it doesn't parse, which just ignores any
41+
// erroneous output.
42+
43+
let mut stdout_errors = String::new();
44+
let mut stderr_errors = String::new();
45+
let mut read_at_least_one_stdout_message = false;
46+
let mut read_at_least_one_stderr_message = false;
47+
let process_line = |line: &str, error: &mut String| {
48+
// Try to deserialize a message from Cargo or Rustc.
49+
if let Some(t) = T::from_line(line, error) {
50+
self.sender.send(t).unwrap();
51+
true
52+
} else {
53+
false
54+
}
55+
};
56+
let output = streaming_output(
57+
self.stdout,
58+
self.stderr,
59+
&mut |line| {
60+
if process_line(line, &mut stdout_errors) {
61+
read_at_least_one_stdout_message = true;
62+
}
63+
},
64+
&mut |line| {
65+
if process_line(line, &mut stderr_errors) {
66+
read_at_least_one_stderr_message = true;
67+
}
68+
},
69+
&mut || {
70+
if let Some(t) = T::from_eof() {
71+
self.sender.send(t).unwrap();
72+
}
73+
},
74+
);
75+
76+
let read_at_least_one_message =
77+
read_at_least_one_stdout_message || read_at_least_one_stderr_message;
78+
let mut error = stdout_errors;
79+
error.push_str(&stderr_errors);
80+
match output {
81+
Ok(_) => Ok((read_at_least_one_message, error)),
82+
Err(e) => Err(io::Error::new(e.kind(), format!("{e:?}: {error}"))),
83+
}
84+
}
85+
}
86+
87+
struct JodGroupChild(GroupChild);
88+
89+
impl Drop for JodGroupChild {
90+
fn drop(&mut self) {
91+
_ = self.0.kill();
92+
_ = self.0.wait();
93+
}
94+
}
95+
96+
/// A handle to a cargo process used for fly-checking.
97+
pub(crate) struct CommandHandle<T> {
98+
/// The handle to the actual cargo process. As we cannot cancel directly from with
99+
/// a read syscall dropping and therefore terminating the process is our best option.
100+
child: JodGroupChild,
101+
thread: stdx::thread::JoinHandle<io::Result<(bool, String)>>,
102+
pub(crate) receiver: Receiver<T>,
103+
program: OsString,
104+
arguments: Vec<OsString>,
105+
current_dir: Option<PathBuf>,
106+
}
107+
108+
impl<T> fmt::Debug for CommandHandle<T> {
109+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110+
f.debug_struct("CommandHandle")
111+
.field("program", &self.program)
112+
.field("arguments", &self.arguments)
113+
.field("current_dir", &self.current_dir)
114+
.finish()
115+
}
116+
}
117+
118+
impl<T: ParseFromLine> CommandHandle<T> {
119+
pub(crate) fn spawn(mut command: Command) -> std::io::Result<Self> {
120+
command.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null());
121+
let mut child = command.group_spawn().map(JodGroupChild)?;
122+
123+
let program = command.get_program().into();
124+
let arguments = command.get_args().map(|arg| arg.into()).collect::<Vec<OsString>>();
125+
let current_dir = command.get_current_dir().map(|arg| arg.to_path_buf());
126+
127+
let stdout = child.0.inner().stdout.take().unwrap();
128+
let stderr = child.0.inner().stderr.take().unwrap();
129+
130+
let (sender, receiver) = unbounded();
131+
let actor = CargoActor::<T>::new(sender, stdout, stderr);
132+
let thread = stdx::thread::Builder::new(stdx::thread::ThreadIntent::Worker)
133+
.name("CargoHandle".to_owned())
134+
.spawn(move || actor.run())
135+
.expect("failed to spawn thread");
136+
Ok(CommandHandle { program, arguments, current_dir, child, thread, receiver })
137+
}
138+
139+
pub(crate) fn cancel(mut self) {
140+
let _ = self.child.0.kill();
141+
let _ = self.child.0.wait();
142+
}
143+
144+
pub(crate) fn join(mut self) -> io::Result<()> {
145+
let _ = self.child.0.kill();
146+
let exit_status = self.child.0.wait()?;
147+
let (read_at_least_one_message, error) = self.thread.join()?;
148+
if read_at_least_one_message || exit_status.success() {
149+
Ok(())
150+
} else {
151+
Err(io::Error::new(io::ErrorKind::Other, format!(
152+
"Cargo watcher failed, the command produced no valid metadata (exit code: {exit_status:?}):\n{error}"
153+
)))
154+
}
155+
}
156+
}

0 commit comments

Comments
 (0)