use crate::args::{Arguments, TimeThreshold};
use crate::bench::Bencher;
use crate::stats::Summary;
use std::any::{Any, TypeId};
use std::backtrace::Backtrace;
use std::cmp::{max, Ordering};
use std::collections::HashMap;
use std::fmt::{Debug, Display, Formatter};
use std::future::Future;
use std::hash::Hash;
use std::pin::Pin;
use std::process::ExitCode;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
#[derive(Clone)]
#[allow(clippy::type_complexity)]
pub enum TestFunction {
Sync(
Arc<
dyn Fn(Arc<dyn DependencyView + Send + Sync>) -> Box<dyn TestReturnValue>
+ Send
+ Sync
+ 'static,
>,
),
SyncBench(
Arc<dyn Fn(&mut Bencher, Arc<dyn DependencyView + Send + Sync>) + Send + Sync + 'static>,
),
#[cfg(feature = "tokio")]
Async(
Arc<
dyn (Fn(
Arc<dyn DependencyView + Send + Sync>,
) -> Pin<Box<dyn Future<Output = Box<dyn TestReturnValue>>>>)
+ Send
+ Sync
+ 'static,
>,
),
#[cfg(feature = "tokio")]
AsyncBench(
Arc<
dyn for<'a> Fn(
&'a mut crate::bench::AsyncBencher,
Arc<dyn DependencyView + Send + Sync>,
) -> Pin<Box<dyn Future<Output = ()> + 'a>>
+ Send
+ Sync
+ 'static,
>,
),
}
impl TestFunction {
#[cfg(not(feature = "tokio"))]
pub fn is_bench(&self) -> bool {
matches!(self, TestFunction::SyncBench(_))
}
#[cfg(feature = "tokio")]
pub fn is_bench(&self) -> bool {
matches!(
self,
TestFunction::SyncBench(_) | TestFunction::AsyncBench(_)
)
}
}
pub trait TestReturnValue {
fn into_result(self: Box<Self>) -> Result<(), FailureCause>;
}
impl TestReturnValue for () {
fn into_result(self: Box<Self>) -> Result<(), FailureCause> {
Ok(())
}
}
impl<T, E: Display + Debug + Send + Sync + 'static> TestReturnValue for Result<T, E> {
fn into_result(self: Box<Self>) -> Result<(), FailureCause> {
match *self {
Ok(_) => Ok(()),
Err(e) => Err(FailureCause::from_error(e)),
}
}
}
#[derive(Clone)]
pub enum FailureCause {
ReturnedError {
display: String,
debug: String,
prefer_debug: bool,
error: Arc<dyn Any + Send + Sync>,
},
ReturnedMessage(String),
Panic(PanicCause),
HarnessError(String),
}
#[derive(Debug, Clone)]
pub struct PanicCause {
pub message: Option<String>,
pub location: Option<PanicLocation>,
pub backtrace: Option<Arc<Backtrace>>,
}
#[derive(Debug, Clone)]
pub struct PanicLocation {
pub file: String,
pub line: u32,
pub column: u32,
}
impl std::fmt::Debug for FailureCause {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
FailureCause::ReturnedError { display, .. } => {
f.debug_tuple("ReturnedError").field(display).finish()
}
FailureCause::ReturnedMessage(s) => f.debug_tuple("ReturnedMessage").field(s).finish(),
FailureCause::Panic(p) => f.debug_tuple("Panic").field(p).finish(),
FailureCause::HarnessError(s) => f.debug_tuple("HarnessError").field(s).finish(),
}
}
}
impl FailureCause {
pub fn from_error<E: Display + Debug + Send + Sync + 'static>(e: E) -> Self {
if TypeId::of::<E>() == TypeId::of::<String>() {
let any: Box<dyn Any + Send + Sync> = Box::new(e);
return FailureCause::ReturnedMessage(*any.downcast::<String>().unwrap());
}
let mut _prefer_debug = false;
#[cfg(feature = "anyhow")]
{
_prefer_debug = TypeId::of::<E>() == TypeId::of::<anyhow::Error>();
}
FailureCause::ReturnedError {
display: format!("{e:#}"),
debug: format!("{e:?}"),
prefer_debug: _prefer_debug,
error: Arc::new(e),
}
}
pub fn render(&self) -> String {
match self {
FailureCause::ReturnedError {
display,
debug,
prefer_debug,
..
} => {
if *prefer_debug {
debug.clone()
} else {
display.clone()
}
}
FailureCause::ReturnedMessage(s) => s.clone(),
FailureCause::Panic(p) => p.render(),
FailureCause::HarnessError(s) => s.clone(),
}
}
pub fn panic_message(&self) -> Option<&str> {
match self {
FailureCause::Panic(p) => p.message.as_deref(),
_ => None,
}
}
}
impl PanicCause {
pub fn render(&self) -> String {
let mut out = self.message.clone().unwrap_or_default();
if let Some(loc) = &self.location {
out.push_str(&format!("\n at {}:{}:{}", loc.file, loc.line, loc.column));
}
if let Some(bt) = &self.backtrace {
let bt_str = format!("{bt}");
if !bt_str.is_empty() && bt_str != "disabled backtrace" {
out.push_str(&format!("\n\nStack backtrace:\n{bt}"));
}
}
out
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ShouldPanic {
No,
Yes,
WithMessage(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TestType {
UnitTest,
IntegrationTest,
}
impl TestType {
pub fn from_path(path: &str) -> Self {
if path.contains("/src/") {
TestType::UnitTest
} else {
TestType::IntegrationTest
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FlakinessControl {
None,
ProveNonFlaky(usize),
RetryKnownFlaky(usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DetachedPanicPolicy {
FailTest,
Ignore,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CaptureControl {
Default,
AlwaysCapture,
NeverCapture,
}
impl CaptureControl {
pub fn requires_capturing(&self, default: bool) -> bool {
match self {
CaptureControl::Default => default,
CaptureControl::AlwaysCapture => true,
CaptureControl::NeverCapture => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReportTimeControl {
Default,
Enabled,
Disabled,
}
#[derive(Clone)]
pub struct TestProperties {
pub should_panic: ShouldPanic,
pub test_type: TestType,
pub timeout: Option<Duration>,
pub flakiness_control: FlakinessControl,
pub capture_control: CaptureControl,
pub report_time_control: ReportTimeControl,
pub ensure_time_control: ReportTimeControl,
pub tags: Vec<String>,
pub is_ignored: bool,
pub detached_panic_policy: DetachedPanicPolicy,
}
impl TestProperties {
pub fn unit_test() -> Self {
TestProperties {
test_type: TestType::UnitTest,
..Default::default()
}
}
pub fn integration_test() -> Self {
TestProperties {
test_type: TestType::IntegrationTest,
..Default::default()
}
}
}
impl Default for TestProperties {
fn default() -> Self {
Self {
should_panic: ShouldPanic::No,
test_type: TestType::UnitTest,
timeout: None,
flakiness_control: FlakinessControl::None,
capture_control: CaptureControl::Default,
report_time_control: ReportTimeControl::Default,
ensure_time_control: ReportTimeControl::Default,
tags: Vec::new(),
is_ignored: false,
detached_panic_policy: DetachedPanicPolicy::FailTest,
}
}
}
#[derive(Clone)]
pub struct RegisteredTest {
pub name: String,
pub crate_name: String,
pub module_path: String,
pub run: TestFunction,
pub props: TestProperties,
pub dependencies: Option<Vec<String>>,
}
impl RegisteredTest {
pub fn filterable_name(&self) -> String {
if !self.module_path.is_empty() {
format!("{}::{}", self.module_path, self.name)
} else {
self.name.clone()
}
}
pub fn fully_qualified_name(&self) -> String {
[&self.crate_name, &self.module_path, &self.name]
.into_iter()
.filter(|s| !s.is_empty())
.cloned()
.collect::<Vec<String>>()
.join("::")
}
pub fn crate_and_module(&self) -> String {
[&self.crate_name, &self.module_path]
.into_iter()
.filter(|s| !s.is_empty())
.cloned()
.collect::<Vec<String>>()
.join("::")
}
}
impl Debug for RegisteredTest {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RegisteredTest")
.field("name", &self.name)
.field("crate_name", &self.crate_name)
.field("module_path", &self.module_path)
.finish()
}
}
pub static REGISTERED_TESTS: Mutex<Vec<RegisteredTest>> = Mutex::new(Vec::new());
#[derive(Clone)]
#[allow(clippy::type_complexity)]
pub enum DependencyConstructor {
Sync(
Arc<
dyn (Fn(Arc<dyn DependencyView + Send + Sync>) -> Arc<dyn Any + Send + Sync + 'static>)
+ Send
+ Sync
+ 'static,
>,
),
Async(
Arc<
dyn (Fn(
Arc<dyn DependencyView + Send + Sync>,
) -> Pin<Box<dyn Future<Output = Arc<dyn Any + Send + Sync>>>>)
+ Send
+ Sync
+ 'static,
>,
),
}
pub trait CloneableDep: Sized + Send + Sync + 'static {
fn to_wire(&self) -> Vec<u8>;
fn from_wire(bytes: &[u8]) -> Self;
}
pub trait HostedDep: Sized + Send + Sync + 'static {
fn descriptor(&self) -> Vec<u8>;
fn from_descriptor(bytes: &[u8]) -> Self;
}
pub trait AsyncHostedDep: Sized + Send + Sync + 'static {
fn descriptor(&self) -> Vec<u8>;
fn from_descriptor(bytes: &[u8]) -> impl std::future::Future<Output = Self> + Send;
}
impl<T: HostedDep> AsyncHostedDep for T {
fn descriptor(&self) -> Vec<u8> {
<T as HostedDep>::descriptor(self)
}
fn from_descriptor(bytes: &[u8]) -> impl std::future::Future<Output = Self> + Send {
std::future::ready(<T as HostedDep>::from_descriptor(bytes))
}
}
#[cfg(test)]
mod hosted_dep_blanket_bridge_tests {
use super::{AsyncHostedDep, HostedDep};
use std::future::Future;
#[derive(Debug, PartialEq, Eq)]
struct SyncOnlyDep {
bytes: Vec<u8>,
}
impl HostedDep for SyncOnlyDep {
fn descriptor(&self) -> Vec<u8> {
self.bytes.clone()
}
fn from_descriptor(bytes: &[u8]) -> Self {
Self {
bytes: bytes.to_vec(),
}
}
}
fn requires_async_hosted_dep<T: AsyncHostedDep>(_t: &T) {}
#[test]
fn blanket_impl_exposes_sync_hosted_dep_via_async_api() {
let dep = SyncOnlyDep {
bytes: vec![1, 2, 3, 4],
};
requires_async_hosted_dep(&dep);
assert_eq!(
<SyncOnlyDep as HostedDep>::descriptor(&dep),
vec![1, 2, 3, 4]
);
assert_eq!(
<SyncOnlyDep as AsyncHostedDep>::descriptor(&dep),
vec![1, 2, 3, 4]
);
let fut = <SyncOnlyDep as AsyncHostedDep>::from_descriptor(&[7, 8, 9]);
let mut fut = Box::pin(fut);
let waker = futures_test_helpers::noop_waker();
let mut cx = std::task::Context::from_waker(&waker);
match fut.as_mut().poll(&mut cx) {
std::task::Poll::Ready(value) => {
assert_eq!(
value,
SyncOnlyDep {
bytes: vec![7, 8, 9]
},
"blanket-bridged from_descriptor must yield the same value the sync impl produces"
);
}
std::task::Poll::Pending => panic!(
"blanket-bridged from_descriptor must be immediately ready (std::future::ready)"
),
}
}
mod futures_test_helpers {
use std::task::{RawWaker, RawWakerVTable, Waker};
unsafe fn clone(p: *const ()) -> RawWaker {
RawWaker::new(p, &VTABLE)
}
unsafe fn wake(_: *const ()) {}
unsafe fn wake_by_ref(_: *const ()) {}
unsafe fn drop(_: *const ()) {}
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
pub fn noop_waker() -> Waker {
unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) }
}
}
}
pub trait HostedRpcDep: Send + Sync + 'static {
type Stub: Send + Sync + 'static;
fn dispatch(&mut self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String>;
fn build_stub(channel: HostedRpcChannel) -> Self::Stub;
}
pub trait HostedRpcDispatcher: Send + Sync {
fn dispatch(&mut self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String>;
}
impl<T: HostedRpcDep> HostedRpcDispatcher for T {
fn dispatch(&mut self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String> {
<T as HostedRpcDep>::dispatch(self, method_idx, args)
}
}
pub trait AsyncHostedRpcDep: Send + Sync + 'static {
type Stub: Send + Sync + 'static;
fn dispatch<'a>(
&'a mut self,
method_idx: u32,
args: &'a [u8],
) -> impl Future<Output = Result<Vec<u8>, String>> + Send + 'a;
fn build_stub(channel: HostedRpcChannel) -> Self::Stub;
}
impl<T: HostedRpcDep> AsyncHostedRpcDep for T {
type Stub = <T as HostedRpcDep>::Stub;
fn dispatch<'a>(
&'a mut self,
method_idx: u32,
args: &'a [u8],
) -> impl Future<Output = Result<Vec<u8>, String>> + Send + 'a {
std::future::ready(<T as HostedRpcDep>::dispatch(self, method_idx, args))
}
fn build_stub(channel: HostedRpcChannel) -> Self::Stub {
<T as HostedRpcDep>::build_stub(channel)
}
}
pub trait AsyncHostedRpcDispatcher: Send + Sync {
fn dispatch<'a>(
&'a mut self,
method_idx: u32,
args: &'a [u8],
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + Send + 'a>>;
}
impl<T: AsyncHostedRpcDep> AsyncHostedRpcDispatcher for T {
fn dispatch<'a>(
&'a mut self,
method_idx: u32,
args: &'a [u8],
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + Send + 'a>> {
Box::pin(<T as AsyncHostedRpcDep>::dispatch(self, method_idx, args))
}
}
#[cfg(test)]
mod hosted_rpc_blanket_bridge_tests {
use super::{AsyncHostedRpcDep, HostedRpcChannel, HostedRpcDep};
struct SyncOnlyOwner {
next: u64,
}
pub struct SyncOnlyStub {
_channel: HostedRpcChannel,
}
impl HostedRpcDep for SyncOnlyOwner {
type Stub = SyncOnlyStub;
fn dispatch(&mut self, method_idx: u32, _args: &[u8]) -> Result<Vec<u8>, String> {
if method_idx == 0 {
self.next += 1;
Ok(self.next.to_be_bytes().to_vec())
} else {
Err(format!("SyncOnlyOwner: unknown method_idx {method_idx}"))
}
}
fn build_stub(channel: HostedRpcChannel) -> Self::Stub {
SyncOnlyStub { _channel: channel }
}
}
fn requires_async_hosted_rpc_dep<T: AsyncHostedRpcDep>(_t: &T) {}
#[test]
fn blanket_impl_exposes_sync_hosted_rpc_dep_via_async_api() {
let owner = SyncOnlyOwner { next: 0 };
requires_async_hosted_rpc_dep(&owner);
}
#[cfg(feature = "tokio")]
#[test]
fn bridged_async_dispatch_round_trips_sync_owner_bytes() {
let mut owner = SyncOnlyOwner { next: 0 };
let rt = ::tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("build tokio runtime");
let bytes = rt
.block_on(<SyncOnlyOwner as AsyncHostedRpcDep>::dispatch(
&mut owner,
0,
&[],
))
.expect("bridged dispatch must succeed");
assert_eq!(
bytes,
1u64.to_be_bytes().to_vec(),
"bridged async dispatch must yield the same bytes the sync impl produces"
);
}
}
pub struct HostedRpcOwnerCell {
inner: HostedRpcOwnerCellInner,
}
enum HostedRpcOwnerCellInner {
Sync(Mutex<Box<dyn HostedRpcDispatcher>>),
#[cfg(feature = "tokio")]
Async(AsyncOwnerCell),
}
#[cfg(feature = "tokio")]
struct AsyncOwnerCell {
poisoned: std::sync::atomic::AtomicBool,
inner: tokio::sync::Mutex<Box<dyn AsyncHostedRpcDispatcher>>,
}
impl HostedRpcOwnerCell {
pub fn from_owner<T: HostedRpcDep>(owner: T) -> Self {
Self {
inner: HostedRpcOwnerCellInner::Sync(Mutex::new(
Box::new(owner) as Box<dyn HostedRpcDispatcher>
)),
}
}
#[cfg(feature = "tokio")]
pub fn from_async_owner<T: AsyncHostedRpcDep>(owner: T) -> Self {
Self {
inner: HostedRpcOwnerCellInner::Async(AsyncOwnerCell {
poisoned: std::sync::atomic::AtomicBool::new(false),
inner: tokio::sync::Mutex::new(Box::new(owner) as Box<dyn AsyncHostedRpcDispatcher>),
}),
}
}
pub fn from_shared_owner_sync<T, F>(owner: Arc<T>, dispatch: F) -> Self
where
T: Send + Sync + 'static,
F: Fn(&T, u32, &[u8]) -> Result<Vec<u8>, String> + Send + Sync + 'static,
{
struct SharedDispatcher<T, F>
where
T: Send + Sync + 'static,
F: Fn(&T, u32, &[u8]) -> Result<Vec<u8>, String> + Send + Sync + 'static,
{
owner: Arc<T>,
dispatch: F,
}
impl<T, F> HostedRpcDispatcher for SharedDispatcher<T, F>
where
T: Send + Sync + 'static,
F: Fn(&T, u32, &[u8]) -> Result<Vec<u8>, String> + Send + Sync + 'static,
{
fn dispatch(&mut self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String> {
(self.dispatch)(&self.owner, method_idx, args)
}
}
let dispatcher: Box<dyn HostedRpcDispatcher> =
Box::new(SharedDispatcher { owner, dispatch });
Self {
inner: HostedRpcOwnerCellInner::Sync(Mutex::new(dispatcher)),
}
}
#[cfg(feature = "tokio")]
pub fn from_shared_owner_async<T, F>(owner: Arc<T>, dispatch: F) -> Self
where
T: Send + Sync + 'static,
F: for<'a> Fn(
&'a T,
u32,
&'a [u8],
)
-> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + Send + 'a>>
+ Send
+ Sync
+ 'static,
{
struct SharedAsyncDispatcher<T, F>
where
T: Send + Sync + 'static,
F: for<'a> Fn(
&'a T,
u32,
&'a [u8],
)
-> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + Send + 'a>>
+ Send
+ Sync
+ 'static,
{
owner: Arc<T>,
dispatch: F,
}
impl<T, F> AsyncHostedRpcDispatcher for SharedAsyncDispatcher<T, F>
where
T: Send + Sync + 'static,
F: for<'a> Fn(
&'a T,
u32,
&'a [u8],
)
-> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + Send + 'a>>
+ Send
+ Sync
+ 'static,
{
fn dispatch<'a>(
&'a mut self,
method_idx: u32,
args: &'a [u8],
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + Send + 'a>> {
(self.dispatch)(&self.owner, method_idx, args)
}
}
let dispatcher: Box<dyn AsyncHostedRpcDispatcher> =
Box::new(SharedAsyncDispatcher { owner, dispatch });
Self {
inner: HostedRpcOwnerCellInner::Async(AsyncOwnerCell {
poisoned: std::sync::atomic::AtomicBool::new(false),
inner: tokio::sync::Mutex::new(dispatcher),
}),
}
}
pub fn dispatch(&self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String> {
match &self.inner {
HostedRpcOwnerCellInner::Sync(mtx) => sync_dispatch_inner(mtx, method_idx, args),
#[cfg(feature = "tokio")]
HostedRpcOwnerCellInner::Async(_) => Err(
"hosted rpc owner cell uses the async dispatch path; use dispatch_async or dispatch_blocking"
.to_string(),
),
}
}
#[cfg(feature = "tokio")]
pub async fn dispatch_async(&self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String> {
match &self.inner {
HostedRpcOwnerCellInner::Sync(mtx) => sync_dispatch_inner(mtx, method_idx, args),
HostedRpcOwnerCellInner::Async(cell) => {
async_dispatch_inner(cell, method_idx, args).await
}
}
}
#[cfg(feature = "tokio")]
pub fn dispatch_blocking(&self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String> {
match &self.inner {
HostedRpcOwnerCellInner::Sync(mtx) => sync_dispatch_inner(mtx, method_idx, args),
HostedRpcOwnerCellInner::Async(cell) => {
let handle = tokio::runtime::Handle::try_current().map_err(|_| {
"hosted rpc owner is async-only and no Tokio runtime is active at the dispatch site"
.to_string()
})?;
if !matches!(
handle.runtime_flavor(),
tokio::runtime::RuntimeFlavor::MultiThread
) {
return Err(
"hosted rpc owner is async-only and the current Tokio runtime is not multi-threaded"
.to_string(),
);
}
tokio::task::block_in_place(|| {
handle.block_on(async_dispatch_inner(cell, method_idx, args))
})
}
}
}
}
fn sync_dispatch_inner(
mtx: &Mutex<Box<dyn HostedRpcDispatcher>>,
method_idx: u32,
args: &[u8],
) -> Result<Vec<u8>, String> {
let dispatch_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut guard = match mtx.lock() {
Ok(g) => g,
Err(_) => return Err("hosted rpc owner poisoned".to_string()),
};
guard.dispatch(method_idx, args)
}));
panic_payload_to_err(dispatch_result)
}
#[cfg(feature = "tokio")]
async fn async_dispatch_inner(
cell: &AsyncOwnerCell,
method_idx: u32,
args: &[u8],
) -> Result<Vec<u8>, String> {
use futures::FutureExt;
use std::sync::atomic::Ordering;
if cell.poisoned.load(Ordering::SeqCst) {
return Err("hosted rpc owner poisoned".to_string());
}
let mut guard = cell.inner.lock().await;
if cell.poisoned.load(Ordering::SeqCst) {
return Err("hosted rpc owner poisoned".to_string());
}
let fut = std::panic::AssertUnwindSafe(async {
AsyncHostedRpcDispatcher::dispatch(&mut **guard, method_idx, args).await
});
let outcome = fut.catch_unwind().await;
match outcome {
Ok(r) => {
drop(guard);
r
}
Err(payload) => {
cell.poisoned.store(true, Ordering::SeqCst);
drop(guard);
let msg = panic_payload_to_string(&payload);
Err(format!("hosted rpc owner panicked: {msg}"))
}
}
}
fn panic_payload_to_err(
dispatch_result: Result<Result<Vec<u8>, String>, Box<dyn Any + Send>>,
) -> Result<Vec<u8>, String> {
match dispatch_result {
Ok(r) => r,
Err(payload) => {
let msg = panic_payload_to_string(&payload);
Err(format!("hosted rpc owner panicked: {msg}"))
}
}
}
fn panic_payload_to_string(payload: &Box<dyn Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"<non-string panic payload>".to_string()
}
}
pub struct HostedBothShared {
descriptor_bytes: Vec<u8>,
owner: Arc<dyn Any + Send + Sync>,
rpc_cell: Arc<HostedRpcOwnerCell>,
}
impl HostedBothShared {
pub fn new(
descriptor_bytes: Vec<u8>,
owner: Arc<dyn Any + Send + Sync>,
rpc_cell: Arc<HostedRpcOwnerCell>,
) -> Self {
Self {
descriptor_bytes,
owner,
rpc_cell,
}
}
pub fn descriptor_bytes(&self) -> &[u8] {
&self.descriptor_bytes
}
pub fn rpc_cell(&self) -> Arc<HostedRpcOwnerCell> {
self.rpc_cell.clone()
}
pub fn owner_arc<T>(&self) -> Arc<T>
where
T: Send + Sync + 'static,
{
Arc::clone(&self.owner)
.downcast::<T>()
.expect("HostedBothShared owner type mismatch")
}
}
#[derive(Debug, Clone)]
pub enum HostedRpcError {
Dispatch(String),
Transport(String),
}
impl std::fmt::Display for HostedRpcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HostedRpcError::Dispatch(s) => write!(f, "hosted rpc dispatch error: {s}"),
HostedRpcError::Transport(s) => write!(f, "hosted rpc transport error: {s}"),
}
}
}
impl std::error::Error for HostedRpcError {}
pub trait HostedRpcTransport: Send + Sync {
fn call(&self, dep_id: &str, method_idx: u32, args: Vec<u8>)
-> Result<Vec<u8>, HostedRpcError>;
}
pub struct HostedRpcChannel {
dep_id: String,
transport: Arc<dyn HostedRpcTransport>,
}
impl HostedRpcChannel {
pub fn new(dep_id: String, transport: Arc<dyn HostedRpcTransport>) -> Self {
Self { dep_id, transport }
}
pub fn dep_id(&self) -> &str {
&self.dep_id
}
pub fn call(&self, method_idx: u32, args: Vec<u8>) -> Result<Vec<u8>, HostedRpcError> {
self.transport.call(&self.dep_id, method_idx, args)
}
}
impl Clone for HostedRpcChannel {
fn clone(&self) -> Self {
Self {
dep_id: self.dep_id.clone(),
transport: self.transport.clone(),
}
}
}
pub struct InProcessHostedRpcTransport {
cells: HashMap<String, Arc<HostedRpcOwnerCell>>,
}
impl InProcessHostedRpcTransport {
pub fn new(cells: HashMap<String, Arc<HostedRpcOwnerCell>>) -> Self {
Self { cells }
}
}
impl HostedRpcTransport for InProcessHostedRpcTransport {
fn call(
&self,
dep_id: &str,
method_idx: u32,
args: Vec<u8>,
) -> Result<Vec<u8>, HostedRpcError> {
let cell = self.cells.get(dep_id).ok_or_else(|| {
HostedRpcError::Transport(format!("in-process HostedRpc: unknown dep id '{dep_id}'"))
})?;
#[cfg(feature = "tokio")]
let result = cell.dispatch_blocking(method_idx, &args);
#[cfg(not(feature = "tokio"))]
let result = cell.dispatch(method_idx, &args);
result.map_err(HostedRpcError::Dispatch)
}
}
#[derive(Clone)]
#[allow(clippy::type_complexity)]
pub struct RpcFactory {
pub owner_into_cell: Arc<
dyn (Fn(Arc<dyn Any + Send + Sync>) -> Arc<HostedRpcOwnerCell>) + Send + Sync + 'static,
>,
pub build_stub:
Arc<dyn (Fn(HostedRpcChannel) -> Arc<dyn Any + Send + Sync>) + Send + Sync + 'static>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
pub enum DepScope {
#[default]
Shared,
PerWorker,
Cloneable,
Hosted,
HostedRpc,
}
impl DepScope {
pub fn requires_single_thread_when_capturing(&self) -> bool {
matches!(self, DepScope::Shared)
}
pub fn parent_must_materialize_under_spawn_workers(&self) -> bool {
matches!(
self,
DepScope::Cloneable | DepScope::Hosted | DepScope::HostedRpc
)
}
}
#[derive(Clone)]
#[allow(clippy::type_complexity)]
pub enum WorkerReconstructor {
Sync(
Arc<
dyn (Fn(
Arc<dyn Any + Send + Sync>,
Arc<dyn DependencyView + Send + Sync>,
) -> Arc<dyn Any + Send + Sync + 'static>)
+ Send
+ Sync
+ 'static,
>,
),
Async(
Arc<
dyn (Fn(
Arc<dyn Any + Send + Sync>,
Arc<dyn DependencyView + Send + Sync>,
) -> Pin<Box<dyn Future<Output = Arc<dyn Any + Send + Sync>>>>)
+ Send
+ Sync
+ 'static,
>,
),
}
#[derive(Clone)]
#[allow(clippy::type_complexity)]
pub struct CloneableCodec {
pub to_wire: Arc<dyn (Fn(Arc<dyn Any + Send + Sync>) -> Vec<u8>) + Send + Sync + 'static>,
pub from_wire_bytes: Arc<dyn (Fn(&[u8]) -> Arc<dyn Any + Send + Sync>) + Send + Sync + 'static>,
}
#[derive(Clone)]
pub struct RegisteredDependency {
pub name: String, pub crate_name: String,
pub module_path: String,
pub constructor: DependencyConstructor,
pub dependencies: Vec<String>,
pub scope: DepScope,
pub worker_fn: Option<WorkerReconstructor>,
pub cloneable_codec: Option<CloneableCodec>,
pub hosted_codec: Option<CloneableCodec>,
pub rpc_factory: Option<RpcFactory>,
pub companions: Vec<String>,
}
impl RegisteredDependency {
pub fn new_shared(
name: String,
crate_name: String,
module_path: String,
constructor: DependencyConstructor,
dependencies: Vec<String>,
) -> Self {
Self {
name,
crate_name,
module_path,
constructor,
dependencies,
scope: DepScope::Shared,
worker_fn: None,
cloneable_codec: None,
hosted_codec: None,
rpc_factory: None,
companions: Vec::new(),
}
}
}
impl Debug for RegisteredDependency {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RegisteredDependency")
.field("name", &self.name)
.field("crate_name", &self.crate_name)
.field("module_path", &self.module_path)
.finish()
}
}
impl PartialEq for RegisteredDependency {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl Eq for RegisteredDependency {}
impl Hash for RegisteredDependency {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
impl RegisteredDependency {
pub fn crate_and_module(&self) -> String {
[&self.crate_name, &self.module_path]
.into_iter()
.filter(|s| !s.is_empty())
.cloned()
.collect::<Vec<String>>()
.join("::")
}
pub fn qualified_id(&self) -> String {
[&self.crate_name, &self.module_path, &self.name]
.into_iter()
.filter(|s| !s.is_empty())
.cloned()
.collect::<Vec<String>>()
.join("::")
}
}
pub static REGISTERED_DEPENDENCY_CONSTRUCTORS: Mutex<Vec<RegisteredDependency>> =
Mutex::new(Vec::new());
#[derive(Debug, Clone)]
pub enum RegisteredTestSuiteProperty {
Sequential {
name: String,
crate_name: String,
module_path: String,
},
Tag {
name: String,
crate_name: String,
module_path: String,
tag: String,
},
Timeout {
name: String,
crate_name: String,
module_path: String,
timeout: Duration,
},
}
impl RegisteredTestSuiteProperty {
pub fn crate_name(&self) -> &String {
match self {
RegisteredTestSuiteProperty::Sequential { crate_name, .. } => crate_name,
RegisteredTestSuiteProperty::Tag { crate_name, .. } => crate_name,
RegisteredTestSuiteProperty::Timeout { crate_name, .. } => crate_name,
}
}
pub fn module_path(&self) -> &String {
match self {
RegisteredTestSuiteProperty::Sequential { module_path, .. } => module_path,
RegisteredTestSuiteProperty::Tag { module_path, .. } => module_path,
RegisteredTestSuiteProperty::Timeout { module_path, .. } => module_path,
}
}
pub fn name(&self) -> &String {
match self {
RegisteredTestSuiteProperty::Sequential { name, .. } => name,
RegisteredTestSuiteProperty::Tag { name, .. } => name,
RegisteredTestSuiteProperty::Timeout { name, .. } => name,
}
}
pub fn crate_and_module(&self) -> String {
[self.crate_name(), self.module_path(), self.name()]
.into_iter()
.filter(|s| !s.is_empty())
.cloned()
.collect::<Vec<String>>()
.join("::")
}
}
pub static REGISTERED_TESTSUITE_PROPS: Mutex<Vec<RegisteredTestSuiteProperty>> =
Mutex::new(Vec::new());
#[derive(Clone)]
#[allow(clippy::type_complexity)]
pub enum TestGeneratorFunction {
Sync(Arc<dyn Fn() -> Vec<GeneratedTest> + Send + Sync + 'static>),
Async(
Arc<
dyn (Fn() -> Pin<Box<dyn Future<Output = Vec<GeneratedTest>> + Send>>)
+ Send
+ Sync
+ 'static,
>,
),
}
pub struct DynamicTestRegistration {
tests: Vec<GeneratedTest>,
}
impl Default for DynamicTestRegistration {
fn default() -> Self {
Self::new()
}
}
impl DynamicTestRegistration {
pub fn new() -> Self {
Self { tests: Vec::new() }
}
pub fn to_vec(self) -> Vec<GeneratedTest> {
self.tests
}
pub fn add_sync_test<R: TestReturnValue + 'static>(
&mut self,
name: impl AsRef<str>,
props: TestProperties,
dependencies: Option<Vec<String>>,
run: impl Fn(Arc<dyn DependencyView + Send + Sync>) -> R + Send + Sync + Clone + 'static,
) {
self.tests.push(GeneratedTest {
name: name.as_ref().to_string(),
run: TestFunction::Sync(Arc::new(move |deps| {
Box::new(run(deps)) as Box<dyn TestReturnValue>
})),
props,
dependencies,
});
}
#[cfg(feature = "tokio")]
pub fn add_async_test<R: TestReturnValue + 'static>(
&mut self,
name: impl AsRef<str>,
props: TestProperties,
dependencies: Option<Vec<String>>,
run: impl (Fn(Arc<dyn DependencyView + Send + Sync>) -> Pin<Box<dyn Future<Output = R> + Send>>)
+ Send
+ Sync
+ Clone
+ 'static,
) {
self.tests.push(GeneratedTest {
name: name.as_ref().to_string(),
run: TestFunction::Async(Arc::new(move |deps| {
let run = run.clone();
Box::pin(async move {
let r = run(deps).await;
Box::new(r) as Box<dyn TestReturnValue>
})
})),
props,
dependencies,
});
}
}
#[derive(Clone)]
pub struct GeneratedTest {
pub name: String,
pub run: TestFunction,
pub props: TestProperties,
pub dependencies: Option<Vec<String>>,
}
#[derive(Clone)]
pub struct RegisteredTestGenerator {
pub name: String,
pub crate_name: String,
pub module_path: String,
pub run: TestGeneratorFunction,
pub is_ignored: bool,
}
impl RegisteredTestGenerator {
pub fn crate_and_module(&self) -> String {
[&self.crate_name, &self.module_path]
.into_iter()
.filter(|s| !s.is_empty())
.cloned()
.collect::<Vec<String>>()
.join("::")
}
}
pub static REGISTERED_TEST_GENERATORS: Mutex<Vec<RegisteredTestGenerator>> = Mutex::new(Vec::new());
pub(crate) fn filter_test(test: &RegisteredTest, filter: &str, exact: bool) -> bool {
if let Some(tag_list) = filter.strip_prefix(":tag:") {
if tag_list.is_empty() {
test.props.tags.is_empty()
} else {
let or_tags = tag_list.split('|').collect::<Vec<&str>>();
let mut result = false;
for or_tag in or_tags {
let and_tags = or_tag.split('&').collect::<Vec<&str>>();
let mut and_result = true;
for and_tag in and_tags {
if !test.props.tags.contains(&and_tag.to_string()) {
and_result = false;
break;
}
}
if and_result {
result = true;
break;
}
}
result
}
} else if exact {
test.filterable_name() == filter
} else {
test.filterable_name().contains(filter)
}
}
pub(crate) fn apply_suite_props_to_tests(
tests: &[RegisteredTest],
props: &[RegisteredTestSuiteProperty],
) -> Vec<RegisteredTest> {
let props_with_prefix = props
.iter()
.map(|prop| (prop.crate_and_module(), prop))
.collect::<Vec<_>>();
let mut result = Vec::new();
for test in tests {
let mut test = test.clone();
for (prefix, prop) in &props_with_prefix {
if test.crate_and_module().starts_with(prefix) {
match prop {
RegisteredTestSuiteProperty::Tag { tag, .. } => {
test.props.tags.push(tag.clone());
}
RegisteredTestSuiteProperty::Timeout { timeout, .. } => {
if test.props.timeout.is_none() {
test.props.timeout = Some(*timeout);
}
}
RegisteredTestSuiteProperty::Sequential { .. } => {
}
}
}
}
result.push(test);
}
result
}
pub(crate) fn filter_registered_tests(
args: &Arguments,
registered_tests: &[RegisteredTest],
) -> Vec<RegisteredTest> {
registered_tests
.iter()
.filter(|registered_test| {
!args
.skip
.iter()
.any(|skip| filter_test(registered_test, skip, args.exact))
})
.filter(|registered_test| {
args.filter.is_empty()
|| args
.filter
.iter()
.any(|filter| filter_test(registered_test, filter, args.exact))
})
.filter(|registered_tests| {
(args.bench && registered_tests.run.is_bench())
|| (args.test && !registered_tests.run.is_bench())
|| (!args.bench && !args.test)
})
.filter(|registered_test| {
!args.exclude_should_panic || registered_test.props.should_panic == ShouldPanic::No
})
.cloned()
.collect::<Vec<_>>()
}
fn add_generated_tests(
target: &mut Vec<RegisteredTest>,
generator: &RegisteredTestGenerator,
generated: Vec<GeneratedTest>,
) {
target.extend(generated.into_iter().map(|mut test| {
test.props.is_ignored |= generator.is_ignored;
RegisteredTest {
name: format!("{}::{}", generator.name, test.name),
crate_name: generator.crate_name.clone(),
module_path: generator.module_path.clone(),
run: test.run,
props: test.props,
dependencies: test.dependencies,
}
}));
}
#[cfg(feature = "tokio")]
pub(crate) async fn generate_tests(generators: &[RegisteredTestGenerator]) -> Vec<RegisteredTest> {
let mut result = Vec::new();
for generator in generators {
match &generator.run {
TestGeneratorFunction::Sync(generator_fn) => {
let tests = generator_fn();
add_generated_tests(&mut result, generator, tests);
}
TestGeneratorFunction::Async(generator_fn) => {
let tests = generator_fn().await;
add_generated_tests(&mut result, generator, tests);
}
}
}
result
}
pub(crate) fn generate_tests_sync(generators: &[RegisteredTestGenerator]) -> Vec<RegisteredTest> {
let mut result = Vec::new();
for generator in generators {
match &generator.run {
TestGeneratorFunction::Sync(generator_fn) => {
let tests = generator_fn();
add_generated_tests(&mut result, generator, tests);
}
TestGeneratorFunction::Async(_) => {
panic!("Async test generators are not supported in sync mode")
}
}
}
result
}
pub(crate) fn get_ensure_time(args: &Arguments, test: &RegisteredTest) -> Option<TimeThreshold> {
let should_ensure_time = match test.props.ensure_time_control {
ReportTimeControl::Default => args.ensure_time,
ReportTimeControl::Enabled => true,
ReportTimeControl::Disabled => false,
};
if should_ensure_time {
match test.props.test_type {
TestType::UnitTest => Some(args.unit_test_threshold()),
TestType::IntegrationTest => Some(args.integration_test_threshold()),
}
} else {
None
}
}
#[derive(Clone)]
pub enum TestResult {
Passed {
captured: Vec<CapturedOutput>,
exec_time: Duration,
},
Benchmarked {
captured: Vec<CapturedOutput>,
exec_time: Duration,
ns_iter_summ: Summary,
mb_s: usize,
},
Failed {
cause: FailureCause,
captured: Vec<CapturedOutput>,
exec_time: Duration,
},
Ignored {
captured: Vec<CapturedOutput>,
},
}
impl TestResult {
pub fn passed(exec_time: Duration) -> Self {
TestResult::Passed {
captured: Vec::new(),
exec_time,
}
}
pub fn benchmarked(exec_time: Duration, ns_iter_summ: Summary, mb_s: usize) -> Self {
TestResult::Benchmarked {
captured: Vec::new(),
exec_time,
ns_iter_summ,
mb_s,
}
}
pub fn failed(exec_time: Duration, cause: FailureCause) -> Self {
TestResult::Failed {
cause,
captured: Vec::new(),
exec_time,
}
}
pub fn ignored() -> Self {
TestResult::Ignored {
captured: Vec::new(),
}
}
pub(crate) fn is_passed(&self) -> bool {
matches!(self, TestResult::Passed { .. })
}
pub(crate) fn is_benchmarked(&self) -> bool {
matches!(self, TestResult::Benchmarked { .. })
}
pub(crate) fn is_failed(&self) -> bool {
matches!(self, TestResult::Failed { .. })
}
pub(crate) fn is_ignored(&self) -> bool {
matches!(self, TestResult::Ignored { .. })
}
pub(crate) fn captured_output(&self) -> &Vec<CapturedOutput> {
match self {
TestResult::Passed { captured, .. } => captured,
TestResult::Failed { captured, .. } => captured,
TestResult::Ignored { captured, .. } => captured,
TestResult::Benchmarked { captured, .. } => captured,
}
}
pub(crate) fn stats(&self) -> Option<&Summary> {
match self {
TestResult::Benchmarked { ns_iter_summ, .. } => Some(ns_iter_summ),
_ => None,
}
}
pub(crate) fn set_captured_output(&mut self, captured: Vec<CapturedOutput>) {
match self {
TestResult::Passed {
captured: captured_ref,
..
} => *captured_ref = captured,
TestResult::Failed {
captured: captured_ref,
..
} => *captured_ref = captured,
TestResult::Ignored {
captured: captured_ref,
} => *captured_ref = captured,
TestResult::Benchmarked {
captured: captured_ref,
..
} => *captured_ref = captured,
}
}
pub(crate) fn from_result<A>(
should_panic: &ShouldPanic,
elapsed: Duration,
result: Result<Result<A, FailureCause>, Box<dyn Any + Send>>,
) -> Self {
match result {
Ok(Ok(_)) => {
if should_panic == &ShouldPanic::No {
TestResult::passed(elapsed)
} else {
TestResult::failed(
elapsed,
FailureCause::HarnessError("Test did not panic as expected".to_string()),
)
}
}
Ok(Err(cause)) => TestResult::failed(elapsed, cause),
Err(panic) => TestResult::from_panic(should_panic, elapsed, panic),
}
}
pub(crate) fn from_summary(
should_panic: &ShouldPanic,
elapsed: Duration,
result: Result<Summary, Box<dyn Any + Send>>,
bytes: u64,
) -> Self {
match result {
Ok(summary) => {
let ns_iter = max(summary.median as u64, 1);
let mb_s = bytes * 1000 / ns_iter;
TestResult::benchmarked(elapsed, summary, mb_s as usize)
}
Err(panic) => Self::from_panic(should_panic, elapsed, panic),
}
}
fn from_panic(
should_panic: &ShouldPanic,
elapsed: Duration,
panic: Box<dyn Any + Send>,
) -> Self {
let captured = crate::panic_hook::take_current_panic_capture();
let panic_cause = if let Some(cause) = captured {
cause
} else {
let message = panic
.downcast_ref::<String>()
.cloned()
.or(panic.downcast_ref::<&str>().map(|s| s.to_string()));
PanicCause {
message,
location: None,
backtrace: None,
}
};
match should_panic {
ShouldPanic::WithMessage(expected) => match &panic_cause.message {
Some(message) if message.contains(expected) => TestResult::passed(elapsed),
_ => TestResult::failed(
elapsed,
FailureCause::Panic(PanicCause {
message: Some(format!(
"Test panicked with unexpected message: {}",
panic_cause.message.as_deref().unwrap_or_default()
)),
location: None,
backtrace: None,
}),
),
},
ShouldPanic::Yes => TestResult::passed(elapsed),
ShouldPanic::No => TestResult::failed(elapsed, FailureCause::Panic(panic_cause)),
}
}
pub(crate) fn failure_message(&self) -> Option<String> {
self.failure_cause().map(|c| c.render())
}
pub fn failure_cause(&self) -> Option<&FailureCause> {
match self {
TestResult::Failed { cause, .. } => Some(cause),
_ => None,
}
}
}
pub struct SuiteResult {
pub passed: usize,
pub failed: usize,
pub ignored: usize,
pub measured: usize,
pub filtered_out: usize,
pub exec_time: Duration,
}
impl SuiteResult {
pub fn from_test_results(
registered_tests: &[RegisteredTest],
results: &[(RegisteredTest, TestResult)],
exec_time: Duration,
) -> Self {
let passed = results
.iter()
.filter(|(_, result)| result.is_passed())
.count();
let measured = results
.iter()
.filter(|(_, result)| result.is_benchmarked())
.count();
let failed = results
.iter()
.filter(|(_, result)| result.is_failed())
.count();
let ignored = results
.iter()
.filter(|(_, result)| result.is_ignored())
.count();
let filtered_out = registered_tests.len() - results.len();
Self {
passed,
failed,
ignored,
measured,
filtered_out,
exec_time,
}
}
pub fn exit_code(results: &[(RegisteredTest, TestResult)]) -> ExitCode {
if results.iter().any(|(_, result)| result.is_failed()) {
ExitCode::from(101)
} else {
ExitCode::SUCCESS
}
}
}
pub trait DependencyView: Debug {
fn get(&self, name: &str) -> Option<Arc<dyn Any + Send + Sync>>;
}
impl DependencyView for Arc<dyn DependencyView + Send + Sync> {
fn get(&self, name: &str) -> Option<Arc<dyn Any + Send + Sync>> {
self.as_ref().get(name)
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum CapturedOutput {
Stdout {
timestamp: SystemTime,
line: String,
},
Stderr {
timestamp: SystemTime,
line: String,
},
Host {
timestamp: SystemTime,
line: String,
},
}
impl CapturedOutput {
pub fn stdout(line: String) -> Self {
CapturedOutput::Stdout {
timestamp: SystemTime::now(),
line,
}
}
pub fn stderr(line: String) -> Self {
CapturedOutput::Stderr {
timestamp: SystemTime::now(),
line,
}
}
pub fn host(timestamp: SystemTime, line: String) -> Self {
CapturedOutput::Host { timestamp, line }
}
pub fn timestamp(&self) -> SystemTime {
match self {
CapturedOutput::Stdout { timestamp, .. } => *timestamp,
CapturedOutput::Stderr { timestamp, .. } => *timestamp,
CapturedOutput::Host { timestamp, .. } => *timestamp,
}
}
pub fn line(&self) -> &str {
match self {
CapturedOutput::Stdout { line, .. } => line,
CapturedOutput::Stderr { line, .. } => line,
CapturedOutput::Host { line, .. } => line,
}
}
}
impl PartialOrd for CapturedOutput {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CapturedOutput {
fn cmp(&self, other: &Self) -> Ordering {
self.timestamp().cmp(&other.timestamp())
}
}
#[cfg(test)]
mod error_reporting_tests {
use super::*;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::time::Duration;
fn simulate_runner(
test_fn: impl FnOnce() -> Box<dyn TestReturnValue> + std::panic::UnwindSafe,
) -> TestResult {
crate::panic_hook::install_panic_hook();
let test_id = crate::panic_hook::next_test_id();
crate::panic_hook::set_current_test_id(test_id);
let result = catch_unwind(AssertUnwindSafe(move || {
let ret = test_fn();
ret.into_result()?;
Ok(())
}));
let test_result =
TestResult::from_result(&ShouldPanic::No, Duration::from_millis(1), result);
crate::panic_hook::clear_current_test_id();
test_result
}
#[test]
fn panic_with_assert_eq() {
let result = simulate_runner(|| {
assert_eq!(1, 2);
Box::new(())
});
assert!(result.is_failed());
let msg = result.failure_message().unwrap();
println!("=== panic assert_eq failure message ===\n{msg}\n===");
assert!(
msg.contains("assertion `left == right` failed"),
"Expected assertion message, got: {msg}"
);
assert!(
msg.contains("at "),
"Expected location info in message, got: {msg}"
);
}
#[test]
fn string_error() {
let result = simulate_runner(|| {
let r: Result<(), String> = Err("something went wrong".to_string());
Box::new(r)
});
assert!(result.is_failed());
let msg = result.failure_message().unwrap();
println!("=== string error failure message ===\n{msg}\n===");
assert_eq!(msg, "something went wrong");
}
#[test]
fn anyhow_error() {
let result = simulate_runner(|| {
let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err = anyhow::anyhow!(inner).context("operation failed");
let r: Result<(), anyhow::Error> = Err(err);
Box::new(r)
});
assert!(result.is_failed());
let msg = result.failure_message().unwrap();
println!("=== anyhow error failure message ===\n{msg}\n===");
assert!(
msg.contains("operation failed"),
"Expected 'operation failed', got: {msg}"
);
assert!(
msg.contains("file not found"),
"Expected 'file not found', got: {msg}"
);
}
#[test]
fn std_io_error() {
let result = simulate_runner(|| {
let r: Result<(), std::io::Error> = Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found",
));
Box::new(r)
});
assert!(result.is_failed());
let msg = result.failure_message().unwrap();
println!("=== std io error failure message ===\n{msg}\n===");
assert_eq!(msg, "file not found");
}
#[test]
fn panic_with_location_info() {
let result = simulate_runner(|| {
panic!("test panic with location");
#[allow(unreachable_code)]
Box::new(())
});
assert!(result.is_failed());
let cause = result.failure_cause().unwrap();
match cause {
FailureCause::Panic(p) => {
assert!(p.location.is_some(), "Expected location info");
let loc = p.location.as_ref().unwrap();
assert!(
loc.file.contains("internal.rs"),
"Expected file to contain internal.rs, got: {}",
loc.file
);
assert!(loc.line > 0, "Expected non-zero line number");
}
other => panic!("Expected Panic cause, got: {other:?}"),
}
}
#[test]
fn panic_render_includes_location() {
let result = simulate_runner(|| {
panic!("location test");
#[allow(unreachable_code)]
Box::new(())
});
let msg = result.failure_message().unwrap();
assert!(
msg.contains("location test"),
"Expected panic message, got: {msg}"
);
assert!(
msg.contains("\n at "),
"Expected location line in render, got: {msg}"
);
}
#[test]
fn should_panic_with_message_matching() {
crate::panic_hook::install_panic_hook();
let test_id = crate::panic_hook::next_test_id();
crate::panic_hook::set_current_test_id(test_id);
let result = catch_unwind(AssertUnwindSafe(|| {
panic!("expected panic message");
}));
let test_result = TestResult::from_result(
&ShouldPanic::WithMessage("expected panic".to_string()),
Duration::from_millis(1),
result.map(|_| Ok(())),
);
crate::panic_hook::clear_current_test_id();
assert!(
test_result.is_passed(),
"Expected test to pass with matching panic message"
);
}
#[test]
fn should_panic_with_wrong_message() {
crate::panic_hook::install_panic_hook();
let test_id = crate::panic_hook::next_test_id();
crate::panic_hook::set_current_test_id(test_id);
let result = catch_unwind(AssertUnwindSafe(|| {
panic!("actual panic message");
}));
let test_result = TestResult::from_result(
&ShouldPanic::WithMessage("completely different".to_string()),
Duration::from_millis(1),
result.map(|_| Ok(())),
);
crate::panic_hook::clear_current_test_id();
assert!(
test_result.is_failed(),
"Expected test to fail with wrong panic message"
);
let msg = test_result.failure_message().unwrap();
assert!(
msg.contains("unexpected message"),
"Expected 'unexpected message' in: {msg}"
);
}
#[test]
fn pretty_assertions_diff() {
let result = simulate_runner(|| {
pretty_assertions::assert_eq!("hello world\nfoo\nbar\n", "hello world\nbaz\nbar\n");
Box::new(())
});
assert!(result.is_failed());
let cause = result.failure_cause().unwrap();
let panic_cause = match cause {
FailureCause::Panic(p) => p,
other => panic!("Expected Panic cause, got: {other:?}"),
};
let message = panic_cause.message.as_deref().unwrap();
println!("=== pretty_assertions failure message ===\n{message}\n===");
assert!(
message.contains("foo") && message.contains("baz"),
"Expected diff with 'foo' and 'baz', got: {message}"
);
assert!(panic_cause.location.is_some(), "Expected location info");
let rendered = cause.render();
println!("=== pretty_assertions rendered ===\n{rendered}\n===");
assert!(
!rendered.contains("stack backtrace") && !rendered.contains("Stack backtrace"),
"Expected no backtrace noise in rendered output, got: {rendered}"
);
assert!(
rendered.contains("\n at "),
"Expected location in rendered output, got: {rendered}"
);
}
#[test]
fn detached_thread_panic_detected() {
crate::panic_hook::install_panic_hook();
let test_id = crate::panic_hook::next_test_id();
crate::panic_hook::set_current_test_id(test_id);
crate::panic_hook::create_detached_collector(test_id);
let result = catch_unwind(AssertUnwindSafe(|| {
let handle = crate::spawn::spawn_thread(|| {
panic!("background thread panic");
});
let _ = handle.join();
}));
let mut test_result = TestResult::from_result(
&ShouldPanic::No,
Duration::from_millis(1),
result.map(|_| Ok(())),
);
if let Some(collector) = crate::panic_hook::take_detached_collector(test_id) {
let panics = match collector.lock() {
Ok(p) => p,
Err(poisoned) => poisoned.into_inner(),
};
if !panics.is_empty() && test_result.is_passed() {
let messages: Vec<String> = panics.iter().map(|p| p.render()).collect();
test_result = TestResult::failed(
Duration::from_millis(1),
FailureCause::Panic(PanicCause {
message: Some(format!(
"Detached task(s) panicked:\n{}",
messages.join("\n---\n")
)),
location: panics.first().and_then(|p| p.location.clone()),
backtrace: panics.first().and_then(|p| p.backtrace.clone()),
}),
);
}
}
crate::panic_hook::clear_current_test_id();
assert!(
test_result.is_failed(),
"Expected test to fail due to detached panic"
);
let msg = test_result.failure_message().unwrap();
assert!(
msg.contains("Detached task(s) panicked"),
"Expected detached panic message, got: {msg}"
);
assert!(
msg.contains("background thread panic"),
"Expected original panic message, got: {msg}"
);
}
#[test]
fn detached_thread_panic_ignored_with_policy() {
crate::panic_hook::install_panic_hook();
let test_id = crate::panic_hook::next_test_id();
crate::panic_hook::set_current_test_id(test_id);
crate::panic_hook::create_detached_collector(test_id);
let result = catch_unwind(AssertUnwindSafe(|| {
let handle = crate::spawn::spawn_thread(|| {
panic!("ignored thread panic");
});
let _ = handle.join();
}));
let test_result = TestResult::from_result(
&ShouldPanic::No,
Duration::from_millis(1),
result.map(|_| Ok(())),
);
if let Some(collector) = crate::panic_hook::take_detached_collector(test_id) {
let panics = match collector.lock() {
Ok(p) => p,
Err(poisoned) => poisoned.into_inner(),
};
assert!(
!panics.is_empty(),
"Expected panics in collector even with Ignore policy"
);
}
crate::panic_hook::clear_current_test_id();
assert!(
test_result.is_passed(),
"Expected test to pass with Ignore policy"
);
}
#[cfg(feature = "tokio")]
#[test]
fn detached_task_panic_detected() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
crate::panic_hook::install_panic_hook();
let test_id = crate::panic_hook::next_test_id();
crate::panic_hook::set_current_test_id(test_id);
crate::panic_hook::create_detached_collector(test_id);
let handle = crate::spawn::spawn(async {
panic!("detached task panic");
});
let _ = handle.await;
let collector = crate::panic_hook::take_detached_collector(test_id).unwrap();
let panics = collector.lock().unwrap();
assert_eq!(panics.len(), 1);
assert!(
panics[0]
.message
.as_ref()
.unwrap()
.contains("detached task panic"),
"Expected panic message, got: {:?}",
panics[0].message
);
crate::panic_hook::clear_current_test_id();
});
}
#[test]
fn failure_cause_variants() {
let cause = FailureCause::ReturnedMessage("simple message".to_string());
assert_eq!(cause.render(), "simple message");
assert!(cause.panic_message().is_none());
let cause = FailureCause::ReturnedError {
display: "display text".to_string(),
debug: "debug text".to_string(),
prefer_debug: false,
error: Arc::new("display text".to_string()),
};
assert_eq!(cause.render(), "display text");
let cause = FailureCause::ReturnedError {
display: "display text".to_string(),
debug: "debug text".to_string(),
prefer_debug: true,
error: Arc::new("debug text".to_string()),
};
assert_eq!(cause.render(), "debug text");
let cause = FailureCause::HarnessError("harness error".to_string());
assert_eq!(cause.render(), "harness error");
let cause = FailureCause::Panic(PanicCause {
message: Some("panic msg".to_string()),
location: None,
backtrace: None,
});
assert_eq!(cause.render(), "panic msg");
assert_eq!(cause.panic_message(), Some("panic msg"));
}
}
#[cfg(test)]
mod filter_tests {
use super::*;
fn make_test(name: &str, module_path: &str) -> RegisteredTest {
RegisteredTest {
name: name.to_string(),
crate_name: "mycrate".to_string(),
module_path: module_path.to_string(),
run: TestFunction::Sync(Arc::new(|_| Box::new(()))),
props: TestProperties::default(),
dependencies: None,
}
}
fn make_tagged_test(name: &str, module_path: &str, tags: Vec<&str>) -> RegisteredTest {
let mut test = make_test(name, module_path);
test.props.tags = tags.into_iter().map(String::from).collect();
test
}
fn make_args(filters: Vec<&str>, skip: Vec<&str>, exact: bool) -> Arguments {
Arguments {
filter: filters.into_iter().map(String::from).collect(),
skip: skip.into_iter().map(String::from).collect(),
exact,
..Default::default()
}
}
fn filtered_names(args: &Arguments, tests: &[RegisteredTest]) -> Vec<String> {
filter_registered_tests(args, tests)
.into_iter()
.map(|t| t.filterable_name())
.collect()
}
#[test]
fn filter_test_substring_match() {
let test = make_test("hello_world", "mod1");
assert!(filter_test(&test, "hello", false));
assert!(filter_test(&test, "world", false));
assert!(filter_test(&test, "mod1::hello", false));
assert!(!filter_test(&test, "nonexistent", false));
}
#[test]
fn filter_test_exact_match() {
let test = make_test("hello_world", "mod1");
assert!(filter_test(&test, "mod1::hello_world", true));
assert!(!filter_test(&test, "hello_world", true));
assert!(!filter_test(&test, "hello", true));
}
#[test]
fn filter_test_tag_match() {
let test = make_tagged_test("t1", "mod1", vec!["fast", "unit"]);
assert!(filter_test(&test, ":tag:fast", false));
assert!(filter_test(&test, ":tag:unit", false));
assert!(!filter_test(&test, ":tag:slow", false));
}
#[test]
fn filter_test_tag_empty_matches_untagged() {
let untagged = make_test("t1", "mod1");
let tagged = make_tagged_test("t2", "mod1", vec!["fast"]);
assert!(filter_test(&untagged, ":tag:", false));
assert!(!filter_test(&tagged, ":tag:", false));
}
#[test]
fn no_filters_includes_all() {
let tests = vec![make_test("a", "m"), make_test("b", "m")];
let args = make_args(vec![], vec![], false);
assert_eq!(filtered_names(&args, &tests), vec!["m::a", "m::b"]);
}
#[test]
fn single_filter_substring() {
let tests = vec![
make_test("alpha", "m"),
make_test("beta", "m"),
make_test("alphabet", "m"),
];
let args = make_args(vec!["alpha"], vec![], false);
assert_eq!(
filtered_names(&args, &tests),
vec!["m::alpha", "m::alphabet"]
);
}
#[test]
fn multiple_filters_or_semantics() {
let tests = vec![
make_test("alpha", "m"),
make_test("beta", "m"),
make_test("gamma", "m"),
];
let args = make_args(vec!["alpha", "gamma"], vec![], false);
assert_eq!(filtered_names(&args, &tests), vec!["m::alpha", "m::gamma"]);
}
#[test]
fn multiple_filters_exact() {
let tests = vec![
make_test("alpha", "m"),
make_test("alphabet", "m"),
make_test("beta", "m"),
];
let args = make_args(vec!["m::alpha", "m::beta"], vec![], true);
assert_eq!(filtered_names(&args, &tests), vec!["m::alpha", "m::beta"]);
}
#[test]
fn skip_substring_match() {
let tests = vec![
make_test("fast_test", "m"),
make_test("slow_test", "m"),
make_test("slower_test", "m"),
];
let args = make_args(vec![], vec!["slow"], false);
assert_eq!(filtered_names(&args, &tests), vec!["m::fast_test"]);
}
#[test]
fn skip_exact_match() {
let tests = vec![make_test("slow_test", "m"), make_test("slower_test", "m")];
let args = make_args(vec![], vec!["m::slow_test"], true);
assert_eq!(filtered_names(&args, &tests), vec!["m::slower_test"]);
}
#[test]
fn skip_with_tag() {
let tests = vec![
make_tagged_test("t1", "m", vec!["slow"]),
make_tagged_test("t2", "m", vec!["fast"]),
make_test("t3", "m"),
];
let args = make_args(vec![], vec![":tag:slow"], false);
assert_eq!(filtered_names(&args, &tests), vec!["m::t2", "m::t3"]);
}
#[test]
fn include_and_skip_combined() {
let tests = vec![
make_test("alpha_fast", "m"),
make_test("alpha_slow", "m"),
make_test("beta_fast", "m"),
];
let args = make_args(vec!["alpha"], vec!["slow"], false);
assert_eq!(filtered_names(&args, &tests), vec!["m::alpha_fast"]);
}
#[test]
fn skip_wins_over_include() {
let tests = vec![make_test("target", "m")];
let args = make_args(vec!["target"], vec!["target"], false);
assert_eq!(filtered_names(&args, &tests), Vec::<String>::new());
}
#[test]
fn filter_test_tag_or_expression() {
let test_a = make_tagged_test("t1", "m", vec!["a"]);
let test_b = make_tagged_test("t2", "m", vec!["b"]);
let test_c = make_tagged_test("t3", "m", vec!["c"]);
assert!(filter_test(&test_a, ":tag:a|b", false));
assert!(filter_test(&test_b, ":tag:a|b", false));
assert!(!filter_test(&test_c, ":tag:a|b", false));
}
#[test]
fn filter_test_tag_and_expression() {
let test_ab = make_tagged_test("t1", "m", vec!["a", "b"]);
let test_a = make_tagged_test("t2", "m", vec!["a"]);
let test_b = make_tagged_test("t3", "m", vec!["b"]);
assert!(filter_test(&test_ab, ":tag:a&b", false));
assert!(!filter_test(&test_a, ":tag:a&b", false));
assert!(!filter_test(&test_b, ":tag:a&b", false));
}
#[test]
fn filter_test_tag_mixed_and_or() {
let test_a = make_tagged_test("t1", "m", vec!["a"]);
let test_bc = make_tagged_test("t2", "m", vec!["b", "c"]);
let test_b = make_tagged_test("t3", "m", vec!["b"]);
let test_c = make_tagged_test("t4", "m", vec!["c"]);
let test_none = make_test("t5", "m");
assert!(filter_test(&test_a, ":tag:a|b&c", false));
assert!(filter_test(&test_bc, ":tag:a|b&c", false));
assert!(!filter_test(&test_b, ":tag:a|b&c", false));
assert!(!filter_test(&test_c, ":tag:a|b&c", false));
assert!(!filter_test(&test_none, ":tag:a|b&c", false));
}
#[test]
fn filter_test_tag_exact_flag_does_not_affect_tags() {
let test = make_tagged_test("t1", "m", vec!["fast"]);
assert!(filter_test(&test, ":tag:fast", true));
assert!(!filter_test(&test, ":tag:slow", true));
}
#[test]
fn include_by_tag_or_expression() {
let tests = vec![
make_tagged_test("t1", "m", vec!["unit"]),
make_tagged_test("t2", "m", vec!["integration"]),
make_tagged_test("t3", "m", vec!["e2e"]),
];
let args = make_args(vec![":tag:unit|integration"], vec![], false);
assert_eq!(filtered_names(&args, &tests), vec!["m::t1", "m::t2"]);
}
#[test]
fn skip_by_tag_and_expression() {
let tests = vec![
make_tagged_test("t1", "m", vec!["slow", "network"]),
make_tagged_test("t2", "m", vec!["slow"]),
make_tagged_test("t3", "m", vec!["network"]),
make_test("t4", "m"),
];
let args = make_args(vec![], vec![":tag:slow&network"], false);
assert_eq!(
filtered_names(&args, &tests),
vec!["m::t2", "m::t3", "m::t4"]
);
}
}