cargo hollywood
Diffstat (limited to 'src/cargo.rs')
-rw-r--r--src/cargo.rs255
1 files changed, 255 insertions, 0 deletions
diff --git a/src/cargo.rs b/src/cargo.rs
new file mode 100644
index 0000000..35007af
--- /dev/null
+++ b/src/cargo.rs
@@ -0,0 +1,255 @@
+use anyhow::{bail, Result};
+use crossbeam::channel::Sender;
+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),
+ Finished,
+}
+
+pub fn test(to: Sender<TestMessage>, at: Option<&Path>) -> Result<TestEvent> {
+ let mut proc = Command::new("cargo");
+ if let Some(at) = at {
+ proc.arg("-C");
+ proc.arg(at.as_os_str());
+ }
+ proc.args([
+ "-Zunstable-options",
+ "test",
+ "--",
+ "-Zunstable-options",
+ "--report-time",
+ "--show-output",
+ "--format",
+ "json",
+ ]);
+ log::trace!("running {proc:?}");
+ let mut proc = proc.stdout(Stdio::piped()).stderr(Stdio::null()).spawn()?;
+ 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)?;
+ 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();
+ 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);
+ }
+ b => tmp.push(b),
+ }
+ }
+ if let Some(exit) = proc.try_wait()? {
+ log::trace!("process died, we die");
+ bail!("process exited too early ({exit})");
+ }
+ 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???"),
+ },
+ })
+}
+
+#[derive(Deserialize)]
+pub struct Package {
+ pub name: String,
+}
+
+#[derive(Deserialize)]
+pub struct Metadata {
+ pub package: Package,
+}
+
+pub fn meta(at: &Path) -> Result<Metadata> {
+ Ok(toml::from_str(&std::fs::read_to_string(
+ 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 }
+ "#;
+ }
+}