#![doc = include_str!("../examples/hello.rs")]
#![warn(clippy::pedantic)]
#![warn(missing_docs)]
use eyre::eyre;
use futures_util::{FutureExt, future::BoxFuture};
use tokio::task::JoinSet;
use crate::scons::Scons;
mod scons;
#[must_use]
pub struct Success {
warning: Option<eyre::Report>,
branches: Vec<Box<dyn TestCase>>,
}
impl Success {
pub fn ok() -> Self {
Self {
warning: None,
branches: vec![],
}
}
pub fn warn(warning: eyre::Report) -> Self {
Self {
warning: Some(warning),
branches: vec![],
}
}
pub fn branch(branches: Vec<Box<dyn TestCase>>) -> Self {
Self {
warning: None,
branches,
}
}
pub fn warn_and_branch(warning: eyre::Report, branches: Vec<Box<dyn TestCase>>) -> Self {
Self {
warning: Some(warning),
branches,
}
}
#[must_use]
pub fn warning(&self) -> Option<&eyre::Report> {
self.warning.as_ref()
}
#[must_use]
pub fn branches(&self) -> &[Box<dyn TestCase>] {
&self.branches
}
#[must_use]
pub fn into_branches(self) -> Vec<Box<dyn TestCase>> {
self.branches
}
}
pub struct Runner {
js: JoinSet<(usize, eyre::Result<Success>)>,
scons: Scons,
n_spawned: usize,
}
impl Default for Runner {
fn default() -> Self {
Self::new()
}
}
impl Runner {
#[must_use]
pub fn new() -> Self {
Self {
js: JoinSet::new(),
scons: Scons::new(),
n_spawned: 0,
}
}
fn next_id(&mut self) -> usize {
let id = self.n_spawned;
self.n_spawned += 1;
id
}
fn spawn(&mut self, parent: &str, case: Box<dyn TestCase>) {
let name = if parent.is_empty() {
case.name()
} else {
format!("{} > {}", parent, case.name())
};
let id = self.next_id();
self.scons.insert(id, name.clone());
self.js.spawn(case.run().map(move |res| (id, res)));
}
pub async fn run(mut self, entrypoint: Box<dyn TestCase>) -> eyre::Result<()> {
self.spawn("", entrypoint);
let mut errored = false;
while let Some((id, res)) = self
.js
.join_next()
.await
.transpose()
.expect("spawned thread panicked")
{
let name = self.scons.remove(id, &res);
match res {
Ok(success) => {
for case in success.into_branches() {
self.spawn(&name, case);
}
}
Err(_err) => {
errored = true;
}
}
}
self.scons.finalize();
if errored {
Err(eyre!("One or more checks failed"))
} else {
eprintln!("All checks passed!");
Ok(())
}
}
}
pub trait TestCase: Send {
fn name(&self) -> String;
fn run(self: Box<Self>) -> BoxFuture<'static, eyre::Result<Success>>;
}
pub fn test_fn<F, Fut>(name: impl Into<String>, f: F) -> Box<dyn TestCase>
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = eyre::Result<Success>> + Send + 'static,
{
struct TestFn<F> {
name: String,
f: F,
}
impl<F, Fut> TestCase for TestFn<F>
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = eyre::Result<Success>> + Send + 'static,
{
fn name(&self) -> String {
self.name.clone()
}
fn run(self: Box<Self>) -> BoxFuture<'static, eyre::Result<Success>> {
(self.f)().boxed()
}
}
Box::new(TestFn {
name: name.into(),
f,
})
}
#[cfg(test)]
mod tests {
use crate::{Runner, test_fn};
#[tokio::test]
#[should_panic(expected = "panic inside spawned task")]
async fn test_panic() {
Runner::new()
.run(test_fn("entry", || async {
panic!("panic inside spawned task");
}))
.await
.unwrap();
}
}