cargo hollywood
use cargo metadata
bendn 2023-10-22
parent 839bce3 · commit 6d6d2cf
-rw-r--r--Cargo.toml1
-rw-r--r--src/cargo.rs228
-rw-r--r--src/test/mod.rs99
-rw-r--r--src/test/ui/inspector.rs25
-rw-r--r--src/test/ui/mod.rs2
-rw-r--r--src/test/ui/progress.rs13
-rw-r--r--src/test/ui/test_list.rs33
7 files changed, 104 insertions, 297 deletions
diff --git a/Cargo.toml b/Cargo.toml
index cf9dd55..e0f53aa 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,4 +21,5 @@ crossterm = "0.27.0"
serde = "1"
serde_derive = "1"
serde_json = "1"
+cargo_metadata = { version = "0.18.1", features = ["unstable"] }
toml = "0.8.2"
diff --git a/src/cargo.rs b/src/cargo.rs
index 35007af..d0ff7fa 100644
--- a/src/cargo.rs
+++ b/src/cargo.rs
@@ -1,92 +1,25 @@
-use anyhow::{bail, Result};
-use crossbeam::channel::Sender;
+use anyhow::Result;
+pub use cargo_metadata::{
+ libtest::SuiteEvent, libtest::TestEvent, Message, TestMessage as RawTestMessage,
+};
+use crossbeam::channel::bounded;
+use crossbeam::channel::Receiver;
use serde_derive::Deserialize;
use std::path::Path;
use std::{
- error::Error,
io::Read,
process::{Command, Stdio},
};
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "lowercase")]
-enum Type {
- Test,
- Suite,
-}
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "lowercase")]
-enum Event {
- Ok,
- #[serde(rename = "started")]
- Start,
- #[serde(rename = "failed")]
- Fail,
- #[serde(rename = "ignored")]
- Ignore,
-}
-
-#[derive(Deserialize, Debug)]
-// todo: figure out if theres a cool serde trick
-struct TestRaw {
- #[serde(rename = "type")]
- ty: Type,
- event: Event,
- name: Option<String>,
- passed: Option<usize>,
- failed: Option<usize>,
- ignored: Option<usize>,
- measured: Option<usize>,
- filtered_out: Option<usize>,
- test_count: Option<usize>,
- stdout: Option<String>,
- exec_time: Option<f32>,
-}
-
-#[derive(Debug, PartialEq)]
-pub struct TestResult {
- pub name: String,
- pub exec_time: f32,
- pub stdout: Option<String>,
-}
-
-#[derive(Debug, PartialEq)]
-pub enum TestEvent {
- SuiteStart {
- test_count: usize,
- },
- SuiteOk {
- failed: usize,
- passed: usize,
- ignored: usize,
- measured: usize,
- filtered_out: usize,
- exec_time: f32,
- },
- SuiteFail {
- passed: usize,
- failed: usize,
- ignored: usize,
- measured: usize,
- filtered_out: usize,
- exec_time: f32,
- },
- TestStart {
- name: String,
- },
- TestOk(TestResult),
- TestFail(TestResult),
- TestIgnore {
- name: String,
- },
-}
#[derive(Debug)]
pub enum TestMessage {
- Event(TestEvent),
+ CompilerEvent(Message),
+ Event(RawTestMessage),
Finished,
}
-pub fn test(to: Sender<TestMessage>, at: Option<&Path>) -> Result<TestEvent> {
+pub fn test(at: Option<&Path>) -> Result<Receiver<TestMessage>> {
+ let (tx, rx) = bounded(10);
let mut proc = Command::new("cargo");
if let Some(at) = at {
proc.arg("-C");
@@ -95,6 +28,8 @@ pub fn test(to: Sender<TestMessage>, at: Option<&Path>) -> Result<TestEvent> {
proc.args([
"-Zunstable-options",
"test",
+ "--message-format",
+ "json",
"--",
"-Zunstable-options",
"--report-time",
@@ -107,108 +42,34 @@ pub fn test(to: Sender<TestMessage>, at: Option<&Path>) -> Result<TestEvent> {
let mut out = proc.stdout.take().unwrap();
let mut tmp = Vec::with_capacity(32);
let mut stdout = [0; 4096];
- loop {
- let n = out.read(&mut stdout)?;
+
+ std::thread::spawn(move || loop {
+ let n = out.read(&mut stdout).unwrap();
for &byte in &stdout[..n] {
match byte {
b'\n' => {
- log::debug!("got first event, returning");
- let event = parse_test(std::str::from_utf8(&tmp).unwrap()).unwrap();
+ let val = serde_json::from_slice::<serde_json::Value>(&tmp).unwrap();
+ log::debug!("got val: {}", serde_json::to_string_pretty(&val).unwrap());
+ let event = match serde_json::value::from_value::<Message>(val.clone()) {
+ Err(_) => TestMessage::Event(
+ serde_json::value::from_value::<RawTestMessage>(val).unwrap(),
+ ),
+ Ok(v) => TestMessage::CompilerEvent(v),
+ };
tmp.clear();
- log::debug!("spawning thread");
- std::thread::spawn(move || loop {
- let n = out.read(&mut stdout).unwrap();
- for &byte in &stdout[..n] {
- match byte {
- b'\n' => {
- let event =
- parse_test(std::str::from_utf8(&tmp).unwrap()).unwrap();
- tmp.clear();
- to.send(TestMessage::Event(event)).unwrap();
- }
- b => tmp.push(b),
- }
- }
- if let Ok(Some(_)) = proc.try_wait() {
- to.send(TestMessage::Finished).unwrap();
- log::debug!("proc exited, joining thread");
- break;
- }
- std::thread::sleep(std::time::Duration::from_millis(50));
- });
- return Ok(event);
+ tx.send(event).unwrap();
}
b => tmp.push(b),
}
}
- if let Some(exit) = proc.try_wait()? {
- log::trace!("process died, we die");
- bail!("process exited too early ({exit})");
+ if let Ok(Some(_)) = proc.try_wait() {
+ tx.send(TestMessage::Finished).unwrap();
+ log::debug!("proc exited, joining thread");
+ break;
}
- std::thread::sleep(std::time::Duration::from_millis(10));
- }
-}
-
-#[derive(Debug)]
-struct Should(&'static str);
-impl std::fmt::Display for Should {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "should have had a {}, dang", self.0)
- }
-}
-impl Error for Should {}
-
-fn parse_test(s: &str) -> Result<TestEvent> {
- let raw = serde_json::from_str::<TestRaw>(s)?;
- log::trace!("got raw event {raw:?}");
- macro_rules! take {
- ($thing:ident { $($holds:ident),+ $(?= $($opt:ident),+)?}) => {
- $thing {
- $($holds: raw.$holds.ok_or(Should(stringify!($holds)))?,)+
- $($($opt: raw.$opt),+)?
- }
- };
- ($thing:ident($inner:ident { $($holds:ident),+ $(?= $($opt:ident),+)? })) => {
- $thing(take!($inner { $($holds),+ $(?= $($opt),+)? }))
- }
- }
- use TestEvent::*;
- Ok(match raw.ty {
- Type::Test => match raw.event {
- Event::Start => take!(TestStart { name }),
- Event::Ok => take!(TestOk(TestResult {
- name,
- exec_time ?= stdout
- })),
- Event::Fail => take!(TestFail(TestResult {
- name,
- exec_time ?= stdout
- })),
- Event::Ignore => take!(TestIgnore { name }),
- },
- Type::Suite => match raw.event {
- Event::Start => take!(SuiteStart { test_count }),
- Event::Ok => take!(SuiteOk {
- failed,
- passed,
- ignored,
- measured,
- filtered_out,
- exec_time
- }),
- Event::Fail => {
- take!(SuiteFail {
- failed,
- passed,
- ignored,
- measured,
- filtered_out,
- exec_time
- })
- }
- Event::Ignore => panic!("ignore suite???"),
- },
- })
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ });
+ return Ok(rx);
}
#[derive(Deserialize)]
@@ -226,30 +87,3 @@ pub fn meta(at: &Path) -> Result<Metadata> {
at.join("Cargo.toml"),
)?)?)
}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- #[test]
- fn test_output() {
- macro_rules! run {
- ($($input:literal parses to $output:expr),+) => {
- $(assert_eq!(parse_test($input).unwrap(), $output);)+
- };
- }
- run![
- r#"{ "type": "suite", "event": "started", "test_count": 2 }"# parses to TestEvent::SuiteStart { test_count: 2},
- r#"{ "type": "test", "event": "started", "name": "fail" }"# parses to TestEvent::TestStart { name: "fail".into() },
- r#"{ "type": "test", "name": "fail", "event": "ok", "exec_time": 0.000003428, "stdout": "hello world" }"# parses to TestEvent::TestOk(TestResult { name: "fail".into(), exec_time: 0.000003428, stdout: Some("hello world".into()) }),
- r#"{ "type": "test", "event": "started", "name": "nope" }"# parses to TestEvent::TestStart { name: "nope".into() },
- r#"{ "type": "test", "name": "nope", "event": "ignored" }"# parses to TestEvent::TestIgnore { name: "nope".into() },
- r#"{ "type": "suite", "event": "ok", "passed": 1, "failed": 0, "ignored": 1, "measured": 0, "filtered_out": 0, "exec_time": 0.000684028 }"# parses to TestEvent::SuiteOk { passed: 1, failed: 0, ignored: 1, measured: 0, filtered_out: 0, exec_time: 0.000684028 }
- ];
- r#"
- { "type": "suite", "event": "started", "test_count": 1 }
- { "type": "test", "event": "started", "name": "fail" }
- { "type": "test", "name": "fail", "event": "failed", "exec_time": 0.000081092, "stdout": "thread 'fail' panicked at src/main.rs:3:5:\nexplicit panic\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace\n" }
- { "type": "suite", "event": "failed", "passed": 0, "failed": 1, "ignored": 0, "measured": 0, "filtered_out": 0, "exec_time": 0.000731068 }
- "#;
- }
-}
diff --git a/src/test/mod.rs b/src/test/mod.rs
index 7b7d0bb..f5bdb47 100644
--- a/src/test/mod.rs
+++ b/src/test/mod.rs
@@ -1,6 +1,8 @@
mod ui;
use anyhow::Result;
-use crossbeam::channel::{unbounded, Receiver};
+use cargo_metadata::libtest::SuiteEvent;
+use cargo_metadata::TestMessage as RTestMessage;
+use crossbeam::channel::Receiver;
use crossterm::event::{self, Event, KeyCode};
use ratatui::prelude::*;
use ratatui::Terminal;
@@ -8,7 +10,7 @@ use std::path::Path;
use std::time::{Duration, Instant};
use crate::cargo;
-use crate::cargo::{test, TestEvent, TestMessage, TestResult};
+use crate::cargo::{test, TestEvent, TestMessage};
use crate::test::ui::stdout::Stdout;
#[derive(Default, PartialEq, Eq)]
@@ -18,44 +20,8 @@ pub enum Screen {
Stdout,
}
-enum Test {
- InProgress { name: String },
- Succeeded(TestResult),
- Failed(TestResult),
- Ignored { name: String },
-}
-
-impl Test {
- fn name(&self) -> &str {
- let (Self::InProgress { name }
- | Self::Succeeded(TestResult { name, .. })
- | Self::Failed(TestResult { name, .. })
- | Self::Ignored { name }) = self;
- name
- }
-
- fn stdout(&self) -> Option<&str> {
- match self {
- Self::Succeeded(TestResult { stdout, .. })
- | Self::Failed(TestResult { stdout, .. }) => stdout.as_deref(),
- _ => None,
- }
- }
-}
-
-trait VAt {
- fn at(&mut self, which: &str) -> Option<&mut Test>;
-}
-
-impl VAt for Vec<Test> {
- fn at(&mut self, which: &str) -> Option<&mut Test> {
- let p = self.iter().position(|t| t.name() == which)?;
- Some(&mut self[p])
- }
-}
-
pub struct TestState {
- tests: Vec<Test>,
+ tests: Vec<TestEvent>, // use the event like a state (ok => in progress, ..)
test_list: ui::test_list::TestList,
rx: Receiver<TestMessage>,
screen: Screen,
@@ -68,17 +34,14 @@ pub struct TestState {
impl TestState {
pub fn new(dir: Option<&Path>) -> Result<Self> {
log::info!("initializing test state");
- let (tx, rx) = unbounded();
- let TestEvent::SuiteStart { test_count } = test(tx, dir)? else {
- panic!("first ev should be suite start")
- };
+ let rx = test(dir)?;
Ok(Self {
test_list: ui::test_list::TestList::default(),
tests: vec![],
rx,
screen: Screen::default(),
done: false,
- test_count,
+ test_count: 0,
time: 0.,
stdout: Stdout::default(),
})
@@ -97,30 +60,34 @@ impl TestState {
self.done = true;
return;
}
+ TestMessage::CompilerEvent(c) => {
+ return;
+ }
};
match event {
- TestEvent::TestStart { name } => {
- self.tests.push(Test::InProgress { name });
- }
- TestEvent::TestOk(r) => {
- let pre = self.tests.at(&r.name).unwrap();
- *pre = Test::Succeeded(r);
- }
- TestEvent::TestFail(r) => {
- let pre = self.tests.at(&r.name).unwrap();
- *pre = Test::Failed(r);
- }
- TestEvent::TestIgnore { name } => {
- let pre = self.tests.at(&name).unwrap();
- *pre = Test::Ignored { name };
- }
- TestEvent::SuiteOk { exec_time, .. } | TestEvent::SuiteFail { exec_time, .. } => {
- self.time += exec_time;
- }
- TestEvent::SuiteStart { test_count } => {
- log::trace!("have {test_count} tests");
- self.test_count += test_count;
- }
+ RTestMessage::Test(t) => match t {
+ TestEvent::Started { name } => {
+ self.tests.push(TestEvent::Started { name });
+ }
+ t => {
+ let i = self
+ .tests
+ .iter()
+ .position(|o| o.name() == t.name())
+ .unwrap();
+ self.tests[i] = t;
+ }
+ },
+ RTestMessage::Suite(s) => match s {
+ SuiteEvent::Ok { exec_time, .. } | SuiteEvent::Failed { exec_time, .. } => {
+ self.time += exec_time;
+ }
+ SuiteEvent::Started { test_count } => {
+ log::trace!("have {test_count} tests");
+ self.test_count += test_count;
+ }
+ },
+ RTestMessage::Bench { .. } => unreachable!("not applicable"),
};
}
}
diff --git a/src/test/ui/inspector.rs b/src/test/ui/inspector.rs
index 5c2b502..f535cf4 100644
--- a/src/test/ui/inspector.rs
+++ b/src/test/ui/inspector.rs
@@ -7,9 +7,9 @@ use ratatui::{
};
use crate::{
- cargo::TestResult,
+ cargo::TestEvent,
ctext,
- test::{Screen, Test, TestState},
+ test::{Screen, TestState},
};
pub fn inspector<B: Backend>(f: &mut Frame<B>, state: &TestState, chunk: Rect) {
@@ -25,7 +25,7 @@ pub fn inspector<B: Backend>(f: &mut Frame<B>, state: &TestState, chunk: Rect) {
b
};
match t {
- Test::Ignored { name } => {
+ TestEvent::Ignored { name } => {
f.render_widget(
Paragraph::new(ctext!("test {:bold_yellow} was ignored", name))
.alignment(Alignment::Center)
@@ -34,7 +34,16 @@ pub fn inspector<B: Backend>(f: &mut Frame<B>, state: &TestState, chunk: Rect) {
chunk,
);
}
- Test::Failed(TestResult { name, stdout, .. }) => {
+ TestEvent::Timeout { name } => {
+ f.render_widget(
+ Paragraph::new(ctext!("test {:bold_red} timed out", name))
+ .alignment(Alignment::Center)
+ .block(b)
+ .wrap(Wrap { trim: true }),
+ chunk,
+ );
+ }
+ TestEvent::Failed { name, stdout, .. } => {
if let Some(stdout) = stdout {
let chunks = Layout::new()
.direction(Vertical)
@@ -47,7 +56,7 @@ pub fn inspector<B: Backend>(f: &mut Frame<B>, state: &TestState, chunk: Rect) {
chunks[0],
);
f.render_widget(
- Paragraph::new(<String as ansi_to_tui::IntoText>::into_text(stdout).unwrap())
+ Paragraph::new(<String as ansi_to_tui::IntoText>::into_text(&stdout).unwrap())
.block(stdblock())
.scroll((state.stdout.scroll, 0)),
chunks[1],
@@ -62,7 +71,7 @@ pub fn inspector<B: Backend>(f: &mut Frame<B>, state: &TestState, chunk: Rect) {
);
}
}
- Test::Succeeded(TestResult { name, stdout, .. }) => {
+ TestEvent::Ok { name, stdout, .. } => {
if let Some(stdout) = stdout {
let chunks = Layout::new()
.direction(Vertical)
@@ -75,7 +84,7 @@ pub fn inspector<B: Backend>(f: &mut Frame<B>, state: &TestState, chunk: Rect) {
chunks[0],
);
f.render_widget(
- Paragraph::new(<String as ansi_to_tui::IntoText>::into_text(stdout).unwrap())
+ Paragraph::new(<String as ansi_to_tui::IntoText>::into_text(&stdout).unwrap())
.block(stdblock())
.scroll((state.stdout.scroll, 0)),
chunks[1],
@@ -90,7 +99,7 @@ pub fn inspector<B: Backend>(f: &mut Frame<B>, state: &TestState, chunk: Rect) {
);
}
}
- Test::InProgress { name } => {
+ TestEvent::Started { name } => {
f.render_widget(
Paragraph::new(ctext!("test {:bold_yellow} in progress", name))
.alignment(Alignment::Center)
diff --git a/src/test/ui/mod.rs b/src/test/ui/mod.rs
index 40fcb59..8955f9d 100644
--- a/src/test/ui/mod.rs
+++ b/src/test/ui/mod.rs
@@ -13,7 +13,7 @@ mod ls;
mod progress;
pub mod stdout;
pub mod test_list;
-use super::{Screen, Test};
+use super::Screen;
use crate::cargo;
use crate::ctext;
diff --git a/src/test/ui/progress.rs b/src/test/ui/progress.rs
index d30c755..c8a82e7 100644
--- a/src/test/ui/progress.rs
+++ b/src/test/ui/progress.rs
@@ -4,10 +4,7 @@ use ratatui::{
Frame,
};
-use crate::{
- ctext,
- test::{Test, TestState},
-};
+use crate::{cargo::TestEvent, ctext, test::TestState};
pub fn progress<B: Backend>(f: &mut Frame<B>, state: &TestState, chunk: Rect) {
let size =
@@ -19,10 +16,10 @@ pub fn progress<B: Backend>(f: &mut Frame<B>, state: &TestState, chunk: Rect) {
let mut running = 0;
for test in &state.tests {
match test {
- Test::Succeeded(_) => passing += 1,
- Test::Ignored { .. } => ignored += 1,
- Test::Failed(_) => failing += 1,
- Test::InProgress { .. } => running += 1,
+ TestEvent::Ok { .. } => passing += 1,
+ TestEvent::Ignored { .. } => ignored += 1,
+ TestEvent::Failed { .. } | TestEvent::Timeout { .. } => failing += 1,
+ TestEvent::Started { .. } => running += 1,
}
}
let progress = Paragraph::new(ctext!(
diff --git a/src/test/ui/test_list.rs b/src/test/ui/test_list.rs
index c4d9da2..79d98b6 100644
--- a/src/test/ui/test_list.rs
+++ b/src/test/ui/test_list.rs
@@ -1,8 +1,6 @@
-use crate::cargo::TestResult;
-use crate::test::TestState;
-
use super::ls::SList;
-use super::Test;
+use crate::cargo::TestEvent;
+use crate::test::TestState;
use ratatui::{
layout::{Constraint::Percentage, Direction::Horizontal},
prelude::*,
@@ -45,7 +43,7 @@ impl TestList {
self.all().map(SList::prev);
}
- pub fn selects<'a>(&'a self, state: &'a TestState) -> Option<&Test> {
+ pub fn selects<'a>(&'a self, state: &'a TestState) -> Option<&TestEvent> {
state.tests.get(self.a.state.selected()?)
}
@@ -69,30 +67,31 @@ pub fn test_list<B: Backend>(f: &mut Frame<B>, state: &mut TestState, chunk: Rec
}
for test in &state.tests {
match test {
- Test::InProgress { name } => {
+ TestEvent::Started { name } => {
tests.pl(name.bold().yellow());
test_side1.pl("in progress".yellow().italic());
test_side2.pl("");
}
- Test::Succeeded(TestResult {
- name,
- exec_time,
- stdout: _,
- }) => {
+ TestEvent::Ok {
+ name, exec_time, ..
+ } => {
tests.pl(name.bold().green());
test_side1.pl("passed".green().italic());
test_side2.pl(time(*exec_time));
}
- Test::Failed(TestResult {
- name,
- exec_time,
- stdout: _,
- }) => {
+ TestEvent::Failed {
+ name, exec_time, ..
+ } => {
tests.pl(name.bold().red());
test_side1.pl("failed".red().bold().italic());
test_side2.pl(time(*exec_time));
}
- Test::Ignored { name } => {
+ TestEvent::Timeout { name } => {
+ tests.pl(name.bold().red());
+ test_side1.pl("timed out".red().bold().italic());
+ test_side2.pl("");
+ }
+ TestEvent::Ignored { name } => {
tests.pl(name.bold().yellow());
test_side1.pl("ignored".yellow().italic());
test_side2.pl("");