diff --git a/Cargo.toml b/Cargo.toml index ef34d337..98296e10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,11 +27,6 @@ auto-country = ["dep:country-boundaries"] auto-timezone = ["dep:chrono-tz", "dep:tzf-rs"] log = ["opening-hours-syntax/log", "dep:log"] -# Disable timeout behavior for performance tests. This is useful when tests -# need to be in slow environments or with high overhead, for example when -# measuring coverage. -disable-test-timeouts = [] - [dependencies] chrono = "0.4" compact-calendar = { path = "compact-calendar", version = "1.1.0" } diff --git a/opening-hours/src/opening_hours.rs b/opening-hours/src/opening_hours.rs index 08796bfb..0cc4f83a 100644 --- a/opening-hours/src/opening_hours.rs +++ b/opening-hours/src/opening_hours.rs @@ -124,6 +124,9 @@ impl OpeningHours { /// Get the schedule at a given day. pub fn schedule_at(&self, date: NaiveDate) -> Schedule { + #[cfg(test)] + crate::tests::stats::notify::generated_schedule(); + if !(DATE_START.date()..DATE_END.date()).contains(&date) { return Schedule::default(); } diff --git a/opening-hours/src/tests/mod.rs b/opening-hours/src/tests/mod.rs index adb4ed38..84063664 100644 --- a/opening-hours/src/tests/mod.rs +++ b/opening-hours/src/tests/mod.rs @@ -1,3 +1,5 @@ +pub(crate) mod stats; + mod country; mod holiday_selector; mod issues; @@ -13,11 +15,6 @@ mod week_selector; mod weekday_selector; mod year_selector; -use criterion::black_box; -use std::sync::{Arc, Condvar, Mutex}; -use std::thread; -use std::time::{Duration, Instant}; - fn sample() -> impl Iterator { include_str!("data/sample.txt") .lines() @@ -25,66 +22,6 @@ fn sample() -> impl Iterator { .filter(|line| !line.is_empty() && !line.starts_with('#')) } -/// Wraps input function but panics if it runs longer than specified timeout. -pub fn exec_with_timeout( - timeout: Duration, - f: impl FnOnce() -> R + Send + 'static, -) -> R { - if cfg!(feature = "disable-test-timeouts") { - return f(); - } - - let result = Arc::new(Mutex::new((None, false))); - let finished = Arc::new(Condvar::new()); - - struct DetectPanic(Arc, bool)>>); - - impl Drop for DetectPanic { - fn drop(&mut self) { - if thread::panicking() { - self.0.lock().expect("failed to write panic").1 = true; - } - } - } - - let _runner = { - let f = black_box(f); - let result = result.clone(); - let finished = finished.clone(); - - thread::spawn(move || { - let _panic_guard = DetectPanic(result.clone()); - - let elapsed = { - let start = Instant::now(); - let res = f(); - (start.elapsed(), res) - }; - - result.lock().expect("failed to write result").0 = Some(elapsed); - finished.notify_all(); - }) - }; - - let (mut result, _) = finished - .wait_timeout(result.lock().expect("failed to fetch result"), timeout) - .expect("poisoned lock"); - - if result.1 { - panic!("exec stopped due to panic"); - } - - let Some((elapsed, res)) = result.0.take() else { - panic!("exec stopped due to {timeout:?} timeout"); - }; - - if elapsed > timeout { - panic!("exec ran for {elapsed:?}"); - } - - res -} - #[macro_export] macro_rules! date { ( $date: expr ) => {{ diff --git a/opening-hours/src/tests/regression.rs b/opening-hours/src/tests/regression.rs index 43aba105..5a7c0038 100644 --- a/opening-hours/src/tests/regression.rs +++ b/opening-hours/src/tests/regression.rs @@ -1,10 +1,8 @@ -use std::time::Duration; - use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; use opening_hours_syntax::error::Error; use opening_hours_syntax::rules::RuleKind::*; -use crate::tests::exec_with_timeout; +use crate::tests::stats::TestStats; use crate::{datetime, schedule_at, OpeningHours}; #[test] @@ -127,26 +125,24 @@ fn s009_pj_no_open_before_separator() { } #[test] -fn s010_pj_slow_after_24_7() -> Result<(), Error> { - exec_with_timeout(Duration::from_millis(100), || { - assert!("24/7 open ; 2021Jan-Feb off" - .parse::()? +fn s010_pj_slow_after_24_7() { + let stats = TestStats::watch(|| { + assert!(OpeningHours::parse("24/7 open ; 2021Jan-Feb off") + .unwrap() .next_change(datetime!("2021-07-09 19:30")) .is_none()); + }); - Ok::<(), Error>(()) - })?; + assert!(stats.count_generated_schedules < 10); - exec_with_timeout(Duration::from_millis(100), || { - assert!("24/7 open ; 2021 Jan 01-Feb 10 off" - .parse::()? + let stats = TestStats::watch(|| { + assert!(OpeningHours::parse("24/7 open ; 2021 Jan 01-Feb 10 off") + .unwrap() .next_change(datetime!("2021-07-09 19:30")) .is_none()); + }); - Ok::<(), Error>(()) - })?; - - Ok(()) + assert!(stats.count_generated_schedules < 10); } #[test] @@ -159,38 +155,37 @@ fn s011_fuzz_extreme_year() -> Result<(), Error> { ); assert!(oh.is_closed(dt)); - assert!(oh.next_change(dt).is_none()); + assert_eq!(oh.next_change(dt).unwrap(), datetime!("2000-01-01 00:00")); Ok(()) } #[test] -fn s012_fuzz_slow_sh() -> Result<(), Error> { - exec_with_timeout(Duration::from_millis(100), || { - assert!("SH" - .parse::()? +fn s012_fuzz_slow_sh() { + let stats = TestStats::watch(|| { + assert!(OpeningHours::parse("SH") + .unwrap() .next_change(datetime!("2020-01-01 00:00")) .is_none()); + }); - Ok(()) - }) + assert!(stats.count_generated_schedules < 10); } #[test] -fn s013_fuzz_slow_weeknum() -> Result<(), Error> { - exec_with_timeout(Duration::from_millis(200), || { - assert!("Novweek09" - .parse::()? +fn s013_fuzz_slow_weeknum() { + let stats = TestStats::watch(|| { + assert!(OpeningHours::parse("Novweek09") + .unwrap() .next_change(datetime!("2020-01-01 00:00")) .is_none()); + }); - Ok(()) - }) + assert!(stats.count_generated_schedules < 50_000); } #[test] fn s014_fuzz_feb30_before_leap_year() -> Result<(), Error> { - "Feb30" - .parse::()? + OpeningHours::parse("Feb30")? .next_change(datetime!("4419-03-01 00:00")) .unwrap(); @@ -224,26 +219,26 @@ fn s016_fuzz_week01_sh() -> Result<(), Error> { } #[test] -fn s017_fuzz_open_range_timeout() -> Result<(), Error> { - exec_with_timeout(Duration::from_millis(100), || { +fn s017_fuzz_open_range_timeout() { + let stats = TestStats::watch(|| { assert_eq!( - "May2+" - .parse::()? + OpeningHours::parse("May2+") + .unwrap() .next_change(datetime!("2020-01-01 12:00")) .unwrap(), datetime!("2020-05-02 00:00") ); assert_eq!( - "May2+" - .parse::()? + OpeningHours::parse("May2+") + .unwrap() .next_change(datetime!("2020-05-15 12:00")) .unwrap(), datetime!("2021-01-01 00:00") ); + }); - Ok(()) - }) + assert!(stats.count_generated_schedules < 10); } #[cfg(feature = "auto-country")] diff --git a/opening-hours/src/tests/rules.rs b/opening-hours/src/tests/rules.rs index 8092a4e7..2c9de56c 100644 --- a/opening-hours/src/tests/rules.rs +++ b/opening-hours/src/tests/rules.rs @@ -1,6 +1,4 @@ -use std::time::Duration; - -use crate::tests::exec_with_timeout; +use crate::tests::stats::TestStats; use opening_hours_syntax::error::Error; use opening_hours_syntax::rules::RuleKind::*; @@ -113,12 +111,13 @@ fn comments() -> Result<(), Error> { } #[test] -fn explicit_closed_slow() -> Result<(), Error> { - exec_with_timeout(Duration::from_millis(100), || { - assert!(OpeningHours::parse("Feb Fr off")? +fn explicit_closed_slow() { + let stats = TestStats::watch(|| { + assert!(OpeningHours::parse("Feb Fr off") + .unwrap() .next_change(datetime!("2021-07-09 19:30")) .is_none()); + }); - Ok::<(), Error>(()) - }) + assert!(stats.count_generated_schedules < 10); } diff --git a/opening-hours/src/tests/stats.rs b/opening-hours/src/tests/stats.rs new file mode 100644 index 00000000..bf858ee9 --- /dev/null +++ b/opening-hours/src/tests/stats.rs @@ -0,0 +1,27 @@ +use std::cell::RefCell; + +thread_local! { + static TEST_STATS: RefCell = RefCell::default(); + static THREAD_LOCK: RefCell<()> = RefCell::default(); +} + +#[derive(Default)] +pub(crate) struct TestStats { + pub(crate) count_generated_schedules: u64, +} + +impl TestStats { + pub(crate) fn watch(func: F) -> Self { + THREAD_LOCK.with_borrow_mut(|_| { + TEST_STATS.take(); + func(); + TEST_STATS.take() + }) + } +} + +pub(crate) mod notify { + pub(crate) fn generated_schedule() { + super::TEST_STATS.with_borrow_mut(|s| s.count_generated_schedules += 1) + } +}