diff --git a/lib/src/assert/guidance.rs b/lib/src/assert/guidance.rs new file mode 100644 index 0000000..2697dda --- /dev/null +++ b/lib/src/assert/guidance.rs @@ -0,0 +1,244 @@ +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; + +// 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, +} + +pub 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 } + } +} + +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; +} + +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); + matches!(max.partial_cmp(&new), None | Some(Less | Equal)) + } else { + let min = T::fetch_min(&self.mark, new, atomic::Ordering::SeqCst); + matches!(min.partial_cmp(&new), None | Some(Greater | Equal)) + } + } +} + +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))*) => {$( + #[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); + } + )*} +} + +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. + +#[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)); +} + +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))*) => {$( + pub struct $atomic_t($store_t); + + 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()) + } + + 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, +} + +#[allow(clippy::too_many_arguments)] +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..eaa2610 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,206 @@ 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, + }); + // 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... + // 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); + }}; +} + +/// `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) => { + $crate::numeric_guidance_helper!($crate::assert_always, >, false, $left, $right, $message, $details) + }; +} + +/// `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) => { + $crate::numeric_guidance_helper!($crate::assert_always, >=, false, $left, $right, $message, $details) + }; +} + +/// `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) => { + $crate::numeric_guidance_helper!($crate::assert_always, <, true, $left, $right, $message, $details) + }; +} + +/// `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) => { + $crate::numeric_guidance_helper!($crate::assert_always, <=, true, $left, $right, $message, $details) + }; +} + +/// `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) => { + $crate::numeric_guidance_helper!($crate::assert_sometimes, >, true, $left, $right, $message, $details) + }; +} + +/// `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) => { + $crate::numeric_guidance_helper!($crate::assert_sometimes, >=, true, $left, $right, $message, $details) + }; +} + +/// `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) => { + $crate::numeric_guidance_helper!($crate::assert_sometimes, <, false, $left, $right, $message, $details) + }; +} + +/// `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) => { + $crate::numeric_guidance_helper!($crate::assert_sometimes, <=, false, $left, $right, $message, $details) + }; +} + +/// `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) => { + $crate::boolean_guidance_helper!($crate::assert_always, false, {$($name: $cond),*}, $message, $details); + } +} + +/// `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) => { + $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}; 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..0d93787 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() {