cargo hollywood
use cargo metadata
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | src/cargo.rs | 228 | ||||
| -rw-r--r-- | src/test/mod.rs | 99 | ||||
| -rw-r--r-- | src/test/ui/inspector.rs | 25 | ||||
| -rw-r--r-- | src/test/ui/mod.rs | 2 | ||||
| -rw-r--r-- | src/test/ui/progress.rs | 13 | ||||
| -rw-r--r-- | src/test/ui/test_list.rs | 33 |
7 files changed, 104 insertions, 297 deletions
@@ -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(""); |