use crate::types::{int, string};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
pub struct T {
name: String,
failed: AtomicBool,
skipped: AtomicBool,
logbuf: Mutex<String>,
cleanups: Mutex<Vec<Box<dyn FnOnce() + Send>>>,
sub_failures: AtomicBool,
}
impl T {
#[doc(hidden)]
pub fn new(name: impl Into<String>) -> T {
T {
name: name.into(),
failed: AtomicBool::new(false),
skipped: AtomicBool::new(false),
logbuf: Mutex::new(String::new()),
cleanups: Mutex::new(Vec::new()),
sub_failures: AtomicBool::new(false),
}
}
#[allow(non_snake_case)]
pub fn Name(&self) -> string {
self.name.clone().into()
}
#[allow(non_snake_case)]
pub fn Failed(&self) -> bool {
self.failed.load(Ordering::SeqCst) || self.sub_failures.load(Ordering::SeqCst)
}
#[allow(non_snake_case)]
pub fn Skipped(&self) -> bool {
self.skipped.load(Ordering::SeqCst)
}
#[allow(non_snake_case)]
pub fn Log(&self, msg: impl AsRef<str>) {
self.append_log(msg.as_ref());
}
#[allow(non_snake_case)]
pub fn Error(&self, msg: impl AsRef<str>) {
self.append_log(msg.as_ref());
self.failed.store(true, Ordering::SeqCst);
}
#[allow(non_snake_case)]
pub fn Errorf(&self, msg: impl AsRef<str>) {
self.Error(msg);
}
#[allow(non_snake_case)]
pub fn Fatal(&self, msg: impl AsRef<str>) -> ! {
self.append_log(msg.as_ref());
self.failed.store(true, Ordering::SeqCst);
self.abort(Abort::FailNow);
}
#[allow(non_snake_case)]
pub fn Fatalf(&self, msg: impl AsRef<str>) -> ! { self.Fatal(msg) }
#[allow(non_snake_case)]
pub fn Logf(&self, msg: impl AsRef<str>) { self.Log(msg) }
#[allow(non_snake_case)]
pub fn Skip(&self, msg: impl AsRef<str>) -> ! {
self.append_log(msg.as_ref());
self.skipped.store(true, Ordering::SeqCst);
self.abort(Abort::SkipNow);
}
#[allow(non_snake_case)]
pub fn Skipf(&self, msg: impl AsRef<str>) -> ! { self.Skip(msg) }
#[allow(non_snake_case)]
pub fn FailNow(&self) -> ! {
self.failed.store(true, Ordering::SeqCst);
self.abort(Abort::FailNow);
}
#[allow(non_snake_case)]
pub fn SkipNow(&self) -> ! {
self.skipped.store(true, Ordering::SeqCst);
self.abort(Abort::SkipNow);
}
#[allow(non_snake_case)]
pub fn Fail(&self) {
self.failed.store(true, Ordering::SeqCst);
}
#[allow(non_snake_case)]
pub fn Helper(&self) {}
#[allow(non_snake_case)]
pub fn Cleanup<F: FnOnce() + Send + 'static>(&self, f: F) {
self.cleanups.lock().unwrap().push(Box::new(f));
}
#[allow(non_snake_case)]
pub fn Parallel(&self) {}
#[allow(non_snake_case)]
pub fn Run<F>(&self, name: impl AsRef<str>, f: F) -> bool
where
F: FnOnce(&T),
{
let full = format!("{}/{}", self.name, name.as_ref());
let sub = T::new(full);
let sub_ref = ⊂
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
f(sub_ref);
}));
sub.run_cleanups();
let sub_log = sub.logbuf.lock().unwrap().clone();
if !sub_log.is_empty() {
let mut g = self.logbuf.lock().unwrap();
g.push_str(&sub_log);
}
let sub_failed = sub.Failed();
let is_abort = matches!(&result, Err(e) if is_abort_panic(e));
if let Err(e) = result {
if !is_abort_panic(&e) { std::panic::resume_unwind(e); }
}
if sub_failed {
self.sub_failures.store(true, Ordering::SeqCst);
}
!sub_failed && !is_abort
}
#[doc(hidden)]
pub fn append_log(&self, s: &str) {
let mut g = self.logbuf.lock().unwrap();
if !g.is_empty() && !g.ends_with('\n') { g.push('\n'); }
g.push_str(s);
}
#[doc(hidden)]
pub fn log_contents(&self) -> string {
self.logbuf.lock().unwrap().clone().into()
}
#[doc(hidden)]
pub fn log_contents_raw(&self) -> std::string::String {
self.logbuf.lock().unwrap().clone()
}
#[doc(hidden)]
pub fn run_cleanups(&self) {
let mut g = self.cleanups.lock().unwrap();
while let Some(f) = g.pop() { f(); }
}
fn abort(&self, kind: Abort) -> ! {
std::panic::panic_any(kind);
}
#[doc(hidden)]
pub fn finish(&self, outcome: Outcome) -> std::result::Result<(), string> {
self.run_cleanups();
match outcome {
Outcome::Ok => {
if self.Failed() {
Err(self.log_contents())
} else {
Ok(())
}
}
Outcome::Aborted => {
if self.Skipped() && !self.failed.load(Ordering::SeqCst) {
Ok(())
} else {
Err(self.log_contents())
}
}
Outcome::Paniced(msg) => {
let mut log = self.log_contents_raw();
if !log.is_empty() && !log.ends_with('\n') { log.push('\n'); }
log.push_str(&format!("panic: {}", msg));
Err(log.into())
}
}
}
}
#[doc(hidden)]
#[derive(Debug)]
pub enum Abort { FailNow, SkipNow }
#[doc(hidden)]
pub fn is_abort_panic(e: &Box<dyn std::any::Any + Send>) -> bool {
e.is::<Abort>()
}
#[doc(hidden)]
pub enum Outcome {
Ok,
Aborted,
Paniced(String),
}
#[macro_export]
macro_rules! test {
(fn $name:ident ( $t:ident ) $body:block) => {
#[test]
#[allow(non_snake_case)]
fn $name() {
let __t = $crate::testing::T::new(stringify!($name));
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let $t: &$crate::testing::T = &__t;
$body
}));
let finished = match outcome {
Ok(()) => __t.finish($crate::testing::__priv::Outcome::Ok),
Err(e) if $crate::runtime::is_goexit_panic(&e) => {
__t.finish($crate::testing::__priv::Outcome::Ok)
}
Err(e) if $crate::testing::__priv::is_abort_panic(&e) => {
__t.finish($crate::testing::__priv::Outcome::Aborted)
}
Err(e) => {
let msg: std::string::String = if let Some(s) = e.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = e.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
};
__t.finish($crate::testing::__priv::Outcome::Paniced(msg))
}
};
if let Err(log) = finished {
panic!("{}", log);
} else if __t.Skipped() {
eprintln!("--- SKIP: {} ({})", __t.Name(), __t.log_contents());
}
}
};
}
#[macro_export]
macro_rules! test_h {
(fn $name:ident ( $t:ident ) $body:block) => {
#[allow(non_snake_case, dead_code)]
fn $name() {
let __t = $crate::testing::T::new(stringify!($name));
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let $t: &$crate::testing::T = &__t;
$body
}));
let finished = match outcome {
Ok(()) => __t.finish($crate::testing::__priv::Outcome::Ok),
Err(e) if $crate::runtime::is_goexit_panic(&e) => {
__t.finish($crate::testing::__priv::Outcome::Ok)
}
Err(e) if $crate::testing::__priv::is_abort_panic(&e) => {
__t.finish($crate::testing::__priv::Outcome::Aborted)
}
Err(e) => {
let msg: std::string::String = if let Some(s) = e.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = e.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
};
__t.finish($crate::testing::__priv::Outcome::Paniced(msg))
}
};
if let Err(log) = finished { panic!("{}", log); }
}
$crate::__goish_inventory::submit! {
$crate::testing::RegisteredTest {
name: stringify!($name),
run: $name,
}
}
};
}
#[doc(hidden)]
pub mod __priv {
pub use super::{is_abort_panic, Outcome};
}
use std::sync::OnceLock;
fn flags() -> &'static Flags {
static F: OnceLock<Flags> = OnceLock::new();
F.get_or_init(Flags::parse)
}
struct Flags { short: bool, verbose: bool }
impl Flags {
fn parse() -> Self {
let mut short = false;
let mut verbose = false;
for a in std::env::args() {
match a.as_str() {
"-short" | "--short" | "-test.short" => short = true,
"-v" | "--verbose" | "-test.v" => verbose = true,
_ => {}
}
}
Flags { short, verbose }
}
}
#[allow(non_snake_case)]
pub fn Short() -> bool { flags().short }
#[allow(non_snake_case)]
pub fn Verbose() -> bool { flags().verbose }
#[allow(non_snake_case)]
pub fn AllocsPerRun<F: FnMut()>(_runs: int, mut f: F) -> f64 {
f();
0.0
}
use std::time::{Duration, Instant};
pub struct B {
pub N: int,
report_allocs: AtomicBool,
bytes: std::sync::atomic::AtomicI64,
timer_running: bool,
elapsed: Duration,
last_start: Option<Instant>,
loop_counter: int,
}
impl B {
#[doc(hidden)]
pub fn new(n: int) -> B {
B {
N: n,
report_allocs: AtomicBool::new(false),
bytes: std::sync::atomic::AtomicI64::new(0),
timer_running: true,
elapsed: Duration::ZERO,
last_start: Some(Instant::now()),
loop_counter: n,
}
}
#[allow(non_snake_case)]
pub fn Loop(&mut self) -> bool {
if self.loop_counter > 0 {
self.loop_counter -= 1;
true
} else {
self.StopTimer();
false
}
}
#[allow(non_snake_case)]
pub fn ResetTimer(&mut self) {
self.elapsed = Duration::ZERO;
if self.timer_running {
self.last_start = Some(Instant::now());
}
}
#[allow(non_snake_case)]
pub fn StartTimer(&mut self) {
if !self.timer_running {
self.timer_running = true;
self.last_start = Some(Instant::now());
}
}
#[allow(non_snake_case)]
pub fn StopTimer(&mut self) {
if self.timer_running {
if let Some(t) = self.last_start.take() {
self.elapsed += t.elapsed();
}
self.timer_running = false;
}
}
#[allow(non_snake_case)]
pub fn ReportAllocs(&self) {
self.report_allocs.store(true, Ordering::SeqCst);
}
#[allow(non_snake_case)]
pub fn SetBytes(&self, n: int) {
self.bytes.store(n, Ordering::SeqCst);
}
#[doc(hidden)]
pub fn report(&mut self, name: &str) -> String {
self.StopTimer();
let ns = self.elapsed.as_nanos() as f64;
let ran = self.N as f64 - self.loop_counter as f64;
let ran = if ran < 1.0 { self.N as f64 } else { ran };
let ns_per_op = if ran > 0.0 { ns / ran } else { 0.0 };
let mut s = format!("{:<40} {:>10} {:>14.2} ns/op",
name, self.N - self.loop_counter, ns_per_op);
let bytes = self.bytes.load(Ordering::SeqCst);
if bytes > 0 && ns > 0.0 {
let mb_per_s = (bytes as f64 * ran) / (ns / 1e9) / (1024.0 * 1024.0);
s.push_str(&format!(" {:>8.2} MB/s", mb_per_s));
}
s
}
}
#[macro_export]
macro_rules! benchmark {
(fn $name:ident ( $b:ident ) $body:block) => {
#[test]
#[allow(non_snake_case)]
fn $name() {
let n: $crate::types::int = ::std::env::var("GOISH_BENCH_N")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(1000);
let mut __b = $crate::testing::B::new(n);
{
let $b: &mut $crate::testing::B = &mut __b;
$body
}
let line = __b.report(stringify!($name));
::std::eprintln!("{}", line);
}
};
}
pub struct RegisteredTest {
pub name: &'static str,
pub run: fn(),
}
inventory::collect!(RegisteredTest);
pub struct M {
filter: Option<String>,
verbose: bool,
}
impl M {
#[doc(hidden)]
pub fn new() -> Self {
M {
filter: std::env::args().find_map(|a| {
a.strip_prefix("-run=")
.or_else(|| a.strip_prefix("-test.run="))
.map(|s| s.into())
}),
verbose: std::env::args().any(|a| {
matches!(a.as_str(), "-v" | "--verbose" | "-test.v")
}),
}
}
#[allow(non_snake_case)]
pub fn Run(&self) -> int {
let pat: Option<crate::regexp::Regexp> =
self.filter.as_deref().map(|p| {
let (re, _err) = crate::regexp::Compile(p);
re
});
let mut ran = 0usize;
let mut failed = 0usize;
for t in inventory::iter::<RegisteredTest>() {
if let Some(re) = &pat {
if !re.MatchString(t.name) { continue; }
}
if self.verbose { eprintln!("=== RUN {}", t.name); }
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (t.run)()));
ran += 1;
match outcome {
Ok(()) => {
if self.verbose { eprintln!("--- PASS: {}", t.name); }
}
Err(_) => {
failed += 1;
eprintln!("--- FAIL: {}", t.name);
}
}
}
eprintln!("TestMain: ran {} tests, {} failed", ran, failed);
if failed == 0 { 0 } else { 1 }
}
}
impl Default for M {
fn default() -> Self { M::new() }
}
#[macro_export]
macro_rules! test_main {
(fn $name:ident ( $m:ident ) $body:block) => {
#[allow(dead_code, non_snake_case)]
fn $name(__m: &$crate::testing::M) {
let $m: &$crate::testing::M = __m;
$body
}
#[allow(dead_code)]
fn main() {
let __m = $crate::testing::M::new();
$name(&__m);
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn t_name_and_flags() {
let t = T::new("TestSelf");
assert_eq!(t.Name(), "TestSelf");
assert!(!t.Failed());
assert!(!t.Skipped());
}
#[test]
fn t_error_marks_failed() {
let t = T::new("X");
t.Error("oops");
assert!(t.Failed());
assert!(t.log_contents().contains("oops"));
}
#[test]
fn t_fatal_aborts_via_panic() {
let result = std::panic::catch_unwind(|| {
let t = T::new("X");
t.Fatal("boom");
});
assert!(result.is_err());
if let Err(e) = result {
assert!(is_abort_panic(&e));
}
}
#[test]
fn t_skip_aborts_and_marks_skipped() {
let t = std::sync::Arc::new(T::new("X"));
let tt = t.clone();
let result = std::panic::catch_unwind(move || {
tt.Skip("slow");
});
assert!(result.is_err());
assert!(t.Skipped());
}
#[test]
fn cleanup_runs_lifo() {
let log = std::sync::Arc::new(Mutex::new(Vec::<i32>::new()));
let t = T::new("X");
let l1 = log.clone(); t.Cleanup(move || l1.lock().unwrap().push(1));
let l2 = log.clone(); t.Cleanup(move || l2.lock().unwrap().push(2));
let l3 = log.clone(); t.Cleanup(move || l3.lock().unwrap().push(3));
t.run_cleanups();
assert_eq!(*log.lock().unwrap(), vec![3, 2, 1]);
}
#[test]
fn subtest_failure_propagates_to_parent() {
let t = T::new("Parent");
let ok = t.Run("sub", |sub| {
sub.Error("inner fail");
});
assert!(!ok);
assert!(t.Failed());
}
#[test]
fn subtest_pass_does_not_fail_parent() {
let t = T::new("Parent");
let ok = t.Run("sub", |_sub| { });
assert!(ok);
assert!(!t.Failed());
}
#[test]
fn errorf_method_accepts_sprintf() {
let t = T::new("X");
t.Errorf(crate::fmt::Sprintf!("got %d want %d", 1, 2));
assert!(t.Failed());
let log = t.log_contents();
assert!(log.contains("got 1 want 2"), "log = {:?}", log);
}
#[test]
fn short_and_verbose_do_not_panic() {
let _ = Short();
let _ = Verbose();
}
#[test]
fn b_loop_counts_down() {
let mut b = B::new(3);
let mut n = 0;
while b.Loop() { n += 1; }
assert_eq!(n, 3);
assert_eq!(b.N, 3);
}
#[test]
fn b_report_format() {
let mut b = B::new(100);
while b.Loop() { std::hint::black_box(1 + 1); }
let line = b.report("BenchmarkX");
assert!(line.contains("BenchmarkX"));
assert!(line.contains("ns/op"));
}
}