mirror of
https://github.com/rust-windowing/winit.git
synced 2026-06-26 22:53:15 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68acedcdda | ||
|
|
facef799d3 | ||
|
|
25fab64f6e | ||
|
|
b3333b47e1 | ||
|
|
1bdba3cacc |
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -32,3 +32,6 @@
|
||||
# Orbital (Redox OS)
|
||||
/src/platform/orbital.rs @jackpot51
|
||||
/src/platform_impl/orbital @jackpot51
|
||||
|
||||
# Integration tests
|
||||
/it @notgull
|
||||
|
||||
35
.github/workflows/ci.yml
vendored
35
.github/workflows/ci.yml
vendored
@@ -202,6 +202,41 @@ jobs:
|
||||
~/.cargo/git/db/
|
||||
key: cargo-${{ matrix.toolchain }}-${{ matrix.platform.name }}-${{ hashFiles('Cargo.lock') }}
|
||||
|
||||
it:
|
||||
name: Run integration tests on ${{ matrix.platform.name }}
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
toolchain: [stable, nightly]
|
||||
platform:
|
||||
# Note: Make sure that we test all the `docs.rs` targets defined in Cargo.toml!
|
||||
- { name: 'X11', target: x86_64-unknown-linux-gnu, os: ubuntu-latest, options: '--no-default-features --features=x11' }
|
||||
|
||||
env:
|
||||
# Set more verbose terminal output
|
||||
CARGO_TERM_VERBOSE: true
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
# Faster compilation and error on warnings
|
||||
RUSTFLAGS: '--codegen=debuginfo=0 --deny=warnings ${{ matrix.platform.rustflags }}'
|
||||
|
||||
OPTIONS: --target=${{ matrix.platform.target }} ${{ matrix.platform.options }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
|
||||
- name: Log into GHCR
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||
|
||||
- name: Common tests
|
||||
run: cargo run -p gui-test-runner -- common-tests ${{ matrix.platform.target }}
|
||||
|
||||
cargo-deny:
|
||||
name: Run cargo-deny on ${{ matrix.platform.name }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -264,6 +264,9 @@ name = "window"
|
||||
resolver = "2"
|
||||
members = [
|
||||
"dpi",
|
||||
"it/common-tests",
|
||||
"it/gui-test",
|
||||
"it/gui-test-runner",
|
||||
"run-wasm",
|
||||
]
|
||||
|
||||
@@ -274,5 +277,9 @@ 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"] }
|
||||
serde_json = "1.0.114"
|
||||
winit = { path = "." }
|
||||
|
||||
40
dockerfiles/Dockerfile.ubuntu
Normal file
40
dockerfiles/Dockerfile.ubuntu
Normal file
@@ -0,0 +1,40 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Copyright 2024 The Winit Contributors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
ARG DISTRO=ubuntu
|
||||
ARG DISTRO_VERSION=22.04
|
||||
|
||||
FROM "${DISTRO}":"${DISTRO_VERSION}"
|
||||
SHELL ["/bin/bash", "-eEuxo", "pipefail", "-c"]
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN \
|
||||
apt-get -o Acquire::Retries=10 -qq update && \
|
||||
apt-get -o Acquire::Retries=10 -o Dpkg::Use-Pty=0 install -y --no-install-recommends \
|
||||
cargo \
|
||||
ca-certificates \
|
||||
libx11-dev \
|
||||
libxcursor-dev \
|
||||
libxcb1-dev \
|
||||
libxi-dev \
|
||||
libxkbcommon-dev \
|
||||
libxkbcommon-x11-dev \
|
||||
xvfb && \
|
||||
rm -rf \
|
||||
/var/lib/apt/lists/* \
|
||||
/var/cache/* \
|
||||
/var/log/* \
|
||||
/usr/share/{doc,man}
|
||||
|
||||
12
it/common-tests/Cargo.toml
Normal file
12
it/common-tests/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "common-tests"
|
||||
version = "0.1.0"
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
gui-test.workspace = true
|
||||
macro_rules_attribute = "0.2.0"
|
||||
winit.workspace = true
|
||||
23
it/common-tests/src/main.rs
Normal file
23
it/common-tests/src/main.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
//! Run the test.
|
||||
|
||||
use gui_test::{test, Harness};
|
||||
use macro_rules_attribute::apply;
|
||||
|
||||
use winit::event_loop::EventLoop;
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[apply(test)]
|
||||
fn initialize(harness: &mut Harness) {
|
||||
let mut group = harness.group("sanity");
|
||||
group.harness().with_test("startup/shutdown", || {
|
||||
let evl = EventLoop::new().expect("initialization");
|
||||
evl.run(|_event, elwt| {
|
||||
elwt.exit();
|
||||
})
|
||||
.expect("running");
|
||||
});
|
||||
}
|
||||
|
||||
gui_test::main! {
|
||||
gui_test::remote::handler()
|
||||
}
|
||||
13
it/gui-test-runner/Cargo.toml
Normal file
13
it/gui-test-runner/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "gui-test-runner"
|
||||
version = "0.1.0"
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
camino = "1.1.6"
|
||||
fastrand = "2.0.1"
|
||||
gui-test.workspace = true
|
||||
serde_json.workspace = true
|
||||
81
it/gui-test-runner/src/command.rs
Normal file
81
it/gui-test-runner/src/command.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright 2024 The Winit Contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! A wrapper around the `Command` type that dumps the command to stderr.
|
||||
//!
|
||||
//! Essentially it's like `set -x` in Bash.
|
||||
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::io::{self, prelude::*};
|
||||
use std::process::Child;
|
||||
|
||||
/// Simple `Command` wrapper.
|
||||
pub(super) struct Command {
|
||||
/// Actual inner command.
|
||||
inner: std::process::Command,
|
||||
|
||||
/// Command to run.
|
||||
text: Vec<OsString>,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Create a new `Command`.
|
||||
pub(super) fn new(cmd: impl AsRef<OsStr>) -> Self {
|
||||
let cmd = cmd.as_ref();
|
||||
Self {
|
||||
inner: std::process::Command::new(cmd),
|
||||
text: vec![cmd.to_os_string()],
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an argument to the `Command`.
|
||||
pub(super) fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
|
||||
let arg = arg.as_ref();
|
||||
self.inner.arg(arg);
|
||||
self.text.push(arg.to_os_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple arguments to the `Command`.
|
||||
pub(super) fn args<T: AsRef<OsStr>>(&mut self, args: impl IntoIterator<Item = T>) -> &mut Self {
|
||||
for arg in args {
|
||||
let arg = arg.as_ref();
|
||||
self.inner.arg(arg);
|
||||
self.text.push(arg.to_os_string());
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Spawn the process.
|
||||
pub(super) fn spawn(&mut self) -> io::Result<Child> {
|
||||
dump_text(&self.text);
|
||||
self.inner.spawn()
|
||||
}
|
||||
}
|
||||
|
||||
/// Dump `OsString` list to stderr.
|
||||
fn dump_text(text: &[OsString]) {
|
||||
let mut cerr = io::stderr().lock();
|
||||
write!(&mut cerr, "+").unwrap();
|
||||
|
||||
for arg in text {
|
||||
match arg.to_str() {
|
||||
Some(arg) => write!(&mut cerr, " {}", arg).unwrap(),
|
||||
None => write!(&mut cerr, " {:?}", arg).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(&mut cerr).unwrap();
|
||||
}
|
||||
91
it/gui-test-runner/src/docker/command.rs
Normal file
91
it/gui-test-runner/src/docker/command.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright 2024 The Winit Contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Run the actual Docker command.
|
||||
|
||||
use crate::command::Command;
|
||||
|
||||
use camino::Utf8Path;
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::process::Child;
|
||||
|
||||
/// The Docker command line.
|
||||
pub(super) struct DockerRun {
|
||||
command: Command,
|
||||
}
|
||||
|
||||
impl DockerRun {
|
||||
/// Start the command.
|
||||
pub(super) fn new() -> Self {
|
||||
let mut command = Command::new("docker");
|
||||
command.arg("run");
|
||||
|
||||
Self { command }
|
||||
}
|
||||
|
||||
/// Run with an environment variable.
|
||||
pub(super) fn env(&mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> &mut Self {
|
||||
let env_arg = format!("{}={}", name.as_ref(), value.as_ref());
|
||||
self.command.args(["--env", &env_arg]);
|
||||
self
|
||||
}
|
||||
|
||||
/// Run with a simple `init` process.
|
||||
pub(super) fn init(&mut self) -> &mut Self {
|
||||
self.command.arg("--init");
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the working directory.
|
||||
pub(super) fn workdir(&mut self, dir: impl AsRef<OsStr>) -> &mut Self {
|
||||
self.command.arg("--workdir");
|
||||
self.command.arg(dir);
|
||||
self
|
||||
}
|
||||
|
||||
/// Remove the container once it is complete.
|
||||
pub(super) fn rm(&mut self) -> &mut Self {
|
||||
self.command.arg("--rm");
|
||||
self
|
||||
}
|
||||
|
||||
/// Pass a volume into the container.
|
||||
pub(super) fn volume(
|
||||
&mut self,
|
||||
host: impl AsRef<Utf8Path>,
|
||||
container: impl AsRef<Utf8Path>,
|
||||
) -> &mut Self {
|
||||
let list = format!("{}:{}", host.as_ref(), container.as_ref());
|
||||
self.command.args(["--volume", &list]);
|
||||
self
|
||||
}
|
||||
|
||||
/// Run the container with a command.
|
||||
pub(super) fn run_with_command<T: AsRef<OsStr>>(
|
||||
&mut self,
|
||||
container_name: impl AsRef<str>,
|
||||
container_version: impl AsRef<str>,
|
||||
command: impl IntoIterator<Item = T>,
|
||||
) -> io::Result<Child> {
|
||||
self.command.arg(format!(
|
||||
"{}:{}",
|
||||
container_name.as_ref(),
|
||||
container_version.as_ref()
|
||||
));
|
||||
self.command.args(command);
|
||||
self.command.spawn()
|
||||
}
|
||||
}
|
||||
114
it/gui-test-runner/src/docker/linux.rs
Normal file
114
it/gui-test-runner/src/docker/linux.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright 2024 The Winit Contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Run tests inside of a Linux docker container.
|
||||
|
||||
use super::command::DockerRun;
|
||||
use crate::stream::StreamReader;
|
||||
|
||||
use gui_test::remote::handler;
|
||||
use gui_test::TestHandler;
|
||||
|
||||
use std::io;
|
||||
use std::os::unix::net::UnixListener;
|
||||
use std::path::Path;
|
||||
use std::thread;
|
||||
|
||||
const UBUNTU_DOCKERFILE: &str = "ghcr.io/rust-windowing/testubuntu";
|
||||
const LATEST: &str = "latest";
|
||||
|
||||
/// Run the provided test in a Linux docker container.
|
||||
pub(crate) fn linux_test(test_name: &str) -> io::Result<()> {
|
||||
// Create a Unix socket to listen for events on.
|
||||
let unix_path = format!("/tmp/gui_test_{}.sock", fastrand::u16(..));
|
||||
let listener = UnixListener::bind(&unix_path)?;
|
||||
|
||||
// Spawn the Docker container.
|
||||
let mut container = {
|
||||
let mut docker = DockerRun::new();
|
||||
|
||||
// Usual options.
|
||||
docker.rm().init();
|
||||
|
||||
// Pass through the socket as a volume.
|
||||
docker.volume(&unix_path, &unix_path);
|
||||
|
||||
// Pass through the winit directory.
|
||||
let winit_directory = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.ancestors()
|
||||
.find_map(|path| {
|
||||
let cargo_toml = path.join("Cargo.toml");
|
||||
let contents = std::fs::read(cargo_toml).ok()?;
|
||||
|
||||
if std::str::from_utf8(&contents)
|
||||
.ok()?
|
||||
.contains("name = \"winit\"")
|
||||
{
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
docker.volume(
|
||||
camino::Utf8Path::from_path(winit_directory).unwrap(),
|
||||
"/app/winit/",
|
||||
);
|
||||
|
||||
// Set the working dir to this directory.
|
||||
docker.workdir("/app/winit/");
|
||||
|
||||
// Set GUI_TEST_UNIX_STREAM to the socket.
|
||||
docker.env("GUI_TEST_UNIX_STREAM", &unix_path);
|
||||
|
||||
// Set CARGO_TARGET_DIR to a random other directory.
|
||||
docker.env("CARGO_TARGET_DIR", "/tmp/");
|
||||
|
||||
// The command to run the test.
|
||||
let command = ["xvfb-run", "cargo", "run", "-p", test_name];
|
||||
|
||||
// Spawn the test container.
|
||||
docker.run_with_command(UBUNTU_DOCKERFILE, LATEST, command)?
|
||||
};
|
||||
|
||||
// Run the console listener in another thread.
|
||||
let handle = thread::spawn(move || {
|
||||
// Attach to the listener.
|
||||
let (event_reader, _) = listener.accept().unwrap();
|
||||
|
||||
// Read events and output them as we get them.
|
||||
let input = StreamReader::new(event_reader);
|
||||
let mut output = handler();
|
||||
|
||||
for event in input {
|
||||
let event = event?;
|
||||
output.handle_test(event);
|
||||
}
|
||||
|
||||
io::Result::Ok(())
|
||||
});
|
||||
|
||||
// Wait for the container to finish.
|
||||
if !container.wait()?.success() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"docker exited with a failure exit code",
|
||||
));
|
||||
}
|
||||
|
||||
// Stop the thread.
|
||||
handle.join().unwrap().unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
20
it/gui-test-runner/src/docker/mod.rs
Normal file
20
it/gui-test-runner/src/docker/mod.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright 2024 The Winit Contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Dealing with Docker.
|
||||
|
||||
mod command;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub(super) mod linux;
|
||||
69
it/gui-test-runner/src/main.rs
Normal file
69
it/gui-test-runner/src/main.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
//! Runner for the `gui-test` system.
|
||||
|
||||
mod command;
|
||||
mod docker;
|
||||
mod stream;
|
||||
|
||||
use std::env;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
fn main() {
|
||||
let mut args = env::args();
|
||||
|
||||
// Get the test crate name.
|
||||
let test_crate = args.nth(1).unwrap();
|
||||
|
||||
// Get the target.
|
||||
let target_tag = args.next().unwrap();
|
||||
|
||||
// Split the target into the target and the tag.
|
||||
let (target, tag) = {
|
||||
let mut split = target_tag.splitn(1, ':');
|
||||
let target = split.next().unwrap();
|
||||
let tag = split.next();
|
||||
(target, tag)
|
||||
};
|
||||
|
||||
// Get the current target.
|
||||
let current_target = current_target();
|
||||
|
||||
// If we are building for Linux, run the Linux Docker container.
|
||||
// TODO: Architecture differences.
|
||||
if target.contains("linux") {
|
||||
docker::linux::linux_test(&test_crate).unwrap();
|
||||
return;
|
||||
}
|
||||
|
||||
// For now, we only support building for the current target.
|
||||
assert_eq!(target, current_target);
|
||||
assert!(tag.is_none());
|
||||
|
||||
// Just run the crate.
|
||||
if !Command::new("cargo")
|
||||
.args(["run", "-p", &test_crate])
|
||||
.status()
|
||||
.unwrap()
|
||||
.success()
|
||||
{
|
||||
panic!("test failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current target.
|
||||
fn current_target() -> String {
|
||||
let output = Command::new("rustc")
|
||||
.arg("-vV")
|
||||
.stdout(Stdio::piped())
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Look for the line that starts with "host".
|
||||
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||
for line in stdout.lines() {
|
||||
if let Some(host) = line.strip_prefix("host: ") {
|
||||
return host.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
panic!("failed to find host: line in rustc output")
|
||||
}
|
||||
81
it/gui-test-runner/src/stream.rs
Normal file
81
it/gui-test-runner/src/stream.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright 2024 The Winit Contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Read test events from a stream.
|
||||
|
||||
use gui_test::{TestEvent, TestEventType};
|
||||
use std::io::{self, Read};
|
||||
|
||||
/// Read events from a stream.
|
||||
pub(super) struct StreamReader<R> {
|
||||
/// The inner reader.
|
||||
reader: Option<R>,
|
||||
|
||||
/// Reused buffer.
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<R: Read> StreamReader<R> {
|
||||
/// Create a new stream reader.
|
||||
pub(super) fn new(reader: R) -> Self {
|
||||
Self {
|
||||
reader: Some(reader),
|
||||
buffer: vec![0u8; 1024],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! leap {
|
||||
($self:expr, $e:expr) => {{
|
||||
match ($e) {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
($self).reader = None;
|
||||
return Some(Err(err));
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
impl<R: Read> Iterator for StreamReader<R> {
|
||||
type Item = io::Result<TestEvent>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let reader = self.reader.as_mut()?;
|
||||
|
||||
// Read eight bytes from the reader to get payload length.
|
||||
let mut len_buffer = [0u8; 8];
|
||||
leap!(self, reader.read_exact(&mut len_buffer));
|
||||
|
||||
// Parse that, then read the length's worth of bytes.
|
||||
let length = u64::from_be_bytes(len_buffer);
|
||||
self.buffer.resize(length as usize, 0);
|
||||
leap!(self, reader.read_exact(&mut self.buffer));
|
||||
|
||||
// Parse as a test event.
|
||||
let event: TestEvent = leap!(
|
||||
self,
|
||||
serde_json::from_slice(&self.buffer)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))
|
||||
);
|
||||
|
||||
// If this is complete, stop running.
|
||||
if matches!(event.ty, TestEventType::Complete { .. }) {
|
||||
self.reader = None;
|
||||
}
|
||||
|
||||
// We are okay.
|
||||
Some(Ok(event))
|
||||
}
|
||||
}
|
||||
17
it/gui-test/Cargo.toml
Normal file
17
it/gui-test/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[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.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.workspace = true
|
||||
443
it/gui-test/src/lib.rs
Normal file
443
it/gui-test/src/lib.rs
Normal file
@@ -0,0 +1,443 @@
|
||||
//! A testing framework that can be run remotely.
|
||||
|
||||
pub mod remote;
|
||||
pub mod stream;
|
||||
pub mod user;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::env;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::mem;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::panic;
|
||||
|
||||
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 = 1;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use inventory as __inventory;
|
||||
|
||||
/// Replacement for the `main` function.
|
||||
#[macro_export]
|
||||
macro_rules! main {
|
||||
($handler:expr) => {
|
||||
fn main() {
|
||||
$crate::__entry(|| $handler)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// 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 const 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.send_event(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());
|
||||
match panic::catch_unwind(panic::AssertUnwindSafe(f)) {
|
||||
Ok(x) => x,
|
||||
|
||||
Err(err) => {
|
||||
if let Some(panic) = err.downcast_ref::<&'static str>() {
|
||||
test.fail(panic.to_string());
|
||||
} else if let Some(panic) = err.downcast_ref::<String>() {
|
||||
test.fail(panic.clone());
|
||||
} else {
|
||||
test.fail("unintelligible error".to_string());
|
||||
}
|
||||
|
||||
panic::resume_unwind(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.send_event(TestEventType::GroupStarted { name: name.into() });
|
||||
|
||||
// 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.send_event(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.send_event(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),
|
||||
};
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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);
|
||||
}
|
||||
|
||||
/// Fail this test.
|
||||
fn fail(mut self, panic: String) {
|
||||
self.harness
|
||||
.take()
|
||||
.unwrap()
|
||||
.end_test(TestResult::Failed(panic));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
impl<T: TestHandler + ?Sized> TestHandler for Box<T> {
|
||||
fn handle_test(&mut self, event: TestEvent) {
|
||||
(**self).handle_test(event)
|
||||
}
|
||||
}
|
||||
|
||||
/// An event produced by the test harness.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TestEvent {
|
||||
/// The name of the runner associated with the event.
|
||||
pub runner: String,
|
||||
|
||||
/// The type of the event.
|
||||
pub 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 {
|
||||
/// 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 },
|
||||
|
||||
/// 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: impl FnOnce() -> 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.
|
||||
panic::catch_unwind(panic::AssertUnwindSafe(move || {
|
||||
(test_to_run.func)(&mut harness)
|
||||
}))
|
||||
.ok();
|
||||
} 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()
|
||||
}
|
||||
31
it/gui-test/src/remote.rs
Normal file
31
it/gui-test/src/remote.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
//! Create a test handler that can be run remotely.
|
||||
|
||||
use crate::stream::WriteHandler;
|
||||
use crate::user::UserHandler;
|
||||
use crate::TestHandler;
|
||||
|
||||
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())
|
||||
}
|
||||
39
it/gui-test/src/stream.rs
Normal file
39
it/gui-test/src/stream.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
//! 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.
|
||||
#[derive(Debug)]
|
||||
pub struct WriteHandler<W: Write> {
|
||||
/// 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();
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Drop for WriteHandler<W> {
|
||||
fn drop(&mut self) {
|
||||
self.writer.flush().ok();
|
||||
}
|
||||
}
|
||||
180
it/gui-test/src/user.rs
Normal file
180
it/gui-test/src/user.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
//! 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 {
|
||||
ender = false;
|
||||
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.
|
||||
self.dump_events(entries.into_iter().inspect(|ty| {
|
||||
ender |= matches!(ty, TestEventType::Complete { .. });
|
||||
}));
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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());
|
||||
|
||||
// Write the final bit to the stdout.
|
||||
let mut stdout = io::stdout().lock();
|
||||
|
||||
if !self.failures.is_empty() {
|
||||
writeln!(stdout, "Test Failures:").ok();
|
||||
|
||||
for (test_name, panic) in &self.failures {
|
||||
writeln!(stdout, " {}", test_name).ok();
|
||||
writeln!(stdout, "-------------").ok();
|
||||
writeln!(stdout, "{}", panic).ok();
|
||||
writeln!(stdout, "-------------").ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user