From ecc6d08170d6f4914fd90bf9fe574c657546cfae Mon Sep 17 00:00:00 2001 From: 张杨 Date: Wed, 18 Oct 2023 03:35:17 +0000 Subject: merge bind-rs-timeout --- bindings/rs-timeout/tests/test_timeout.rs | 793 ++++++++++++++++++++++++++++++ 1 file changed, 793 insertions(+) create mode 100644 bindings/rs-timeout/tests/test_timeout.rs (limited to 'bindings/rs-timeout/tests/test_timeout.rs') diff --git a/bindings/rs-timeout/tests/test_timeout.rs b/bindings/rs-timeout/tests/test_timeout.rs new file mode 100644 index 0000000..520e276 --- /dev/null +++ b/bindings/rs-timeout/tests/test_timeout.rs @@ -0,0 +1,793 @@ +use libc::free; +use rand::Rng; + +use rs_timeout::timeout_bind::timeout_version; +use rs_timeout::timeout_bind::*; +use std::{ + ffi::CStr, + io::{self, Write}, + usize, +}; + +static mut n_failed: i32 = 0; +const THE_END_OF_TIME: u64 = std::u64::MAX; + +macro_rules! DO { + ($fn:expr) => {{ + print!("."); + io::stdout().flush().unwrap(); + if $fn { + unsafe { + n_failed += 1; + } + println!("{} failed", stringify!($fn)); + } + }}; +} + +macro_rules! DO_N { + ($n:expr, $fn:expr) => {{ + for j in 0..$n { + DO!($fn); + } + }}; +} + +macro_rules! fail { + () => {{ + println!("Failure on line {}", line!()); + return true; + }}; +} + +fn check_misc() -> bool { + if (TIMEOUT_V_REL as i32) != unsafe { timeout_version() } { + return true; + } + if (TIMEOUT_V_REL as i32) != unsafe { timeout_v_rel() } { + return true; + } + if (TIMEOUT_V_API as i32) != unsafe { timeout_v_api() } { + return true; + } + if (TIMEOUT_V_ABI as i32) != unsafe { timeout_v_abi() } { + return true; + } + if unsafe { CStr::from_ptr(timeout_vendor()).to_bytes() } != TIMEOUT_VENDOR { + return true; + } + false +} + +fn check_open_close(hz_set: timeout_t, hz_expect: timeout_t) -> bool { + let mut err = 0 as usize; + let tos: *mut timeouts = unsafe { timeouts_open(hz_set, &mut err) }; + + if tos.is_null() { + return true; + } + if err != 0 { + return true; + } + if hz_expect != unsafe { timeouts_hz(tos) } { + return true; + } + false +} + +/* configuration for check_randomized */ +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct rand_cfg { + /* When creating timeouts, smallest possible delay */ + min_timeout: timeout_t, + /* When creating timeouts, largest possible delay */ + max_timeout: timeout_t, + /* First time to start the clock at. */ + start_at: timeout_t, + /* Do not advance the clock past this time. */ + end_at: timeout_t, + /* Number of timeouts to create and monitor. */ + n_timeouts: usize, + /* Advance the clock by no more than this each step. */ + max_step: timeout_t, + /* Use relative timers and stepping */ + // 实际上是 bool 值 + relative: usize, + /* Every time the clock ticks, try removing this many timeouts at + * random. */ + try_removing: usize, + /* When we're done, advance the clock to the end of time. */ + finalize: usize, +} + +/* Not very random */ +// fn random_to(min: timeout_t, max: timeout_t) -> timeout_t { +// let mut rng = rand::thread_rng(); +// if max <= min { +// return min; +// } +// /* Not actually all that random, but should exercise the code. */ +// let rand64 = rng.gen::() * (i32::MAX as timeout_t) + rng.gen::(); +// min + (rand64 % (max - min)) +// } + +fn random_to(min: u64, max: u64) -> u64 { + if max <= min { + return min; + } + let rand64 = rand::thread_rng().gen::(); + min + (rand64 % (max - min)) +} + +fn random() -> usize { + rand::thread_rng().gen::() +} + +fn check_randomized(cfg: &rand_cfg) -> bool { + let (i, rv) = (0, 0); + let mut err = 0 as usize; + + let mut t: Vec = vec![timeout::default(); cfg.n_timeouts]; + let mut timeouts: Vec = vec![0; cfg.n_timeouts]; + let mut fired: Vec = vec![0; cfg.n_timeouts]; + let mut found: Vec = vec![0; cfg.n_timeouts]; + let mut deleted: Vec = vec![0; cfg.n_timeouts]; + + let mut tos = unsafe { Some(timeouts_open(0, &mut err)) }; // manager 的角色 + let mut now = cfg.start_at; + + let (mut n_added_pending, mut cnt_added_pending, mut n_added_expired, mut cnt_added_expired) = + (0, 0, 0, 0); + + let (mut it_p, mut it_e, mut it_all) = ( + timeouts_it::default(), + timeouts_it::default(), + timeouts_it::default(), + ); + + let (mut p_done, mut e_done, mut all_done) = (false, false, false); + // let mut to: Option<&mut timeout> = None; + let mut to: *mut timeout = std::ptr::null_mut(); + let rel = cfg.relative; + + // 对应 done + let cleanup = |tos, + t: Vec, + timeouts: Vec, + fired: Vec, + found: Vec, + deleted: Vec| { + if let Some(tos) = tos { + unsafe { timeouts_close(tos) }; + } + if t.is_empty() { + drop(t); + // unsafe { free(t) }; + } + if !timeouts.is_empty() { + drop(timeouts); + // unsafe { free(timeouts) }; + } + if !fired.is_empty() { + drop(fired); + // unsafe { free(fired) }; + } + if !found.is_empty() { + drop(found); + // unsafe { free(found) }; + } + if !deleted.is_empty() { + drop(deleted); + // unsafe { free(deleted) }; + } + }; + + if t.is_empty() + || timeouts.is_empty() + || tos.is_none() + || fired.is_empty() + || found.is_empty() + || deleted.is_empty() + { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + if let Some(mut tos) = tos { + unsafe { timeouts_update(tos, cfg.start_at) }; // manger 写入开始时间 + } + + // test-timeout.c line 98 + for i in 0..cfg.n_timeouts { + // 初始化 timeout + if &t[i] as *const _ + != unsafe { timeout_init(&mut t[i], if rel > 0 { 0 } else { TIMEOUT_ABS }) } as *const _ + { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + // 检查 timeout 的状态 + if unsafe { timeout_pending(&mut t[i]) || timeout_expired(&mut t[i]) } { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + + timeouts[i] = random_to(cfg.min_timeout, cfg.max_timeout); // 随机取超时时间 [min_timeout, max_timeout)] + + unsafe { + if let Some(temp) = tos { + timeouts_add( + temp, // manger + &mut t[i], // timeout + timeouts[i] - (if rel > 0 { now } else { 0 }), // 超时时间 + ) + } + } + // 超时时间 小于 开始时间? + if timeouts[i] <= cfg.start_at { + // 如果还在等待 且 没有过期 + if unsafe { timeout_pending(&mut t[i]) || !timeout_expired(&mut t[i]) } { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + n_added_expired += 1; // 计数 + } else { + // 如果 没有等待 且 已经过期了 + if unsafe { !timeout_pending(&mut t[i]) || timeout_expired(&mut t[i]) } { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + n_added_pending += 1; // 计数 + } + } + + // test-timeout.c line 124 + // 检查 等待事件数量 与 计数 是否相等 + if unsafe { + if let Some(temp) = tos { + (n_added_pending != 0) != timeouts_pending(temp) + } else { + true + } + } { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + // test-timeout.c line 126 + // 检查 过期事件数量 与 计数 是否相等 + if unsafe { + if let Some(temp) = tos { + (n_added_expired != 0) != timeouts_expired(temp) + } else { + true + } + } { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + + TIMEOUTS_IT_INIT(&mut it_p, TIMEOUTS_PENDING); // 等待 + TIMEOUTS_IT_INIT(&mut it_e, TIMEOUTS_EXPIRED); // 过期 + TIMEOUTS_IT_INIT(&mut it_all, TIMEOUTS_ALL); // 全部 + + // timeout.c line 133 + while !(p_done && e_done && all_done) { + if !p_done { + if let Some(temp) = tos { + let to = unsafe { timeouts_next(temp, &mut it_p) }; // 等待队列的下一个 + if !to.is_null() { + // c 语言中的指针运算,这里只能以 内存地址长度/ timeout 类型长度,来进行运算 + // 找到 to 在 t[] 的位置 + let i = + (to as usize - &t[0] as *const _ as usize) / std::mem::size_of::(); + found[i] += 1; // 事件的状态 + cnt_added_pending += 1; + } else { + // to 为空,没有等待的 timeout 了 + p_done = true; + } + } + } + if !e_done { + if let Some(temp) = tos { + let to = unsafe { timeouts_next(temp, &mut it_e) }; // 过期队列的下一个 + if !to.is_null() { + // c 语言中的指针运算,这里只能以 内存地址长度/ timeout 类型长度,来进行运算 + let i = + (to as usize - &t[0] as *const _ as usize) / std::mem::size_of::(); + found[i] += 1; + cnt_added_expired += 1; + } else { + // to 为空,没有过期的 timeout 了 + e_done = true; + } + } + } + if !all_done { + if let Some(temp) = tos { + let to = unsafe { timeouts_next(temp, &mut it_all) }; + if !to.is_null() { + // c 语言中的指针运算,这里只能以 内存地址长度/ timeout 类型长度,来进行运算 + let i = + (to as usize - &t[0] as *const _ as usize) / std::mem::size_of::(); + found[i] += 1; + } else { + // to 为空,没有 timeout 了 + all_done = true; + } + } + } + } + // timeout.c line 164 + for i in 0..cfg.n_timeouts { + // 事件经历 等待 or 超时 + all 两次 + if found[i] != 2 { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + } + if cnt_added_expired != n_added_expired { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + if cnt_added_pending != n_added_pending { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + // timeout.c line 174 + if let Some(temp) = tos { + loop { + let to: *mut timeout = unsafe { timeouts_get(temp) }; // 任何已经过期的事件, 直到没有超时事件 + if to.is_null() { + break; + } + let i = (to as usize - &t[0] as *const _ as usize) / std::mem::size_of::(); // 找到 to 在 t[] 的位置 + assert_eq!(&t[i] as *const _, to); + if timeouts[i] > cfg.start_at { + //已过期的 事件绝对 + /* shouldn't have happened yet */ + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + n_added_expired -= 1; /* drop expired timeouts. */ + fired[i] += 1; + } + } + // 所有 过期事件 已经经过遍历了 + if n_added_expired != 0 { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + + // test-timeout.c line 187 + // 时间流逝到 end_at + while now < cfg.end_at { + let mut n_fired_this_time = 0; + + // test-timeout.c line 189 + // timeouts_timeout 取到一定程度会返回 u64 最大值, + // c 语言中 max_u64 + now == now -1 ; 但 rust 会直接 panic; + let temp = unsafe { timeouts_timeout(tos.unwrap()) }; // 下一次更新所需要的间隔 ?? + let first_at = match temp.checked_add(now) { + Some(v) => v, + None => now - 1, // 溢出则按照 C 语言执行结果处理 -1; + }; // 过期时间 第一个事件的 时间 + + let oldtime = now; // 记录上一次的时间 + let step = random_to(1, cfg.max_step); // 随机取步长 [1, max_step) + now += step; // 时间流逝 + + if rel != 0 { + unsafe { timeouts_step(tos.unwrap(), step) }; // 相对时间更新 计时轮 + } else { + unsafe { timeouts_update(tos.unwrap(), now) }; // 绝对时间更新 计时轮 + } + // 每次时钟 滴答时,尝试 随机删除 try_removing 个 timeout + for _i in 0..cfg.try_removing { + let idx = random() % cfg.n_timeouts; // 随机取个 timeout + // 超时已经被遍历过了 + if !(fired[idx] == 0) { + unsafe { + timeout_del(&mut t[idx]); + } + deleted[idx] += 1; + } + } + + let mut another = unsafe { timeouts_timeout(tos.unwrap()) } == 0; // 下一次更新所需要的间隔 是否 == 0, + + loop { + let to = unsafe { timeouts_get(tos.unwrap()) }; // 任何已经过期的事件, 直到没有超时事件 + if to.is_null() { + break; + } + if !another { + /* Thought we saw the last one! */ + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + + let i = (to as usize - &t[0] as *const _ as usize) / std::mem::size_of::(); // 找到 to 在 t[] 的位置 + assert_eq!(&t[i] as *const _, to); + // 已经过期时间 超时时间不会比现在还小 + if timeouts[i] > now { + /* shouldn't have happened yet */ + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + // 已经过期时间 超时时间不会比上一次的时间还小 | 上次的过期事件绝对在上次已经处理完毕了. + if timeouts[i] <= oldtime { + /* should have happened already*/ + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + // 已经过期时间 超时时间不会比 这次更新需要的最小 时间间隔还小 + if timeouts[i] < first_at { + /* first_at should've been earlier */ + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + fired[i] += 1; // 过期数组 计数 + n_fired_this_time += 1; // + another = (unsafe { timeouts_timeout(tos.unwrap()) } == 0); + } + // 这轮处理过 超时事件 且 第一个超时事件的时间点 > now + if (n_fired_this_time != 0) && (first_at > now) { + /* first_at should've been earlier */ + // 这里的处理逻辑是,如果有超时事件,那么第一个超时事件的时间点应该是 <= now + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + // 这轮处理过 超时事件 且 下一次更新所需要的间隔 == 0, 不可能出现. + if another { + /* Huh? We think there are more? */ + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + // 有效?? + if unsafe { !timeouts_check(tos.unwrap(), stderr) } { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + } + + // test-timout.c line 233 + for i in 0..cfg.n_timeouts { + // 过期 了两次 + if fired[i] > 1 { + /* Nothing fired twice. */ + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + // 超时时间 小于 开始时间? + if timeouts[i] <= now { + // 已经过期了, 却 还没有过期 且 没有删除 ? + if (fired[i] == 0) && (deleted[i] == 0) { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + } else { + // 没有过期, 但被标记为 已经过期了 + if fired[i] != 0 { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + } + // 过期了 且 删除了 + if (fired[i] != 0) && (deleted[i] != 0) { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + // 完成后 将时钟 更新到 timeout 的最大值 + if cfg.finalize > 1 { + if !(fired[i] != 0) { + unsafe { + timeout_del(&mut t[i]); + } + } + } + } + + // test-timeout.c line 251 + /* Now nothing more should fire between now and the end of time. */ + // 从 end_t 到 max_t 不应该再有事件了 + if cfg.finalize > 0 { + // 时间移动到 timeout 的尽头 + unsafe { timeouts_update(tos.unwrap(), THE_END_OF_TIME) } + // + if cfg.finalize > 1 { + // 任何已经过期的事件 + let tmpe = unsafe { timeouts_get(tos.unwrap()) }; + // 如果还有 事件 + if !tmpe.is_null() { + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + let mut _it = TIMEOUTS_IT_INITIALIZER(TIMEOUTS_ALL); // 所有队列的事件 + loop { + to = unsafe { timeouts_next(tos.unwrap(), &mut _it) }; + if to.is_null() { + break; + } + // 如何还有事件 + cleanup(tos, t, timeouts, fired, found, deleted); + fail!(); + } + } + } + + cleanup(tos, t, timeouts, fired, found, deleted); + return false; +} + +struct intervals_cfg<'a> { + timeouts: &'a [timeout_t], + n_timeouts: usize, + start_at: timeout_t, + end_at: timeout_t, + skip: timeout_t, +} + +fn check_intervals(cfg: &intervals_cfg) -> bool { + let (mut i, rv) = (0, 0); + let mut err = 0 as usize; + + let mut to: *mut timeout = std::ptr::null_mut(); + let mut t: Vec = vec![timeout::default(); cfg.n_timeouts as usize]; + let mut fired: Vec = vec![0; cfg.n_timeouts]; + let mut tos = unsafe { Some(timeouts_open(0, &mut err)) }; // manger + + let mut now = cfg.start_at; + + // 对应 done + let cleanup = |t: Vec, tos: Option<*mut timeouts>, fired: Vec| { + if t.is_empty() { + drop(t); + // unsafe { free(t) }; + } + if let Some(tos) = tos { + unsafe { timeouts_close(tos) }; + } + if !fired.is_empty() { + drop(fired); + // unsafe { free(fired) }; + } + }; + + if t.is_empty() || tos.is_none() || fired.is_empty() { + cleanup(t, tos, fired); + fail!(); + } + + unsafe { timeouts_update(tos.unwrap(), now) }; // 时钟行进到 now + // test-timeout.c line 300 + for i in 0..cfg.n_timeouts { + // 初始化 按照绝对时间 计算超时 + if &t[i] as *const _ != unsafe { timeout_init(&mut t[i], TIMEOUT_INT) } as *const _ { + cleanup(t, tos, fired); + fail!(); + } + // timeout 等待(在时间轮上) 或 过期 + if unsafe { timeout_pending(&mut t[i]) || timeout_expired(&mut t[i]) } { + cleanup(t, tos, fired); + fail!(); + } + // 添加 timeout 到 时间轮上 + unsafe { timeouts_add(tos.unwrap(), &mut t[i], cfg.timeouts[i]) } + // 没有在时间轮上 且 已过期 + if unsafe { (!timeout_pending(&mut t[i])) || timeout_expired(&mut t[i]) } { + cleanup(t, tos, fired); + fail!(); + } + } + // test-timeout.c line 315 + while now < cfg.end_at { + let mut delay = unsafe { timeouts_timeout(tos.unwrap()) }; // 触发下一个 超时事件 最小时间间隔 + // 有 跳过间歇 且 delay < 间歇 + if (cfg.skip != 0) && (delay < cfg.skip) { + delay = cfg.skip; // delay = 间歇 + } + unsafe { timeouts_step(tos.unwrap(), delay) } // 更新时钟 (相对方式) + now += delay; // 时间流逝 + + loop { + let to = unsafe { timeouts_get(tos.unwrap()) }; // + if to.is_null() { + break; + } + i = (to as usize - &t[0] as *const _ as usize) / std::mem::size_of::(); + assert_eq!(&t[i] as *const _, to); + + fired[i] += 1; + + if 0 != (unsafe { (*to).expires - cfg.start_at } % cfg.timeouts[i]) { + cleanup(t, tos, fired); + fail!(); + } + if unsafe { (*to).expires <= now } { + cleanup(t, tos, fired); + fail!(); + } + if unsafe { (*to).expires > now + cfg.timeouts[i] } { + cleanup(t, tos, fired); + fail!(); + } + } + if unsafe { !timeouts_check(tos.unwrap(), stderr) } { + cleanup(t, tos, fired); + fail!(); + } + } + + let duration = now - cfg.start_at; + for i in 0..cfg.n_timeouts { + if cfg.skip != 0 { + if fired[i] as u64 > (duration / cfg.timeouts[i]) { + cleanup(t, tos, fired); + fail!(); + } + } else { + if fired[i] as u64 != (duration / cfg.timeouts[i]) { + println!("{} != {}", fired[i], duration / cfg.timeouts[i]); + cleanup(t, tos, fired); + fail!(); + } + } + if unsafe { !timeout_pending(&mut t[i]) } { + cleanup(t, tos, fired); + fail!(); + } + } + + return false; +} + +#[test] +fn main() { + unsafe { + n_failed = 0; + } + DO!(check_misc()); + DO!(check_open_close(1000, 1000)); + DO!(check_open_close(0, TIMEOUT_mHZ)); + + let cfg1 = rand_cfg { + min_timeout: 1, + max_timeout: 100, + start_at: 5, + end_at: 1000, + n_timeouts: 1000, + max_step: 10, + relative: 0, + try_removing: 0, + finalize: 2, + }; + DO_N!(300, check_randomized(&cfg1)); + + let cfg2 = rand_cfg { + min_timeout: 20, + max_timeout: 1000, + start_at: 5, + end_at: 100, + n_timeouts: 1000, + max_step: 5, + relative: 1, + try_removing: 0, + finalize: 2, + }; + DO_N!(300, check_randomized(&cfg2)); + + let cfg2b = rand_cfg { + min_timeout: 20, + max_timeout: 1000, + start_at: 10, + end_at: 100, + n_timeouts: 1000, + max_step: 5, + relative: 1, + try_removing: 0, + finalize: 1, + }; + DO_N!(300, check_randomized(&cfg2b)); + + let cfg2c = rand_cfg { + min_timeout: 20, + max_timeout: 1000, + start_at: 10, + end_at: 100, + n_timeouts: 1000, + max_step: 5, + relative: 1, + try_removing: 0, + finalize: 0, + }; + DO_N!(300, check_randomized(&cfg2c)); + + let cfg3 = rand_cfg { + min_timeout: 2000, + max_timeout: 1 << 50, + start_at: 100, + end_at: 1 << 49, + n_timeouts: 1000, + max_step: 1 << 31, + relative: 0, + try_removing: 0, + finalize: 2, + }; + DO_N!(10, check_randomized(&cfg3)); + + let cfg3b = rand_cfg { + min_timeout: 1 << 50, + max_timeout: 1 << 52, + start_at: 100, + end_at: 1 << 53, + n_timeouts: 1000, + max_step: 1 << 48, + relative: 0, + try_removing: 0, + finalize: 2, + }; + DO_N!(10, check_randomized(&cfg3b)); + + let cfg4 = rand_cfg { + min_timeout: 2000, + max_timeout: 1 << 30, + start_at: 100, + end_at: 1 << 26, + n_timeouts: 10000, + max_step: 1 << 16, + relative: 0, + try_removing: 0, + finalize: 2, + }; + DO_N!(10, check_randomized(&cfg4)); + + const primes: [u64; 25] = [ + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, + 97, + ]; + const factors_of_1337: [u64; 4] = [1, 7, 191, 1337]; + const multiples_of_five: [u64; 10] = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50]; + + let icfg1 = intervals_cfg { + timeouts: &primes, + n_timeouts: primes.len(), + start_at: 50, + end_at: 5322, + skip: 0, + }; + DO!(check_intervals(&icfg1)); + + let icfg2 = intervals_cfg { + timeouts: &factors_of_1337, + n_timeouts: factors_of_1337.len(), + start_at: 50, + end_at: 50000, + skip: 0, + }; + DO!(check_intervals(&icfg2)); + + let icfg3 = intervals_cfg { + timeouts: &multiples_of_five, + n_timeouts: multiples_of_five.len(), + start_at: 49, + end_at: 5333, + skip: 0, + }; + DO!(check_intervals(&icfg3)); + + let icfg4 = intervals_cfg { + timeouts: &primes, + n_timeouts: primes.len(), + start_at: 50, + end_at: 5322, + skip: 16, + }; + DO!(check_intervals(&icfg4)); + + if unsafe { n_failed } != 0 { + println!("{} tests failed", unsafe { n_failed }); + } else { + println!("OK"); + } +} -- cgit v1.2.3