324 lines
8.2 KiB
Go
324 lines
8.2 KiB
Go
//go:build enable_antithesis_sdk
|
|
|
|
package assert
|
|
|
|
import (
|
|
"math"
|
|
"sync"
|
|
|
|
"github.com/antithesishq/antithesis-sdk-go/internal"
|
|
)
|
|
|
|
// --------------------------------------------------------------------------------
|
|
// IntegerGap is used for:
|
|
// - int, int8, int16, int32, int64:
|
|
// - uint, uint8, uint16, uint32, uint64, uintptr:
|
|
//
|
|
// FloatGap is used for:
|
|
// - float32, float64
|
|
// --------------------------------------------------------------------------------
|
|
type numericGapType int
|
|
|
|
const (
|
|
integerGapType numericGapType = iota
|
|
floatGapType
|
|
)
|
|
|
|
func gapTypeForOperand[T Number](num T) numericGapType {
|
|
gapType := integerGapType
|
|
|
|
switch any(num).(type) {
|
|
case float32, float64:
|
|
gapType = floatGapType
|
|
}
|
|
return gapType
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------
|
|
// numericGuidanceTracker - Tracking Info for Numeric Guidance
|
|
//
|
|
// For GuidanceFnMaximize:
|
|
// - gap is the largest value sent so far
|
|
//
|
|
// For GuidanceFnMinimize:
|
|
// - gap is the most negative value sent so far
|
|
//
|
|
// --------------------------------------------------------------------------------
|
|
type numericGuidanceInfo struct {
|
|
gap any
|
|
descriminator numericGapType
|
|
maximize bool
|
|
}
|
|
|
|
type numericGuidanceTracker map[string]*numericGuidanceInfo
|
|
|
|
var (
|
|
numeric_guidance_tracker numericGuidanceTracker = make(numericGuidanceTracker)
|
|
numeric_guidance_tracker_mutex sync.Mutex
|
|
numeric_guidance_info_mutex sync.Mutex
|
|
)
|
|
|
|
func (tracker numericGuidanceTracker) getTrackerEntry(messageKey string, trackerType numericGapType, maximize bool) *numericGuidanceInfo {
|
|
var trackerEntry *numericGuidanceInfo
|
|
var ok bool
|
|
|
|
if tracker == nil {
|
|
return nil
|
|
}
|
|
|
|
numeric_guidance_tracker_mutex.Lock()
|
|
defer numeric_guidance_tracker_mutex.Unlock()
|
|
if trackerEntry, ok = numeric_guidance_tracker[messageKey]; !ok {
|
|
trackerEntry = newNumericGuidanceInfo(trackerType, maximize)
|
|
tracker[messageKey] = trackerEntry
|
|
}
|
|
|
|
return trackerEntry
|
|
}
|
|
|
|
// Create an numeric guidance entry
|
|
func newNumericGuidanceInfo(trackerType numericGapType, maximize bool) *numericGuidanceInfo {
|
|
|
|
var gap any
|
|
if trackerType == integerGapType {
|
|
gap = newGapValue(uint64(math.MaxUint64), maximize)
|
|
} else {
|
|
gap = newGapValue(float64(math.MaxFloat64), maximize)
|
|
}
|
|
trackerInfo := numericGuidanceInfo{
|
|
maximize: maximize,
|
|
descriminator: trackerType,
|
|
gap: gap,
|
|
}
|
|
return &trackerInfo
|
|
}
|
|
|
|
func (tI *numericGuidanceInfo) should_maximize() bool {
|
|
return tI.maximize
|
|
}
|
|
|
|
func (tI *numericGuidanceInfo) is_integer_gap() bool {
|
|
return tI.descriminator == integerGapType
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------
|
|
// Represents integral and floating point extremes
|
|
// --------------------------------------------------------------------------------
|
|
type gapValue[T numConstraint] struct {
|
|
gap_size T
|
|
gap_is_negative bool
|
|
}
|
|
|
|
func newGapValue[T numConstraint](sz T, is_neg bool) any {
|
|
switch any(sz).(type) {
|
|
case uint64:
|
|
return gapValue[uint64]{gap_size: uint64(sz), gap_is_negative: is_neg}
|
|
|
|
case float64:
|
|
return gapValue[float64]{gap_size: float64(sz), gap_is_negative: is_neg}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func is_same_sign(left_val int64, right_val int64) bool {
|
|
same_sign := false
|
|
if left_val < 0 {
|
|
// left is negative
|
|
if right_val < 0 {
|
|
same_sign = true
|
|
}
|
|
} else {
|
|
// left is non-negative
|
|
if right_val >= 0 {
|
|
same_sign = true
|
|
}
|
|
}
|
|
return same_sign
|
|
}
|
|
|
|
func abs_int64(val int64) uint64 {
|
|
if val >= 0 {
|
|
return uint64(val)
|
|
}
|
|
return uint64(0 - val)
|
|
}
|
|
|
|
func is_greater_than[T numConstraint](left gapValue[T], right gapValue[T]) bool {
|
|
if !left.gap_is_negative && !right.gap_is_negative {
|
|
return left.gap_size > right.gap_size
|
|
}
|
|
if !left.gap_is_negative && right.gap_is_negative {
|
|
return true // any positive is greater than a negative
|
|
}
|
|
if left.gap_is_negative && right.gap_is_negative {
|
|
return right.gap_size > left.gap_size
|
|
}
|
|
if left.gap_is_negative && !right.gap_is_negative {
|
|
return false // any negative is less than a positive
|
|
}
|
|
return false
|
|
}
|
|
|
|
func is_less_than[T numConstraint](left gapValue[T], right gapValue[T]) bool {
|
|
if !left.gap_is_negative && !right.gap_is_negative {
|
|
return left.gap_size < right.gap_size
|
|
}
|
|
if !left.gap_is_negative && right.gap_is_negative {
|
|
return false // any positive is greater than a negative
|
|
}
|
|
if left.gap_is_negative && right.gap_is_negative {
|
|
return right.gap_size < left.gap_size
|
|
}
|
|
if left.gap_is_negative && !right.gap_is_negative {
|
|
return true // any negative is less than a positive
|
|
}
|
|
return true
|
|
}
|
|
|
|
func send_value_if_needed(tI *numericGuidanceInfo, gI *guidanceInfo) {
|
|
if tI == nil {
|
|
return
|
|
}
|
|
|
|
numeric_guidance_info_mutex.Lock()
|
|
defer numeric_guidance_info_mutex.Unlock()
|
|
|
|
// if this is a catalog entry (gI.hit is false)
|
|
// do not update the reference gap in the tracker (tI *numericGuidanceInfo)
|
|
if !gI.Hit {
|
|
emitGuidance(gI)
|
|
return
|
|
}
|
|
|
|
should_send := false
|
|
maximize := tI.should_maximize()
|
|
|
|
var gap gapValue[uint64]
|
|
var float_gap gapValue[float64]
|
|
|
|
// Needs to have individual case statements to assist
|
|
// the compiler to infer the actual type of the var named 'operands'
|
|
switch operands := (gI.Data).(type) {
|
|
case numericOperands[int32]:
|
|
gap = makeGap(operands)
|
|
case numericOperands[int64]:
|
|
gap = makeGap(operands)
|
|
case numericOperands[uint64]:
|
|
gap = makeGap(operands)
|
|
case numericOperands[float64]:
|
|
float_gap = makeFloatGap(operands)
|
|
}
|
|
|
|
var prev_gap gapValue[uint64]
|
|
var prev_float_gap gapValue[float64]
|
|
has_prev_gap := false
|
|
has_prev_float_gap := false
|
|
|
|
prev_gap, has_prev_gap = tI.gap.(gapValue[uint64])
|
|
if !has_prev_gap {
|
|
prev_float_gap, has_prev_float_gap = tI.gap.(gapValue[float64])
|
|
}
|
|
|
|
if has_prev_gap {
|
|
if maximize {
|
|
should_send = is_greater_than(gap, prev_gap)
|
|
} else {
|
|
should_send = is_less_than(gap, prev_gap)
|
|
}
|
|
}
|
|
|
|
if has_prev_float_gap {
|
|
if maximize {
|
|
should_send = is_greater_than(float_gap, prev_float_gap)
|
|
} else {
|
|
should_send = is_less_than(float_gap, prev_float_gap)
|
|
}
|
|
}
|
|
|
|
if should_send {
|
|
if tI.is_integer_gap() {
|
|
tI.gap = gap
|
|
} else {
|
|
tI.gap = float_gap
|
|
}
|
|
emitGuidance(gI)
|
|
}
|
|
}
|
|
|
|
func emitGuidance(gI *guidanceInfo) error {
|
|
return internal.Json_data(map[string]any{"antithesis_guidance": gI})
|
|
}
|
|
|
|
// When left and right are the same sign (both negative, or both non-negative)
|
|
// Calculate: <result> = (left - right). The gap_size is abs(<result>) and
|
|
// gap_is_negative is (right > left)
|
|
func makeGap[Op operandConstraint](operand numericOperands[Op]) gapValue[uint64] {
|
|
|
|
var gap_size uint64
|
|
var gap_is_negative bool
|
|
|
|
switch this_op := any(operand).(type) {
|
|
case numericOperands[int32]:
|
|
result := int64(this_op.Left) - int64(this_op.Right)
|
|
gap_size = abs_int64(result)
|
|
gap_is_negative = result < 0
|
|
|
|
case numericOperands[int64]:
|
|
if is_same_sign(this_op.Left, this_op.Right) {
|
|
result := int64(this_op.Left) - int64(this_op.Right)
|
|
gap_size = abs_int64(result)
|
|
gap_is_negative = result < 0
|
|
break
|
|
}
|
|
|
|
// Otherwise left and right are opposite signs
|
|
// gap = abs(left) + abs(right)
|
|
// gap_is_negative = left < right
|
|
left_gap_size := abs_int64(this_op.Left)
|
|
right_gap_size := abs_int64(this_op.Right)
|
|
gap_size = left_gap_size + right_gap_size
|
|
gap_is_negative = this_op.Left < this_op.Right
|
|
|
|
case numericOperands[uint64]:
|
|
left_val := this_op.Left
|
|
right_val := this_op.Right
|
|
gap_is_negative = false
|
|
if left_val < right_val {
|
|
gap_is_negative = true
|
|
gap_size = right_val - left_val
|
|
} else {
|
|
gap_size = left_val - right_val
|
|
}
|
|
|
|
default:
|
|
zero_gap, _ := newGapValue(uint64(0), false).(gapValue[uint64])
|
|
return zero_gap
|
|
}
|
|
|
|
this_gap, _ := newGapValue(gap_size, gap_is_negative).(gapValue[uint64])
|
|
return this_gap
|
|
} // MakeGap
|
|
|
|
func makeFloatGap[Op operandConstraint](operand numericOperands[Op]) gapValue[float64] {
|
|
switch this_op := any(operand).(type) {
|
|
case numericOperands[float64]:
|
|
left_val := this_op.Left
|
|
right_val := this_op.Right
|
|
gap_is_negative := false
|
|
var gap_size float64
|
|
if left_val < right_val {
|
|
gap_is_negative = true
|
|
gap_size = right_val - left_val
|
|
} else {
|
|
gap_size = left_val - right_val
|
|
}
|
|
|
|
this_gap, _ := newGapValue(gap_size, gap_is_negative).(gapValue[float64])
|
|
return this_gap
|
|
|
|
default:
|
|
zero_gap, _ := newGapValue(float64(0.0), false).(gapValue[float64])
|
|
return zero_gap
|
|
}
|
|
} // MakeFloatGap
|