From 3c95abd95fa70faaa015d1cd6d26316f8cd16317 Mon Sep 17 00:00:00 2001 From: Shuxian Wang Date: Fri, 27 Sep 2024 21:20:30 +0800 Subject: [PATCH 1/4] Support rich assertion macros. --- lib/src/assert/guidance.rs | 222 ++++++++++++++++++++++++++++++++++++ lib/src/assert/macros.rs | 227 +++++++++++++++++++++++++++++++++---- lib/src/assert/mod.rs | 29 ++++- lib/src/lib.rs | 3 + lib/src/prelude.rs | 10 ++ 5 files changed, 469 insertions(+), 22 deletions(-) create mode 100644 lib/src/assert/guidance.rs diff --git a/lib/src/assert/guidance.rs b/lib/src/assert/guidance.rs new file mode 100644 index 0000000..a7de21a --- /dev/null +++ b/lib/src/assert/guidance.rs @@ -0,0 +1,222 @@ +use std::sync::atomic::{self, AtomicI16, AtomicI32, AtomicI64, AtomicI8, AtomicIsize, AtomicU16, AtomicU32, AtomicU64, AtomicU8, AtomicUsize}; + +use once_cell::sync::Lazy; +use serde::Serialize; +use serde_json::{json, Value}; + +use crate::internal; + +use super::AntithesisLocationInfo; + +pub struct Guard { + mark: T::Atomic, +} + +trait Extremal { + const MIN: Self; + const MAX: Self; +} + +impl Guard +where T::Atomic: Extremal { + pub const fn init() -> Self { + let mark = if MAX { T::Atomic::MIN } else { T::Atomic::MAX }; + Self { mark } + } +} + +trait AtomicMinMax { + type Atomic; + fn fetch_min(current: &Self::Atomic, other: Self, ordering: atomic::Ordering) -> Self; + fn fetch_max(current: &Self::Atomic, other: Self, ordering: atomic::Ordering) -> Self; +} + +impl Guard { + pub fn should_emit(&self, new: T) -> bool { + if MAX { + let max = T::fetch_max(&self.mark, new, atomic::Ordering::SeqCst); + !(max >= new) + } else { + let min = T::fetch_min(&self.mark, new, atomic::Ordering::SeqCst); + !(min <= new) + } + } +} + +pub trait Diff { + type Output; + + fn diff(&self, other: Self) -> Self::Output; +} + +macro_rules! impl_extremal { + ($($t:ty)*) => {$( + impl Extremal for $t { + const MIN: $t = <$t>::MIN; + const MAX: $t = <$t>::MAX; + } + )*} +} + +impl_extremal! { usize u8 u16 u32 u64 u128 isize i8 i16 i32 i64 i128 f32 f64 } + +macro_rules! impl_extremal_atomic { + ($(($t:ty, $raw_t:ty))*) => {$( + impl Extremal for $t { + const MIN: $t = <$t>::new(<$raw_t>::MIN); + const MAX: $t = <$t>::new(<$raw_t>::MAX); + } + )*} +} + +impl_extremal_atomic! { (AtomicUsize, usize) (AtomicU8, u8) (AtomicU16, u16) (AtomicU32, u32) (AtomicU64, u64) (AtomicIsize, isize) (AtomicI8, i8) (AtomicI16, i16) (AtomicI32, i32) (AtomicI64, i64) } + +impl Extremal for AtomicF32 { + const MIN: Self = AtomicF32(AtomicU32::new(0xff800000)); + const MAX: Self = AtomicF32(AtomicU32::new(0x7f800000)); +} + +impl Extremal for AtomicF64 { + const MIN: Self = AtomicF64(AtomicU64::new(0xfff0000000000000)); + const MAX: Self = AtomicF64(AtomicU64::new(0x7ff0000000000000)); +} + +macro_rules! impl_atomic_min_max { + ($(($t:ty, $atomic_t:ty))*) => {$( + impl AtomicMinMax for $t { + type Atomic = $atomic_t; + + fn fetch_min(current: &Self::Atomic, other: Self, ordering: atomic::Ordering) -> Self { + current.fetch_min(other, ordering) + } + + fn fetch_max(current: &Self::Atomic, other: Self, ordering: atomic::Ordering) -> Self { + current.fetch_max(other, ordering) + } + } + )*}; +} + +impl_atomic_min_max! { (usize, AtomicUsize) (u8, AtomicU8) (u16, AtomicU16) (u32, AtomicU32) (u64, AtomicU64) (isize, AtomicIsize) (i8, AtomicI8) (i16, AtomicI16) (i32, AtomicI32) (i64, AtomicI64) } + +macro_rules! impl_atomic_min_max_float { + ($(($t:ty, $atomic_t:ident, $store_t:ty))*) => {$( + struct $atomic_t($store_t); + + impl AtomicMinMax for $t { + type Atomic = $atomic_t; + + fn fetch_min(current: &Self::Atomic, other: Self, ordering: atomic::Ordering) -> Self { + <$t>::from_bits(current.0.fetch_update(ordering, ordering, |x| Some(<$t>::from_bits(x).min(other).to_bits())).unwrap()) + } + + fn fetch_max(current: &Self::Atomic, other: Self, ordering: atomic::Ordering) -> Self { + <$t>::from_bits(current.0.fetch_update(ordering, ordering, |x| Some(<$t>::from_bits(x).max(other).to_bits())).unwrap()) + } + } + )*}; +} + +impl_atomic_min_max_float! { (f32, AtomicF32, AtomicU32) (f64, AtomicF64, AtomicU64)} + +macro_rules! impl_diff_unsigned { + ($($t:ty)*) => {$( + impl Diff for $t { + type Output = f64; + + fn diff(&self, other: Self) -> Self::Output { + if *self < other { + -((other - self) as f64) + } else { + (self - other) as f64 + } + } + } + )*}; +} + +impl_diff_unsigned! { usize u8 u16 u32 u64 u128 } + +macro_rules! impl_diff_signed { + ($(($t:ty, $unsigned_t:ty))*) => {$( + impl Diff for $t { + type Output = f64; + + fn diff(&self, other: Self) -> Self::Output { + if *self < other { + -((other as $unsigned_t).wrapping_sub(*self as $unsigned_t) as f64) + } else { + (*self as $unsigned_t).wrapping_sub(other as $unsigned_t) as f64 + } + } + } + )*}; +} + +impl_diff_signed! { (isize, usize) (i8, u8) (i16, u16) (i32, u32) (i64, u64) (i128, u128) } + +macro_rules! impl_diff_float { + ($($t:ty)*) => {$( + impl Diff for $t { + type Output = f64; + + fn diff(&self, other: Self) -> Self::Output { + (self - other) as f64 + } + } + )*}; +} + +impl_diff_float! { f32 f64 } + +#[derive(Copy, Clone, Serialize)] +#[serde(rename_all(serialize = "lowercase"))] +pub enum GuidanceType { + Numeric, + Boolean, + JSON, +} + +#[derive(Serialize)] +struct GuidanceInfo { + guidance_type: GuidanceType, + message: String, + id: String, + location: AntithesisLocationInfo, + maximize: bool, + guidance_data: Value, + hit: bool, +} + +pub struct GuidanceCatalogInfo { + pub guidance_type: GuidanceType, + pub message: &'static str, + pub id: &'static str, + pub class: &'static str, + pub function: &'static Lazy<&'static str>, + pub file: &'static str, + pub begin_line: u32, + pub begin_column: u32, + pub maximize: bool, +} + +pub fn guidance_impl( + guidance_type: GuidanceType, + message: String, + id: String, + class: String, + function: String, + file: String, + begin_line: u32, + begin_column: u32, + maximize: bool, + guidance_data: Value, + hit: bool, +) { + let location = AntithesisLocationInfo { class, function, file, begin_line, begin_column }; + let guidance = GuidanceInfo { + guidance_type, message, id, location, maximize, guidance_data, hit + }; + + internal::dispatch_output(&json!({ "antithesis_guidance": guidance })); +} diff --git a/lib/src/assert/macros.rs b/lib/src/assert/macros.rs index 8a06d89..0b74e36 100644 --- a/lib/src/assert/macros.rs +++ b/lib/src/assert/macros.rs @@ -1,3 +1,33 @@ +#[cfg(feature = "full")] +#[doc(hidden)] +#[macro_export] +macro_rules! function { + ($static:ident) => { + // Define a do-nothing function `'_f()'` within the context of + // the function invoking an assertion. Then the ``type_name`` of + // this do-nothing will be something like: + // + // bincrate::binmod::do_stuff::_f + // + // After trimming off the last three chars ``::_f`` what remains is + // the full path to the name of the function invoking the assertion + // + // The result will be stored as a lazily initialized statics in + // `$static`, so that it can be available at + // assertion catalog registration time. + use $crate::once_cell::sync::Lazy; + fn _f() {} + static $static: $crate::once_cell::sync::Lazy<&'static str> = + $crate::once_cell::sync::Lazy::new(|| { + fn type_name_of(_: T) -> &'static str { + ::std::any::type_name::() + } + let name = type_name_of(_f); + &name[..name.len() - 4] + }); + }; +} + /// Common handling used by all the assertion-related macros #[cfg(feature = "full")] #[doc(hidden)] @@ -10,29 +40,12 @@ macro_rules! assert_helper { let condition = $condition; let details = $details; - // Define a do-nothing function `'f()'` within the context of - // the function invoking an assertion. Then the ``type_name`` of - // this do-nothing will be something like: - // - // bincrate::binmod::do_stuff::f - // - // After trimming off the last three chars ``::f`` what remains is - // the full path to the name of the function invoking the assertion - // - // Both the untrimmed ``NAME`` and trimmed ``FUN_NAME`` are lazily - // initialized statics so that ``FUN_NAME`` can be available at - // assertion catalog registration time. - use $crate::once_cell::sync::Lazy; - fn f() {} - fn type_name_of(_: T) -> &'static str { - ::std::any::type_name::() - } - static NAME: Lazy<&'static str> = Lazy::new(|| type_name_of(f)); - static FUN_NAME: Lazy<&'static str> = Lazy::new(|| &NAME[..NAME.len() - 3]); + $crate::function!(FUN_NAME); + use $crate::assert::AssertionCatalogInfo; #[$crate::linkme::distributed_slice($crate::assert::ANTITHESIS_CATALOG)] #[linkme(crate = $crate::linkme)] // Refer to our re-exported linkme. - static ALWAYS_CATALOG_ITEM: $crate::assert::CatalogInfo = $crate::assert::CatalogInfo { + static ALWAYS_CATALOG_ITEM: AssertionCatalogInfo = AssertionCatalogInfo { assert_type: $assert_type, display_type: $display_type, condition: false, @@ -227,3 +240,177 @@ macro_rules! assert_unreachable { ) }; } + +#[cfg(feature = "full")] +#[doc(hidden)] +#[macro_export] +macro_rules! guidance_helper { + ($guidance_type:expr, $message:literal, $maximize:literal, $guidance_data:expr) => { + $crate::function!(FUN_NAME); + + use $crate::assert::guidance::{GuidanceCatalogInfo, GuidanceType}; + #[$crate::linkme::distributed_slice($crate::assert::ANTITHESIS_GUIDANCE_CATALOG)] + #[linkme(crate = $crate::linkme)] // Refer to our re-exported linkme. + static GUIDANCE_CATALOG_ITEM: GuidanceCatalogInfo = GuidanceCatalogInfo { + guidance_type: $guidance_type, + message: $message, + id: $message, + class: ::std::module_path!(), + function: &FUN_NAME, + file: ::std::file!(), + begin_line: ::std::line!(), + begin_column: ::std::column!(), + maximize: $maximize, + }; + + $crate::assert::guidance::guidance_impl( + $guidance_type, + $message.to_owned(), + $message.to_owned(), + ::std::module_path!().to_owned(), + Lazy::force(&FUN_NAME).to_string(), + ::std::file!().to_owned(), + ::std::line!(), + ::std::column!(), + $maximize, + $guidance_data, + true, + ) + }; +} + +#[cfg(feature = "full")] +#[doc(hidden)] +#[macro_export] +macro_rules! numeric_guidance_helper { + ($assert:path, $op:tt, $maximize:literal, $left:expr, $right:expr, $message:literal, $details:expr) => {{ + let left = $left; + let right = $right; + let mut details = $details.clone(); + details["left"] = left.into(); + details["right"] = right.into(); + $assert!(left $op right, $message, &details); + + let guidance_data = $crate::serde_json::json!({ + "left": left, + "right": right, + }); + let diff = $crate::assert::guidance::Diff::diff(&left, right); + type Guard = $crate::assert::guidance::Guard<$maximize, T>; + // TODO: Waiting for [type_alias_impl_trait](https://github.com/rust-lang/rust/issues/63063) to stabilize... + // type Distance = impl Minimal; + type Distance = f64; + static GUARD: Guard = Guard::init(); + if GUARD.should_emit(diff) { + $crate::guidance_helper!($crate::assert::guidance::GuidanceType::Numeric, $message, $maximize, guidance_data); + } + }}; +} + +#[cfg(not(feature = "full"))] +#[doc(hidden)] +#[macro_export] +macro_rules! numeric_guidance_helper { + ($assert:ident, $op:tt, $maximize:literal, $left:expr, $right:expr, $message:literal, $details:expr) => { + assert!($left $op $right, $message, $details); + }; +} + +#[cfg(feature = "full")] +#[doc(hidden)] +#[macro_export] +macro_rules! boolean_guidance_helper { + ($assert:path, $all:literal, {$($name:ident: $cond:expr),*}, $message:literal, $details:expr) => {{ + let mut details = $details.clone(); + let (cond, guidance_data) = { + $(let $name = $cond;)* + $(details[::std::stringify!($name)] = $name.into();)* + ( + if $all { $($name)&&* } else { $($name)||* }, + $crate::serde_json::json!({$(::std::stringify!($name): $name),*}) + ) + }; + $assert!(cond, $message, &details); + $crate::guidance_helper!($crate::assert::guidance::GuidanceType::Boolean, $message, $all, guidance_data); + }}; +} + +#[cfg(not(feature = "full"))] +#[doc(hidden)] +#[macro_export] +macro_rules! boolean_guidance_helper { + ($assert:path, $all:literal, {$($name:ident: $cond:expr),*}, $message:literal, $details:expr) => {{ + let cond = if $all { $($name)&&* } else { $($name)||* }; + $assert!(cond, $message, &details); + }}; +} + +#[macro_export] +macro_rules! assert_always_greater_than { + ($left:expr, $right:expr, $message:literal, $details:expr) => { + $crate::numeric_guidance_helper!($crate::assert_always, >, false, $left, $right, $message, $details) + }; +} + +#[macro_export] +macro_rules! assert_always_greater_than_or_equal_to { + ($left:expr, $right:expr, $message:literal, $details:expr) => { + $crate::numeric_guidance_helper!($crate::assert_always, >=, false, $left, $right, $message, $details) + }; +} + +#[macro_export] +macro_rules! assert_always_less_than { + ($left:expr, $right:expr, $message:literal, $details:expr) => { + $crate::numeric_guidance_helper!($crate::assert_always, <, true, $left, $right, $message, $details) + }; +} + +#[macro_export] +macro_rules! assert_always_less_than_or_equal_to { + ($left:expr, $right:expr, $message:literal, $details:expr) => { + $crate::numeric_guidance_helper!($crate::assert_always, <=, true, $left, $right, $message, $details) + }; +} + +#[macro_export] +macro_rules! assert_sometimes_greater_than { + ($left:expr, $right:expr, $message:literal, $details:expr) => { + $crate::numeric_guidance_helper!($crate::assert_sometimes, >, true, $left, $right, $message, $details) + }; +} + +#[macro_export] +macro_rules! assert_sometimes_greater_than_or_equal_to { + ($left:expr, $right:expr, $message:literal, $details:expr) => { + $crate::numeric_guidance_helper!($crate::assert_sometimes, >=, true, $left, $right, $message, $details) + }; +} + +#[macro_export] +macro_rules! assert_sometimes_less_than { + ($left:expr, $right:expr, $message:literal, $details:expr) => { + $crate::numeric_guidance_helper!($crate::assert_sometimes, <, false, $left, $right, $message, $details) + }; +} + +#[macro_export] +macro_rules! assert_sometimes_less_than_or_equal_to { + ($left:expr, $right:expr, $message:literal, $details:expr) => { + $crate::numeric_guidance_helper!($crate::assert_sometimes, <=, false, $left, $right, $message, $details) + }; +} + +#[macro_export] +macro_rules! assert_always_some { + ({$($name:ident: $cond:expr),*}, $message:literal, $details:expr) => { + $crate::boolean_guidance_helper!($crate::assert_always, false, {$($name: $cond),*}, $message, $details); + } +} + +#[macro_export] +macro_rules! assert_sometimes_all { + ({$($name:ident: $cond:expr),*}, $message:literal, $details:expr) => { + $crate::boolean_guidance_helper!($crate::assert_sometimes, true, {$($name: $cond),*}, $message, $details); + } +} diff --git a/lib/src/assert/mod.rs b/lib/src/assert/mod.rs index 88c8918..e70d4fb 100644 --- a/lib/src/assert/mod.rs +++ b/lib/src/assert/mod.rs @@ -14,13 +14,23 @@ use std::collections::HashMap; #[cfg(feature = "full")] use std::sync::Mutex; +use self::guidance::GuidanceCatalogInfo; + mod macros; +#[doc(hidden)] +pub mod guidance; /// Catalog of all antithesis assertions provided #[doc(hidden)] #[distributed_slice] #[cfg(feature = "full")] -pub static ANTITHESIS_CATALOG: [CatalogInfo]; +pub static ANTITHESIS_CATALOG: [AssertionCatalogInfo]; + +/// Catalog of all antithesis guidances provided +#[doc(hidden)] +#[distributed_slice] +#[cfg(feature = "full")] +pub static ANTITHESIS_GUIDANCE_CATALOG: [GuidanceCatalogInfo]; // Only need an ASSET_TRACKER if there are actually assertions 'hit' // (i.e. encountered and invoked at runtime). @@ -53,6 +63,21 @@ pub(crate) static INIT_CATALOG: Lazy<()> = Lazy::new(|| { &no_details, ); } + for info in ANTITHESIS_GUIDANCE_CATALOG.iter() { + guidance::guidance_impl( + info.guidance_type, + info.message.to_owned(), + info.id.to_owned(), + info.class.to_owned(), + Lazy::force(info.function).to_string(), + info.file.to_owned(), + info.begin_line, + info.begin_column, + info.maximize, + json!(null), + false, + ) + } }); #[cfg(feature = "full")] @@ -99,7 +124,7 @@ struct AntithesisLocationInfo { #[doc(hidden)] #[derive(Debug)] #[cfg(feature = "full")] -pub struct CatalogInfo { +pub struct AssertionCatalogInfo { pub assert_type: AssertType, pub display_type: &'static str, pub condition: bool, diff --git a/lib/src/lib.rs b/lib/src/lib.rs index c0eedbc..76bebab 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -19,6 +19,9 @@ pub use linkme; #[doc(hidden)] #[cfg(feature = "full")] pub use once_cell; +#[doc(hidden)] +#[cfg(feature = "full")] +pub use serde_json; /// The lifecycle module contains functions which inform the Antithesis /// environment that particular test phases or milestones have been reached. diff --git a/lib/src/prelude.rs b/lib/src/prelude.rs index 477652a..39b2db7 100644 --- a/lib/src/prelude.rs +++ b/lib/src/prelude.rs @@ -4,4 +4,14 @@ pub use crate::assert_always_or_unreachable; pub use crate::assert_reachable; pub use crate::assert_sometimes; pub use crate::assert_unreachable; +pub use crate::assert_always_greater_than; +pub use crate::assert_always_greater_than_or_equal_to; +pub use crate::assert_always_less_than; +pub use crate::assert_always_less_than_or_equal_to; +pub use crate::assert_sometimes_greater_than; +pub use crate::assert_sometimes_greater_than_or_equal_to; +pub use crate::assert_sometimes_less_than; +pub use crate::assert_sometimes_less_than_or_equal_to; +pub use crate::assert_always_some; +pub use crate::assert_sometimes_all; pub use crate::{antithesis_init, lifecycle, random}; From d2030ad0c6e5abca6d335dec509d6e72f0f8ac5f Mon Sep 17 00:00:00 2001 From: Shuxian Wang Date: Mon, 30 Sep 2024 16:44:01 +0800 Subject: [PATCH 2/4] Add tests. --- lib/tests/assert_guidance.rs | 58 ++++++++++++++++++++++++++++++++++++ lib/tests/common/mod.rs | 20 +++++++++++++ simple/src/main.rs | 4 +++ 3 files changed, 82 insertions(+) create mode 100644 lib/tests/assert_guidance.rs diff --git a/lib/tests/assert_guidance.rs b/lib/tests/assert_guidance.rs new file mode 100644 index 0000000..a1b5f3a --- /dev/null +++ b/lib/tests/assert_guidance.rs @@ -0,0 +1,58 @@ +use antithesis_sdk::{antithesis_init, assert_always_greater_than, LOCAL_OUTPUT}; +use serde_json::json; + +mod common; +use common::SDKInput; + +use crate::common::{AntithesisGuidance, GuidanceType}; + +#[test] +fn assert_guidance() { + let output_file = "/tmp/antithesis-assert-guidance.json"; + let prev_v = common::env::set_var(LOCAL_OUTPUT, output_file); + antithesis_init(); + + for i in 0..10 { + let x = if i % 2 == 0 { i } else { -i }; + assert_always_greater_than!(x, 0, "Positive x", &json!({"x": x})); + } + + match common::read_jsonl_tags(output_file) { + Ok(x) => { + let mut did_register = false; + let mut did_hit = false; + for obj in x.iter() { + if let SDKInput::AntithesisGuidance(AntithesisGuidance { + guidance_type, + hit, + id, + message, + location, + .. + }) = obj + { + if *hit { + did_hit = true; + } else { + did_register = true; + }; + assert_eq!(*guidance_type, GuidanceType::Numeric); + assert_eq!(message, "Positive x"); + assert_eq!(id, message); + assert!(location.begin_line > 0); + assert!(location.begin_column >= 0); + assert_eq!(location.class, "assert_guidance"); + assert!(location.function.ends_with("::assert_guidance")); + assert!(location + .file + .ends_with("/tests/assert_guidance.rs")); + } + println!("{:?}", obj); + } + assert!(did_register); + assert!(did_hit); + } + Err(e) => println!("{}", e), + } + common::env::restore_var(LOCAL_OUTPUT, prev_v); +} diff --git a/lib/tests/common/mod.rs b/lib/tests/common/mod.rs index b6a0e17..db5aeec 100644 --- a/lib/tests/common/mod.rs +++ b/lib/tests/common/mod.rs @@ -47,11 +47,23 @@ pub struct AntithesisAssert { pub details: Value, } +#[derive(Deserialize, Debug)] +pub struct AntithesisGuidance { + pub guidance_type: GuidanceType, + pub message: String, + pub id: String, + pub location: Location, + pub maximize: bool, + pub guidance_data: Value, + pub hit: bool, +} + #[derive(Deserialize, Debug)] #[serde(rename_all = "snake_case")] pub enum SDKInput { AntithesisSdk(AntithesisSdk), AntithesisAssert(AntithesisAssert), + AntithesisGuidance(AntithesisGuidance), AntithesisSetup(AntithesisSetup), SendEvent { event_name: String, details: Value }, } @@ -64,6 +76,14 @@ pub enum AssertType { Reachability, } +#[derive(Deserialize, Debug, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum GuidanceType { + Numeric, + Boolean, + JSON, +} + fn parse_lines(lines: Vec<&str>) -> Result, Box> { let mut result = Vec::new(); diff --git a/simple/src/main.rs b/simple/src/main.rs index 0aa298a..1e43d60 100644 --- a/simple/src/main.rs +++ b/simple/src/main.rs @@ -79,6 +79,10 @@ fn assert_demo() { // ant_unreachable let details = json!({"impossible!": {"name": "trouble", "weights": [100,200,300]}}); assert_unreachable!("Impossible to get here", &details); + + assert_always_greater_than!(3, 100, "not right", &json!({})); + + assert_sometimes_all!({a: true, b: false}, "not all right", &json!({})); } pub fn main() { From 7a740eb9de18cde9aeabeb9ac01d914cd5c863bb Mon Sep 17 00:00:00 2001 From: Shuxian Wang Date: Mon, 30 Sep 2024 20:04:22 +0800 Subject: [PATCH 3/4] Add comments for rich assertions. --- lib/src/assert/guidance.rs | 18 ++++++++++++++++++ lib/src/assert/macros.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/lib/src/assert/guidance.rs b/lib/src/assert/guidance.rs index a7de21a..703e041 100644 --- a/lib/src/assert/guidance.rs +++ b/lib/src/assert/guidance.rs @@ -8,6 +8,20 @@ use crate::internal; use super::AntithesisLocationInfo; +// Types and traits that model the SDK filtering of numerical guidance reporting. +// For assertions like "always (x < y)", we would like to only report the most extreme +// violations seen so far, which is implemented by having a `Guard` that keep a maximizing +// watermark on the difference (x - y). +// The `AtomicMinMax` trait requirement allows multiple concurrent update to the watermark. + +// NOTE: The structures setup in this modules allow `Guard` to be generic over the numeric +// type (or even any partially ordered type). +// But due to some limitation of stable Rust, we are only instanciating `Guard` by +// converting the result of all `x - y` into `f64`. +// See the impl `numeric_guidance_helper` for more details on the limitation. +// Once that is lifted, some implementations of `Diff` can be changed to truly take advantage +// of the zero-cost polymorphism that `Guard` provides. + pub struct Guard { mark: T::Atomic, } @@ -71,6 +85,8 @@ macro_rules! impl_extremal_atomic { impl_extremal_atomic! { (AtomicUsize, usize) (AtomicU8, u8) (AtomicU16, u16) (AtomicU32, u32) (AtomicU64, u64) (AtomicIsize, isize) (AtomicI8, i8) (AtomicI16, i16) (AtomicI32, i32) (AtomicI64, i64) } +// For atomic floats, their minimal/maximal elements are `-inf` and `+inf` respectively. + impl Extremal for AtomicF32 { const MIN: Self = AtomicF32(AtomicU32::new(0xff800000)); const MAX: Self = AtomicF32(AtomicU32::new(0x7f800000)); @@ -106,6 +122,8 @@ macro_rules! impl_atomic_min_max_float { impl AtomicMinMax for $t { type Atomic = $atomic_t; + // TODO: Check the atomic orderings are used properly in general. + // Right now we are always passing SeqCst, which should be fine. fn fetch_min(current: &Self::Atomic, other: Self, ordering: atomic::Ordering) -> Self { <$t>::from_bits(current.0.fetch_update(ordering, ordering, |x| Some(<$t>::from_bits(x).min(other).to_bits())).unwrap()) } diff --git a/lib/src/assert/macros.rs b/lib/src/assert/macros.rs index 0b74e36..eaa2610 100644 --- a/lib/src/assert/macros.rs +++ b/lib/src/assert/macros.rs @@ -295,6 +295,19 @@ macro_rules! numeric_guidance_helper { "left": left, "right": right, }); + // TODO: Right now it seems to be impossible for this macro to use the returned + // type of `diff` to instanciate the `T` in `Guard`, which has to be + // explicitly provided for the static variable `GUARD`. + // Instead, we currently fix `T` to be `f64`, and ensure all implementations of `Diff` returns `f64`. + // Here are some related language limitations: + // - Although `typeof` is a reserved keyword in Rust, it is never implemented. See . + // - Rust does not, and explicitly would not (see https://doc.rust-lang.org/reference/items/static-items.html#statics--generics), support generic static variable. + // - Type inference is not performed for static variable, i.e. `Guard<_>` is not allowed. + // - Some form of existential type can help, but that's only available in nightly Rust under feature `type_alias_impl_trait`. + // + // Other approaches I can think of either requires dynamic type tagging that has + // runtime overhead, or requires the user of the macro to explicitly provide the type, + // which is really not ergonomic and deviate from the APIs from other SDKs. let diff = $crate::assert::guidance::Diff::diff(&left, right); type Guard = $crate::assert::guidance::Guard<$maximize, T>; // TODO: Waiting for [type_alias_impl_trait](https://github.com/rust-lang/rust/issues/63063) to stabilize... @@ -345,6 +358,7 @@ macro_rules! boolean_guidance_helper { }}; } +/// `assert_always_greater_than(x, y, ...)` is mostly equivalent to `assert_always!(x > y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. #[macro_export] macro_rules! assert_always_greater_than { ($left:expr, $right:expr, $message:literal, $details:expr) => { @@ -352,6 +366,7 @@ macro_rules! assert_always_greater_than { }; } +/// `assert_always_greater_than_or_equal_to(x, y, ...)` is mostly equivalent to `assert_always!(x >= y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. #[macro_export] macro_rules! assert_always_greater_than_or_equal_to { ($left:expr, $right:expr, $message:literal, $details:expr) => { @@ -359,6 +374,7 @@ macro_rules! assert_always_greater_than_or_equal_to { }; } +/// `assert_always_less_than(x, y, ...)` is mostly equivalent to `assert_always!(x < y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. #[macro_export] macro_rules! assert_always_less_than { ($left:expr, $right:expr, $message:literal, $details:expr) => { @@ -366,6 +382,7 @@ macro_rules! assert_always_less_than { }; } +/// `assert_always_less_than_or_equal_to(x, y, ...)` is mostly equivalent to `assert_always!(x <= y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. #[macro_export] macro_rules! assert_always_less_than_or_equal_to { ($left:expr, $right:expr, $message:literal, $details:expr) => { @@ -373,6 +390,7 @@ macro_rules! assert_always_less_than_or_equal_to { }; } +/// `assert_sometimes_greater_than(x, y, ...)` is mostly equivalent to `assert_sometimes!(x > y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. #[macro_export] macro_rules! assert_sometimes_greater_than { ($left:expr, $right:expr, $message:literal, $details:expr) => { @@ -380,6 +398,7 @@ macro_rules! assert_sometimes_greater_than { }; } +/// `assert_sometimes_greater_than_or_equal_to(x, y, ...)` is mostly equivalent to `assert_sometimes!(x >= y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. #[macro_export] macro_rules! assert_sometimes_greater_than_or_equal_to { ($left:expr, $right:expr, $message:literal, $details:expr) => { @@ -387,6 +406,7 @@ macro_rules! assert_sometimes_greater_than_or_equal_to { }; } +/// `assert_sometimes_less_than(x, y, ...)` is mostly equivalent to `assert_sometimes!(x < y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. #[macro_export] macro_rules! assert_sometimes_less_than { ($left:expr, $right:expr, $message:literal, $details:expr) => { @@ -394,6 +414,7 @@ macro_rules! assert_sometimes_less_than { }; } +/// `assert_sometimes_less_than_or_equal_to(x, y, ...)` is mostly equivalent to `assert_sometimes!(x <= y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. #[macro_export] macro_rules! assert_sometimes_less_than_or_equal_to { ($left:expr, $right:expr, $message:literal, $details:expr) => { @@ -401,6 +422,10 @@ macro_rules! assert_sometimes_less_than_or_equal_to { }; } +/// `assert_always_some({a: x, b: y, ...})` is similar to `assert_always(x || y || ...)`, except: +/// - Antithesis has more visibility to the individual propositions. +/// - There is no short-circuiting, so all of `x`, `y`, ... would be evaluated. +/// - The assertion details would be merged with `{"a": x, "b": y, ...}`. #[macro_export] macro_rules! assert_always_some { ({$($name:ident: $cond:expr),*}, $message:literal, $details:expr) => { @@ -408,6 +433,10 @@ macro_rules! assert_always_some { } } +/// `assert_sometimes_all({a: x, b: y, ...})` is similar to `assert_sometimes(x && y && ...)`, except: +/// - Antithesis has more visibility to the individual propositions. +/// - There is no short-circuiting, so all of `x`, `y`, ... would be evaluated. +/// - The assertion details would be merged with `{"a": x, "b": y, ...}`. #[macro_export] macro_rules! assert_sometimes_all { ({$($name:ident: $cond:expr),*}, $message:literal, $details:expr) => { From 9f5f430f51217c5c22050f9022d374aa0eed5382 Mon Sep 17 00:00:00 2001 From: Shuxian Wang Date: Mon, 30 Sep 2024 20:15:10 +0800 Subject: [PATCH 4/4] Clippy fix. --- lib/src/assert/guidance.rs | 18 +++++++++++------- lib/tests/common/mod.rs | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/src/assert/guidance.rs b/lib/src/assert/guidance.rs index 703e041..2697dda 100644 --- a/lib/src/assert/guidance.rs +++ b/lib/src/assert/guidance.rs @@ -21,12 +21,11 @@ use super::AntithesisLocationInfo; // See the impl `numeric_guidance_helper` for more details on the limitation. // Once that is lifted, some implementations of `Diff` can be changed to truly take advantage // of the zero-cost polymorphism that `Guard` provides. - pub struct Guard { mark: T::Atomic, } -trait Extremal { +pub trait Extremal { const MIN: Self; const MAX: Self; } @@ -39,7 +38,7 @@ where T::Atomic: Extremal { } } -trait AtomicMinMax { +pub trait AtomicMinMax { type Atomic; fn fetch_min(current: &Self::Atomic, other: Self, ordering: atomic::Ordering) -> Self; fn fetch_max(current: &Self::Atomic, other: Self, ordering: atomic::Ordering) -> Self; @@ -47,12 +46,13 @@ trait AtomicMinMax { impl Guard { pub fn should_emit(&self, new: T) -> bool { + use std::cmp::Ordering::*; if MAX { let max = T::fetch_max(&self.mark, new, atomic::Ordering::SeqCst); - !(max >= new) + matches!(max.partial_cmp(&new), None | Some(Less | Equal)) } else { let min = T::fetch_min(&self.mark, new, atomic::Ordering::SeqCst); - !(min <= new) + matches!(min.partial_cmp(&new), None | Some(Greater | Equal)) } } } @@ -76,6 +76,7 @@ impl_extremal! { usize u8 u16 u32 u64 u128 isize i8 i16 i32 i64 i128 f32 f64 } macro_rules! impl_extremal_atomic { ($(($t:ty, $raw_t:ty))*) => {$( + #[allow(clippy::declare_interior_mutable_const)] impl Extremal for $t { const MIN: $t = <$t>::new(<$raw_t>::MIN); const MAX: $t = <$t>::new(<$raw_t>::MAX); @@ -87,11 +88,13 @@ impl_extremal_atomic! { (AtomicUsize, usize) (AtomicU8, u8) (AtomicU16, u16) (At // For atomic floats, their minimal/maximal elements are `-inf` and `+inf` respectively. +#[allow(clippy::declare_interior_mutable_const)] impl Extremal for AtomicF32 { const MIN: Self = AtomicF32(AtomicU32::new(0xff800000)); const MAX: Self = AtomicF32(AtomicU32::new(0x7f800000)); } +#[allow(clippy::declare_interior_mutable_const)] impl Extremal for AtomicF64 { const MIN: Self = AtomicF64(AtomicU64::new(0xfff0000000000000)); const MAX: Self = AtomicF64(AtomicU64::new(0x7ff0000000000000)); @@ -117,7 +120,7 @@ impl_atomic_min_max! { (usize, AtomicUsize) (u8, AtomicU8) (u16, AtomicU16) (u32 macro_rules! impl_atomic_min_max_float { ($(($t:ty, $atomic_t:ident, $store_t:ty))*) => {$( - struct $atomic_t($store_t); + pub struct $atomic_t($store_t); impl AtomicMinMax for $t { type Atomic = $atomic_t; @@ -192,7 +195,7 @@ impl_diff_float! { f32 f64 } pub enum GuidanceType { Numeric, Boolean, - JSON, + Json, } #[derive(Serialize)] @@ -218,6 +221,7 @@ pub struct GuidanceCatalogInfo { pub maximize: bool, } +#[allow(clippy::too_many_arguments)] pub fn guidance_impl( guidance_type: GuidanceType, message: String, diff --git a/lib/tests/common/mod.rs b/lib/tests/common/mod.rs index db5aeec..0d93787 100644 --- a/lib/tests/common/mod.rs +++ b/lib/tests/common/mod.rs @@ -81,7 +81,7 @@ pub enum AssertType { pub enum GuidanceType { Numeric, Boolean, - JSON, + Json, } fn parse_lines(lines: Vec<&str>) -> Result, Box> {