mirror of
https://github.com/rust-windowing/winit.git
synced 2026-06-26 22:53:15 -04:00
it: Create the 'gui-test' crate
The 'gui-test' crate is intended to provide a test framework for process isolated and remote test cases. Like how I intend to test winit. Signed-off-by: John Nunley <dev@notgull.net>
This commit is contained in:
@@ -264,6 +264,7 @@ name = "window"
|
||||
resolver = "2"
|
||||
members = [
|
||||
"dpi",
|
||||
"it/gui-test",
|
||||
"run-wasm",
|
||||
]
|
||||
|
||||
|
||||
15
it/gui-test/Cargo.toml
Normal file
15
it/gui-test/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "gui-test"
|
||||
version = "0.1.0"
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-executor = "1.8.0"
|
||||
async-io = "2.3.1"
|
||||
async-lock = "3.3.0"
|
||||
async-process = "2.1.0"
|
||||
inventory = "0.3.15"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
391
it/gui-test/src/lib.rs
Normal file
391
it/gui-test/src/lib.rs
Normal file
@@ -0,0 +1,391 @@
|
||||
//! A testing framework that can be run remotely.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::env;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::mem;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
const GUI_TEST_CURRENT_TEST_NAME: &str = "GUI_TEST_CURRENT_TEST_NAME";
|
||||
const GUI_TEST_SUBPROCESS_LIMIT: &str = "GUI_TEST_SUBPROCESS_LIMIT";
|
||||
const DEFAULT_LIMIT: usize = 4;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use inventory as __inventory;
|
||||
|
||||
/// Replacement for the `main` function.
|
||||
#[macro_export]
|
||||
macro_rules! main {
|
||||
($harness:expr) => {
|
||||
fn main() {
|
||||
$crate::__entry($harness)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Set up a test for the test framework.
|
||||
#[macro_export]
|
||||
macro_rules! test {
|
||||
(
|
||||
$(#[$attr:meta])*
|
||||
fn $name:ident ($hname:ident : $htype:ty) $bl:block
|
||||
) => {
|
||||
const _: () = {
|
||||
$(#[$attr])*
|
||||
fn $name ($hname: $htype) $bl
|
||||
|
||||
$crate::__inventory::submit! {
|
||||
$crate::__TestStart::__new(
|
||||
stringify!($name),
|
||||
$name
|
||||
)
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/// Test start.
|
||||
#[doc(hidden)]
|
||||
pub struct __TestStart {
|
||||
/// The name of the test.
|
||||
name: &'static str,
|
||||
|
||||
/// The function to call.
|
||||
func: fn(&mut Harness),
|
||||
}
|
||||
|
||||
impl __TestStart {
|
||||
/// Create a new test start.
|
||||
#[doc(hidden)]
|
||||
pub fn __new(name: &'static str, func: fn(&mut Harness)) -> Self {
|
||||
Self { name, func }
|
||||
}
|
||||
}
|
||||
|
||||
inventory::collect! {
|
||||
__TestStart
|
||||
}
|
||||
|
||||
/// A harness for running the tests.
|
||||
pub struct Harness {
|
||||
/// Name of the test start.
|
||||
name: String,
|
||||
|
||||
/// The inner test handler.
|
||||
handler: Box<dyn TestHandler + Send + 'static>,
|
||||
|
||||
/// Number of tests that have been run so far.
|
||||
test_count: usize,
|
||||
|
||||
/// Number of tests that have failed so far.
|
||||
test_fails: usize,
|
||||
|
||||
/// Number of tests that have succeeded.
|
||||
test_passed: usize,
|
||||
|
||||
/// Current state of the test harness.
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl Harness {
|
||||
/// Create a new test harness.
|
||||
fn new<H: TestHandler + Send + 'static>(name: &str, handler: H) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
handler: Box::new(handler),
|
||||
test_count: 0,
|
||||
test_fails: 0,
|
||||
test_passed: 0,
|
||||
state: State::Default,
|
||||
}
|
||||
}
|
||||
|
||||
/// Begin a test.
|
||||
pub fn test(&mut self, name: impl Into<String>) -> Testing<'_> {
|
||||
// Make sure we aren't mid test.
|
||||
match mem::replace(&mut self.state, State::Default) {
|
||||
State::InTest { past_groups } => {
|
||||
self.state = State::InTest { past_groups };
|
||||
panic!("tried to start a test while another was underway");
|
||||
}
|
||||
|
||||
State::InGroups(groups) => {
|
||||
self.state = State::InTest {
|
||||
past_groups: Some(groups),
|
||||
};
|
||||
}
|
||||
|
||||
State::Default => {
|
||||
self.state = State::InTest { past_groups: None };
|
||||
}
|
||||
}
|
||||
|
||||
// Send the "test started" event to the handler.
|
||||
self.handler.handle_test(TestEvent {
|
||||
runner: self.name.clone(),
|
||||
ty: TestEventType::TestStarted { name: name.into() },
|
||||
});
|
||||
|
||||
// Return the handle.
|
||||
Testing {
|
||||
harness: Some(self),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a closure as a test.
|
||||
pub fn with_test<T>(&mut self, name: impl Into<String>, f: impl FnOnce() -> T) -> T {
|
||||
let _test = self.test(name.into());
|
||||
f()
|
||||
}
|
||||
|
||||
/// Begin a test group.
|
||||
pub fn group(&mut self, name: impl Into<String>) -> Grouping<'_> {
|
||||
// Make sure we can begin a group.
|
||||
match mem::replace(&mut self.state, State::Default) {
|
||||
State::Default => {
|
||||
self.state = State::InGroups(NonZeroUsize::new(1).unwrap());
|
||||
}
|
||||
State::InGroups(groups) => {
|
||||
self.state = State::InGroups(groups.checked_add(1).unwrap());
|
||||
}
|
||||
State::InTest { past_groups } => {
|
||||
self.state = State::InTest { past_groups };
|
||||
panic!("cannot start group mid-test")
|
||||
}
|
||||
}
|
||||
|
||||
// Send the "group started" event to the handler.
|
||||
self.handler.handle_test(TestEvent {
|
||||
ty: TestEventType::GroupStarted { name: name.into() },
|
||||
runner: self.name.clone(),
|
||||
});
|
||||
|
||||
// Return the handle.
|
||||
Grouping { harness: self }
|
||||
}
|
||||
|
||||
/// Run a closure inside of a group.
|
||||
pub fn with_group<T>(&mut self, name: impl Into<String>, f: impl FnOnce(&mut Self) -> T) -> T {
|
||||
let mut group = self.group(name.into());
|
||||
f(group.harness())
|
||||
}
|
||||
|
||||
/// End an ongoing test.
|
||||
fn end_test(&mut self, reason: TestResult) {
|
||||
self.test_count += 1;
|
||||
match &reason {
|
||||
TestResult::Passed => self.test_passed += 1,
|
||||
TestResult::Failed(..) => self.test_fails += 1,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.handler.handle_test(TestEvent {
|
||||
runner: self.name.clone(),
|
||||
ty: TestEventType::TestEnded { result: reason },
|
||||
});
|
||||
|
||||
let count = match mem::replace(&mut self.state, State::Default) {
|
||||
State::InTest { past_groups } => past_groups,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.state = match count {
|
||||
None => State::Default,
|
||||
Some(count) => State::InGroups(count),
|
||||
};
|
||||
}
|
||||
|
||||
/// End the current group.
|
||||
fn end_group(&mut self) {
|
||||
self.handler.handle_test(TestEvent {
|
||||
runner: self.name.clone(),
|
||||
ty: TestEventType::GroupEnded,
|
||||
});
|
||||
|
||||
let count = match mem::replace(&mut self.state, State::Default) {
|
||||
State::InGroups(groups) => groups,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.state = match NonZeroUsize::new(count.get() - 1) {
|
||||
None => State::Default,
|
||||
Some(groups) => State::InGroups(groups),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// An in-progress test.
|
||||
pub struct Testing<'a> {
|
||||
harness: Option<&'a mut Harness>,
|
||||
}
|
||||
|
||||
impl Testing<'_> {
|
||||
/// Skip this test.
|
||||
pub fn skip(mut self) {
|
||||
// Send the "skipped" event.
|
||||
self.harness.take().unwrap().end_test(TestResult::Skipped);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Testing<'_> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(harness) = self.harness.take() {
|
||||
let result = if std::thread::panicking() {
|
||||
TestResult::Failed("thread panicked".into())
|
||||
} else {
|
||||
TestResult::Passed
|
||||
};
|
||||
|
||||
harness.end_test(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// We are running a test group.
|
||||
pub struct Grouping<'a> {
|
||||
harness: &'a mut Harness,
|
||||
}
|
||||
|
||||
impl Grouping<'_> {
|
||||
/// Get the underlying test harness.
|
||||
pub fn harness(&mut self) -> &mut Harness {
|
||||
&mut self.harness
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Grouping<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.harness.end_group();
|
||||
}
|
||||
}
|
||||
|
||||
/// Current testing state.
|
||||
enum State {
|
||||
/// We are in the middle of this many groups.
|
||||
InGroups(NonZeroUsize),
|
||||
|
||||
/// We are in the middle of a test.
|
||||
InTest { past_groups: Option<NonZeroUsize> },
|
||||
|
||||
/// We are in the default state.
|
||||
Default,
|
||||
}
|
||||
|
||||
/// A handler for incoming test events.
|
||||
pub trait TestHandler {
|
||||
/// Handle a test.
|
||||
fn handle_test(&mut self, event: TestEvent);
|
||||
}
|
||||
|
||||
/// 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,
|
||||
|
||||
/// The type of the event.
|
||||
ty: TestEventType,
|
||||
}
|
||||
|
||||
/// The type of the event.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum TestEventType {
|
||||
/// The tests are complete and the harness can be disconnected.
|
||||
Complete,
|
||||
|
||||
/// A test has started.
|
||||
TestStarted { name: String },
|
||||
|
||||
/// A test has completed.
|
||||
TestEnded { result: TestResult },
|
||||
|
||||
/// A test group has started.
|
||||
GroupStarted { name: String },
|
||||
|
||||
/// A test group has ended.
|
||||
GroupEnded,
|
||||
}
|
||||
|
||||
/// The result of a test.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum TestResult {
|
||||
/// The test passed.
|
||||
Passed,
|
||||
|
||||
/// The test failed with the provided error.
|
||||
Failed(String),
|
||||
|
||||
/// The test was skipped.
|
||||
Skipped,
|
||||
}
|
||||
|
||||
/// Entry point of the test.
|
||||
#[doc(hidden)]
|
||||
pub fn __entry<H: TestHandler + Send + 'static>(handler: H) {
|
||||
// Look for the test name environment variable.
|
||||
if let Some(test_name) = env::var(GUI_TEST_CURRENT_TEST_NAME)
|
||||
.ok()
|
||||
.filter(|test_name| !test_name.is_empty())
|
||||
{
|
||||
// Find the provided test.
|
||||
let test_to_run = inventory::iter::<__TestStart>
|
||||
.into_iter()
|
||||
.find(|test| test.name == test_name)
|
||||
.unwrap_or_else(|| panic!("unable to find test '{test_name}'"));
|
||||
|
||||
// Create a harness.
|
||||
let mut harness = Harness::new(test_to_run.name, handler);
|
||||
|
||||
// Run the test.
|
||||
(test_to_run.func)(&mut harness);
|
||||
} else {
|
||||
// Run a subprocess for every test.
|
||||
let limit = env::var(GUI_TEST_SUBPROCESS_LIMIT)
|
||||
.ok()
|
||||
.and_then(|limit| limit.parse::<usize>().ok())
|
||||
.unwrap_or(DEFAULT_LIMIT);
|
||||
let process_name = env::args_os().next().unwrap();
|
||||
|
||||
let sema = async_lock::Semaphore::new(limit);
|
||||
let ex = async_executor::Executor::new();
|
||||
|
||||
async_io::block_on(ex.run(async {
|
||||
let mut tasks = vec![];
|
||||
|
||||
// Set up an environment variable for this.
|
||||
for test in inventory::iter::<__TestStart> {
|
||||
// Acquire a guard.
|
||||
let guard = sema.acquire().await;
|
||||
|
||||
// Spawn a subprocess.
|
||||
let mut process = async_process::Command::new(&process_name)
|
||||
.envs(env::vars_os().chain(Some({
|
||||
(path(&GUI_TEST_CURRENT_TEST_NAME), path(&test.name))
|
||||
})))
|
||||
.spawn()
|
||||
.expect("failed to spawn child process");
|
||||
|
||||
// Spawn a task to poll that subprocess.
|
||||
let task = ex.spawn(async move {
|
||||
let _guard = guard;
|
||||
process.status().await.unwrap()
|
||||
});
|
||||
|
||||
tasks.push(task);
|
||||
}
|
||||
|
||||
// Finish all of the tasks.
|
||||
for task in tasks {
|
||||
task.await;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn path<A: AsRef<OsStr>>(s: &A) -> OsString {
|
||||
s.as_ref().into()
|
||||
}
|
||||
Reference in New Issue
Block a user