it: Add basic handlers

Signed-off-by: John Nunley <dev@notgull.net>
This commit is contained in:
John Nunley
2024-03-03 21:31:30 -08:00
parent 1bdba3cacc
commit b3333b47e1
6 changed files with 275 additions and 23 deletions

View File

@@ -275,5 +275,7 @@ license = "Apache-2.0"
edition = "2021"
[workspace.dependencies]
serde = { version = "1", features = ["serde_derive"] }
async-io = "2.3.1"
gui-test = { path = "it/gui-test" }
mint = "0.5.6"
serde = { version = "1", features = ["serde_derive"] }

View File

@@ -8,8 +8,10 @@ edition.workspace = true
[dependencies]
async-executor = "1.8.0"
async-io = "2.3.1"
async-io.workspace = true
async-lock = "3.3.0"
async-process = "2.1.0"
inventory = "0.3.15"
owo-colors = "4.0.0"
serde = { workspace = true, features = ["derive"] }
serde_json = "1.0.114"

View File

@@ -1,5 +1,9 @@
//! A testing framework that can be run remotely.
pub mod stream;
pub mod remote;
pub mod user;
use serde::{Deserialize, Serialize};
use std::env;
@@ -17,7 +21,7 @@ pub use inventory as __inventory;
/// Replacement for the `main` function.
#[macro_export]
macro_rules! main {
($harness:expr) => {
($handler:expr) => {
fn main() {
$crate::__entry($harness)
}
@@ -122,10 +126,7 @@ impl Harness {
}
// Send the "test started" event to the handler.
self.handler.handle_test(TestEvent {
runner: self.name.clone(),
ty: TestEventType::TestStarted { name: name.into() },
});
self.send_event(TestEventType::TestStarted { name: name.into() });
// Return the handle.
Testing {
@@ -156,10 +157,7 @@ impl Harness {
}
// Send the "group started" event to the handler.
self.handler.handle_test(TestEvent {
ty: TestEventType::GroupStarted { name: name.into() },
runner: self.name.clone(),
});
self.send_event(TestEventType::GroupStarted { name: name.into() });
// Return the handle.
Grouping { harness: self }
@@ -180,10 +178,7 @@ impl Harness {
_ => {}
}
self.handler.handle_test(TestEvent {
runner: self.name.clone(),
ty: TestEventType::TestEnded { result: reason },
});
self.send_event(TestEventType::TestEnded { result: reason });
let count = match mem::replace(&mut self.state, State::Default) {
State::InTest { past_groups } => past_groups,
@@ -198,10 +193,7 @@ impl Harness {
/// End the current group.
fn end_group(&mut self) {
self.handler.handle_test(TestEvent {
runner: self.name.clone(),
ty: TestEventType::GroupEnded,
});
self.send_event(TestEventType::GroupEnded);
let count = match mem::replace(&mut self.state, State::Default) {
State::InGroups(groups) => groups,
@@ -213,6 +205,26 @@ impl Harness {
Some(groups) => State::InGroups(groups),
};
}
/// Send a test event of the provided type.
fn send_event(&mut self, ty: TestEventType) {
let event = TestEvent {
runner: self.name.clone(),
ty,
};
self.handler.handle_test(event);
}
}
impl Drop for Harness {
fn drop(&mut self) {
self.send_event(TestEventType::Complete {
total: self.test_count,
fail: self.test_fails,
pass: self.test_passed,
});
}
}
/// An in-progress test.
@@ -279,14 +291,13 @@ pub trait TestHandler {
}
/// An event produced by the test harness.
#[non_exhaustive]
#[derive(Debug, Serialize, Deserialize)]
pub struct TestEvent {
/// The name of the runner associated with the event.
runner: String,
pub runner: String,
/// The type of the event.
ty: TestEventType,
pub ty: TestEventType,
}
/// The type of the event.
@@ -294,7 +305,16 @@ pub struct TestEvent {
#[derive(Debug, Serialize, Deserialize)]
pub enum TestEventType {
/// The tests are complete and the harness can be disconnected.
Complete,
Complete {
/// Total number of tests.
total: usize,
/// Total number of passing tests.
pass: usize,
/// Total number of failed tests.
fail: usize,
},
/// A test has started.
TestStarted { name: String },

30
it/gui-test/src/remote.rs Normal file
View File

@@ -0,0 +1,30 @@
//! Create a test handler that can be run remotely.
use crate::TestHandler;
use crate::stream::WriteHandler;
use crate::user::UserHandler;
use std::env;
use std::net::TcpStream;
/// Create a test handler adjusted for the current environment.
pub fn handler() -> Box<dyn TestHandler + Send + 'static> {
// If GUI_TEST_UNIX_STREAM is enabled, use that as a Unix stream.
#[cfg(unix)]
if let Some(stream_path) = env::var_os("GUI_TEST_UNIX_STREAM")
.filter(|s| !s.is_empty()) {
let stream = std::os::unix::net::UnixStream::connect(stream_path).expect("unable to connect to gui-test handler");
return Box::new(WriteHandler::new(stream));
}
// If GUI_TEST_TCP_STREAM is enabled, use that as a TCP stream.
if let Some(tcp_ip) = env::var("GUI_TEST_TCP_STREAM")
.ok()
.filter(|s| !s.is_empty()) {
let stream = TcpStream::connect(tcp_ip).unwrap();
return Box::new(WriteHandler::new(stream));
}
// By default, use the user handler.
Box::new(UserHandler::new())
}

34
it/gui-test/src/stream.rs Normal file
View File

@@ -0,0 +1,34 @@
//! Write events to an output stream.
//!
//! The format is as follows:
//! - First 8 bytes: big-endian length of payload.
//! - Next {len} bytes: JSON payload to deserialize from.
use crate::{TestEvent, TestHandler};
use std::io::Write;
/// A wrapper around a writer that sends data down a stream.
pub struct WriteHandler<W> {
/// The inner writer.
writer: W,
}
impl<W: Write> WriteHandler<W> {
/// Create a new write handler.
pub fn new(writer: W) -> Self {
Self {
writer
}
}
}
impl<W: Write> TestHandler for WriteHandler<W> {
fn handle_test(&mut self, event: TestEvent) {
let payload = serde_json::to_vec(&event).unwrap();
let length = u64::to_be_bytes(payload.len() as u64);
// Write the payload to the stream.
self.writer.write_all(&length).unwrap();
self.writer.write_all(&payload).unwrap();
}
}

164
it/gui-test/src/user.rs Normal file
View File

@@ -0,0 +1,164 @@
//! User-facing reporter.
use crate::{TestEvent, TestEventType, TestHandler, TestResult};
use owo_colors::OwoColorize;
use std::collections::BTreeMap;
use std::io::{self, prelude::*};
const TABSIZE: usize = 2;
/// User-facing reporter.
///
/// This reporter dumps events to the console in a user-readable format.
pub struct UserHandler {
/// Current indent.
indent: usize,
/// The test set we're currently displaying.
current_start: Option<String>,
/// Test name we are running, if any.
test_name: Option<String>,
/// Cached events.
cache: BTreeMap<String, Vec<TestEventType>>,
/// Failures we had.
failures: Vec<(String, String)>,
}
impl UserHandler {
/// Create a new handler.
pub fn new() -> Self {
Self {
indent: 0,
current_start: None,
test_name: None,
cache: BTreeMap::new(),
failures: vec![],
}
}
/// Process the provided events.
fn process_events(&mut self, events: impl IntoIterator<Item = TestEvent>) {
for event in events {
// Tell if this is an end event.
let mut ender = matches!(event.ty, TestEventType::Complete { .. });
// If there is no test name set, run the current one.
match self.current_start.as_ref() {
None => {
let TestEvent { runner, ty } = event;
self.current_start = Some(runner);
self.dump_events(Some(ty));
}
Some(test_name) => {
// If there is a test name set and it's ours, post it immediately.
if test_name == &event.runner {
self.dump_events(Some(event.ty));
} else {
// Add it to the back of another one of the events.
self.cache
.entry(test_name.clone())
.or_default()
.push(event.ty);
}
}
}
// If this is the end, dump other events.
while ender {
assert!(self.current_start.take().is_some());
// Pick one set.
if let Some(entry) = self.cache.first_entry() {
let (test_name, entries) = entry.remove_entry();
self.current_start = Some(test_name);
// Dump events and look for a conclusion.
ender = false;
self.dump_events(entries.into_iter().inspect(|ty| {
ender |= matches!(ty, TestEventType::Complete { .. });
}));
}
}
}
}
/// Dump the provided events to the console.
fn dump_events(&mut self, events: impl IntoIterator<Item = TestEventType>) {
let mut stdout = io::stdout().lock();
for event in events {
// Write the indent.
for _ in 0..(self.indent * TABSIZE) {
stdout.write_all(b" ").unwrap();
}
match event {
TestEventType::GroupStarted { name } => {
assert!(self.test_name.is_none());
// Write the group name and bump the indent.
writeln!(stdout, "{}", name.yellow().italic()).unwrap();
// Add to the indent.
self.indent += 1;
}
TestEventType::GroupEnded => {
assert!(self.test_name.is_none());
// Drop the indent.
self.indent = self.indent.checked_sub(1).unwrap();
}
TestEventType::TestStarted { name } => {
assert!(self.test_name.is_none());
// Write the line.
write!(stdout, "{} ", name.white().italic()).unwrap();
self.test_name = Some(name);
}
TestEventType::TestEnded { result } => {
let test_name = self.test_name.take().unwrap();
// Write the result.
match result {
TestResult::Passed => {
writeln!(stdout, "{}", "ok".green().bold()).unwrap();
}
TestResult::Failed(failure) => {
self.failures.push((test_name, failure));
writeln!(stdout, "{}", "FAIL".red().bold()).unwrap();
}
TestResult::Skipped => {
writeln!(stdout, "{}", "skipped".yellow().bold()).unwrap();
}
}
}
_ => {
// Completion.
}
}
}
}
}
impl TestHandler for UserHandler {
fn handle_test(&mut self, event: TestEvent) {
self.process_events(Some(event));
}
}
impl Drop for UserHandler {
fn drop(&mut self) {
assert!(self.cache.is_empty());
}
}