pax_global_header00006660000000000000000000000064151625205370014520gustar00rootroot0000000000000052 comment=2f2eacf65906fb065e7cd93f1156d9c6a7f582f2 golang-github-antithesishq-antithesis-sdk-go-0.7.0/000077500000000000000000000000001516252053700223105ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/.github/000077500000000000000000000000001516252053700236505ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/.github/workflows/000077500000000000000000000000001516252053700257055ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/.github/workflows/ci.yaml000066400000000000000000000005031516252053700271620ustar00rootroot00000000000000name: CI on: workflow_dispatch: pull_request: push: branches: [main] jobs: lints: name: Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: DeterminateSystems/nix-installer-action@main - run: nix-build -A go_sdk - run: nix-build -A go_sdk_no_antithesis golang-github-antithesishq-antithesis-sdk-go-0.7.0/.github/workflows/zulip_notifier.yml000066400000000000000000000017631516252053700315010ustar00rootroot00000000000000name: Zulip Notification Bot on: push: permissions: contents: read jobs: post-to-zulip: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/github-script@v7 id: generate-msg with: script: | let message = `- **${context.actor}** \`${context.ref}\` | ${context.sha.substring(0,7)} | [${context.payload.head_commit.message?.split('\n')[0]}](${context.payload.compare})` let topic = context.repo.repo core.setOutput("topic", topic); core.setOutput("msg", message); - name: Send a stream message uses: zulip/github-actions-zulip/send-message@v1 with: api-key: ${{ secrets.ZULIP_API_KEY }} email: ${{ secrets.ZULIP_BOT_EMAIL }} organization-url: ${{ secrets.ZULIP_ORG_URL }} to: "Commits" type: "stream" topic: ${{ steps.generate-msg.outputs.topic }} content: ${{ steps.generate-msg.outputs.msg }} golang-github-antithesishq-antithesis-sdk-go-0.7.0/.gitignore000066400000000000000000000000201516252053700242700ustar00rootroot00000000000000result result-* golang-github-antithesishq-antithesis-sdk-go-0.7.0/LICENSE000066400000000000000000000020531516252053700233150ustar00rootroot00000000000000MIT License Copyright (c) 2024 Antithesis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-antithesishq-antithesis-sdk-go-0.7.0/README.md000066400000000000000000000011471516252053700235720ustar00rootroot00000000000000 # Antithesis Go SDK This library provides methods for Go programs to configure the [Antithesis](https://antithesis.com) platform. Functionality is grouped into the packages [`assert`](https://antithesis.com/docs/generated/sdk/golang/assert/) for defining new test properties, [`random`](https://antithesis.com/docs/generated/sdk/golang/random/) for Antithesis input, and [`lifecycle`](https://antithesis.com/docs/generated/sdk/golang/lifecycle/) for controlling the Antithesis simulation. For general usage guidance see the [Antithesis Go SDK Documentation](https://antithesis.com/docs/using_antithesis/sdk/go/) golang-github-antithesishq-antithesis-sdk-go-0.7.0/assert/000077500000000000000000000000001516252053700236115ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/assert/assert.go000066400000000000000000000237151516252053700254510ustar00rootroot00000000000000//go:build !no_antithesis_sdk // Package assert enables defining [test properties] about your program or [workload]. It is part of the [Antithesis Go SDK], which enables Go applications to integrate with the [Antithesis platform]. // // Code that uses this package should be instrumented with the [antithesis-go-generator] utility. This step is required for the Always, Sometime, and Reachable methods. It is not required for the Unreachable and AlwaysOrUnreachable methods, but it will improve the experience of using them. // // These functions are no-ops with minimal performance overhead when called outside of the Antithesis environment. However, if the environment variable ANTITHESIS_SDK_LOCAL_OUTPUT is set, these functions will log to the file pointed to by that variable using a structured JSON format defined [here]. This allows you to make use of the Antithesis assertions package in your regular testing, or even in production. In particular, very few assertions frameworks offer a convenient way to define [Sometimes assertions], but they can be quite useful even outside Antithesis. // // Each function in this package takes a parameter called message, which is a human readable identifier used to aggregate assertions. Antithesis generates one test property per unique message and this test property will be named "" in the [triage report]. // // This test property either passes or fails, which depends upon the evaluation of every assertion that shares its message. Different assertions in different parts of the code should have different message, but the same assertion should always have the same message even if it is moved to a different file. // // Each function also takes a parameter called details, which is a key-value map of optional additional information provided by the user to add context for assertion failures. The information that is logged will appear in the [triage report], under the details section of the corresponding property. Normally the values passed to details are evaluated at runtime. // // [Antithesis Go SDK]: https://antithesis.com/docs/using_antithesis/sdk/go/ // [Antithesis platform]: https://antithesis.com // [test properties]: https://antithesis.com/docs/properties_assertions/properties/ // [workload]: https://antithesis.com/docs/test_templates/first_test/ // [antithesis-go-generator]: https://antithesis.com/docs/using_antithesis/sdk/go/instrumentor/ // [triage report]: https://antithesis.com/docs/reports/ // [here]: https://antithesis.com/docs/using_antithesis/sdk/fallback/ // [Sometimes assertions]: https://antithesis.com/docs/best_practices/sometimes_assertions/ // // [details]: https://antithesis.com/docs/reports/ package assert import ( "encoding/json" "fmt" ) type assertInfo struct { Location *locationInfo `json:"location"` Details map[string]any `json:"details"` AssertType string `json:"assert_type"` DisplayType string `json:"display_type"` Message string `json:"message"` Id string `json:"id"` Hit bool `json:"hit"` MustHit bool `json:"must_hit"` Condition bool `json:"condition"` } // Create a custom json marshaler for assertInfo so that we can force Errors to be marshaled with their error details. // Without this, custom errors are marshaled as an empty object because the default json marshaler doesn't include the error // (because it's a method - not an exported struct field). func (f assertInfo) MarshalJSON() ([]byte, error) { type alias assertInfo // prevent infinite recursion a := alias(f) if a.Details != nil { a.Details = normalizeMap(a.Details) } return json.Marshal(a) } type jsonError struct { innerError error } func (e jsonError) MarshalJSON() ([]byte, error) { // Marshal this as the debug output string instead of e.Error(). These should be equivalent, but Sprintf correctly // handles nil values for us (which otherwise are annoying to defend against due to this - https://go.dev/doc/faq#nil_error) return json.Marshal(fmt.Sprintf("%+v", e.innerError)) } // Recursively replace any `error` with jsonError while doing a deep copy. // Most of the logic is in the normalize method below. This method exists to localize the type assertions // and provide a function that takes in/out a map instead of any. func normalizeMap(v map[string]any) map[string]any { return normalize(v).(map[string]any) } func normalize(input any) any { // This switch will miss some cases (pointers, structs, non-any types), but should catch a very large proportion of real error interfaces // in real details objects. We can augment this if we find other cases common enough to support. switch inputTyped := input.(type) { case error: // Check if the underlying error implements json.Marshaler, so that if the error // already knows who to marshal itself, we don't override that. if _, ok := inputTyped.(json.Marshaler); ok { return inputTyped } else { return jsonError{inputTyped} } case map[string]any: out := make(map[string]any, len(inputTyped)) for k, v := range inputTyped { out[k] = normalize(v) } return out case []any: out := make([]any, len(inputTyped)) for i := range inputTyped { out[i] = normalize(inputTyped[i]) } return out default: return input } } type wrappedAssertInfo struct { A *assertInfo `json:"antithesis_assert"` } // -------------------------------------------------------------------------------- // Assertions // -------------------------------------------------------------------------------- const ( wasHit = true mustBeHit = true optionallyHit = false expectingTrue = true ) const ( universalTest = "always" existentialTest = "sometimes" reachabilityTest = "reachability" ) const ( alwaysDisplay = "Always" alwaysOrUnreachableDisplay = "AlwaysOrUnreachable" sometimesDisplay = "Sometimes" reachableDisplay = "Reachable" unreachableDisplay = "Unreachable" ) // Always asserts that condition is true every time this function is called, and that it is called at least once. The corresponding test property will be viewable in the Antithesis SDK: Always group of your triage report. func Always(condition bool, message string, details map[string]any) { locationInfo := newLocationInfo(offsetAPICaller) id := makeKey(message, locationInfo) assertImpl(condition, message, details, locationInfo, wasHit, mustBeHit, universalTest, alwaysDisplay, id) } // AlwaysOrUnreachable asserts that condition is true every time this function is called. The corresponding test property will pass if the assertion is never encountered (unlike Always assertion types). This test property will be viewable in the “Antithesis SDK: Always” group of your triage report. func AlwaysOrUnreachable(condition bool, message string, details map[string]any) { locationInfo := newLocationInfo(offsetAPICaller) id := makeKey(message, locationInfo) assertImpl(condition, message, details, locationInfo, wasHit, optionallyHit, universalTest, alwaysOrUnreachableDisplay, id) } // Sometimes asserts that condition is true at least one time that this function was called. (If the assertion is never encountered, the test property will therefore fail.) This test property will be viewable in the “Antithesis SDK: Sometimes” group. func Sometimes(condition bool, message string, details map[string]any) { locationInfo := newLocationInfo(offsetAPICaller) id := makeKey(message, locationInfo) assertImpl(condition, message, details, locationInfo, wasHit, mustBeHit, existentialTest, sometimesDisplay, id) } // Unreachable asserts that a line of code is never reached. The corresponding test property will fail if this function is ever called. (If it is never called the test property will therefore pass.) This test property will be viewable in the “Antithesis SDK: Reachablity assertions” group. func Unreachable(message string, details map[string]any) { locationInfo := newLocationInfo(offsetAPICaller) id := makeKey(message, locationInfo) assertImpl(false, message, details, locationInfo, wasHit, optionallyHit, reachabilityTest, unreachableDisplay, id) } // Reachable asserts that a line of code is reached at least once. The corresponding test property will pass if this function is ever called. (If it is never called the test property will therefore fail.) This test property will be viewable in the “Antithesis SDK: Reachablity assertions” group. func Reachable(message string, details map[string]any) { locationInfo := newLocationInfo(offsetAPICaller) id := makeKey(message, locationInfo) assertImpl(true, message, details, locationInfo, wasHit, mustBeHit, reachabilityTest, reachableDisplay, id) } // AssertRaw is a low-level method designed to be used by third-party frameworks. Regular users of the assert package should not call it. func AssertRaw(cond bool, message string, details map[string]any, classname, funcname, filename string, line int, hit bool, mustHit bool, assertType string, displayType string, id string, ) { assertImpl(cond, message, details, &locationInfo{classname, funcname, filename, line, columnUnknown}, hit, mustHit, assertType, displayType, id) } func assertImpl(cond bool, message string, details map[string]any, loc *locationInfo, hit bool, mustHit bool, assertType string, displayType string, id string, ) { trackerEntry := assertTracker.getTrackerEntry(id, loc.Filename, loc.Classname) // Always grab the Filename and Classname captured when the trackerEntry was established // This provides the consistency needed between instrumentation-time and runtime if loc.Filename != trackerEntry.Filename { loc.Filename = trackerEntry.Filename } if loc.Classname != trackerEntry.Classname { loc.Classname = trackerEntry.Classname } aI := &assertInfo{ Hit: hit, MustHit: mustHit, AssertType: assertType, DisplayType: displayType, Message: message, Condition: cond, Id: id, Location: loc, Details: details, } trackerEntry.emit(aI) } func makeKey(message string, _ *locationInfo) string { return message } golang-github-antithesishq-antithesis-sdk-go-0.7.0/assert/assert_noop.go000066400000000000000000000012301516252053700264700ustar00rootroot00000000000000//go:build no_antithesis_sdk package assert func Always(condition bool, message string, details map[string]any) {} func AlwaysOrUnreachable(condition bool, message string, details map[string]any) {} func Sometimes(condition bool, message string, details map[string]any) {} func Unreachable(message string, details map[string]any) {} func Reachable(message string, details map[string]any) {} func AssertRaw(cond bool, message string, details map[string]any, classname, funcname, filename string, line int, hit bool, mustHit bool, assertType string, displayType string, id string, ) { } golang-github-antithesishq-antithesis-sdk-go-0.7.0/assert/assert_types.go000066400000000000000000000013201516252053700266610ustar00rootroot00000000000000package assert // Allowable numeric types of comparison parameters type Number interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint32 | ~float32 | ~float64 | ~uint64 | ~uint | ~uintptr } // Internally, numeric guidanceFn Operands only use these type operandConstraint interface { int32 | int64 | uint64 | float64 } type numConstraint interface { uint64 | float64 } // Used for boolean assertions type NamedBool struct { First string `json:"first"` Second bool `json:"second"` } // Convenience function to construct a NamedBool used for boolean assertions func NewNamedBool(first string, second bool) *NamedBool { p := NamedBool{ First: first, Second: second, } return &p } golang-github-antithesishq-antithesis-sdk-go-0.7.0/assert/boolean_guidance.go000066400000000000000000000030561516252053700274220ustar00rootroot00000000000000//go:build !no_antithesis_sdk package assert import ( "sync" "github.com/antithesishq/antithesis-sdk-go/internal" ) // TODO: Tracker is intended to prevent sending the same guidance // more than once. In this case, we always send, so the tracker // is not presently used. type booleanGuidance struct { n int } type booleanGuidanceTracker map[string]*booleanGuidance var ( boolean_guidance_tracker booleanGuidanceTracker = make(booleanGuidanceTracker) boolean_guidance_tracker_mutex sync.Mutex boolean_guidance_info_mutex sync.Mutex ) func (tracker booleanGuidanceTracker) getTrackerEntry(messageKey string) *booleanGuidance { var trackerEntry *booleanGuidance var ok bool if tracker == nil { return nil } boolean_guidance_tracker_mutex.Lock() defer boolean_guidance_tracker_mutex.Unlock() if trackerEntry, ok = boolean_guidance_tracker[messageKey]; !ok { trackerEntry = newBooleanGuidance() tracker[messageKey] = trackerEntry } return trackerEntry } // Create a boolean guidance tracker func newBooleanGuidance() *booleanGuidance { trackerInfo := booleanGuidance{} return &trackerInfo } func (tI *booleanGuidance) send_value(bgI *booleanGuidanceInfo) { if tI == nil { return } boolean_guidance_info_mutex.Lock() defer boolean_guidance_info_mutex.Unlock() // The tracker entry should be consulted to determine // if this Guidance info has already been sent, or not. emitBooleanGuidance(bgI) } func emitBooleanGuidance(bgI *booleanGuidanceInfo) error { return internal.Json_data(map[string]any{"antithesis_guidance": bgI}) } golang-github-antithesishq-antithesis-sdk-go-0.7.0/assert/location.go000066400000000000000000000031021516252053700257440ustar00rootroot00000000000000//go:build !no_antithesis_sdk package assert import ( "path" "runtime" "strings" ) // stackFrameOffset indicates how many frames to go up in the // call stack to find the filename/location/line info. As // this work is always done in NewLocationInfo(), the offset is // specified from the perspective of NewLocationInfo type stackFrameOffset int // Order is important here since iota is being used const ( offsetNewLocationInfo stackFrameOffset = iota offsetHere offsetAPICaller offsetAPICallersCaller ) // locationInfo represents the attributes known at instrumentation time // for each Antithesis assertion discovered type locationInfo struct { Classname string `json:"class"` Funcname string `json:"function"` Filename string `json:"file"` Line int `json:"begin_line"` Column int `json:"begin_column"` } // columnUnknown is used when the column associated with // a locationInfo is not available const columnUnknown = 0 // NewLocationInfo creates a locationInfo directly from // the current execution context func newLocationInfo(nframes stackFrameOffset) *locationInfo { // Get location info and add to details funcname := "*function*" classname := "*class*" pc, filename, line, ok := runtime.Caller(int(nframes)) if !ok { filename = "*file*" line = 0 } else { if this_func := runtime.FuncForPC(pc); this_func != nil { fullname := this_func.Name() funcname = path.Ext(fullname) classname, _ = strings.CutSuffix(fullname, funcname) funcname = funcname[1:] } } return &locationInfo{classname, funcname, filename, line, columnUnknown} } golang-github-antithesishq-antithesis-sdk-go-0.7.0/assert/numeric_guidance.go000066400000000000000000000203131516252053700274400ustar00rootroot00000000000000//go:build !no_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: = (left - right). The gap_size is abs() 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 golang-github-antithesishq-antithesis-sdk-go-0.7.0/assert/rich_assert.go000066400000000000000000000337061516252053700264570ustar00rootroot00000000000000//go:build !no_antithesis_sdk package assert // A type for writing raw assertions. // guidanceFnType allows the assertion to provide guidance to // the Antithesis platform when testing in Antithesis. // Regular users of the assert package should not use it. type guidanceFnType int const ( guidanceFnMaximize guidanceFnType = iota // Maximize (left - right) values guidanceFnMinimize // Minimize (left - right) values guidanceFnWantAll // Encourages fuzzing explorations where boolean values are true guidanceFnWantNone // Encourages fuzzing explorations where boolean values are false guidanceFnExplore ) // guidanceFnExplore func get_guidance_type_string(gt guidanceFnType) string { switch gt { case guidanceFnMaximize, guidanceFnMinimize: return "numeric" case guidanceFnWantAll, guidanceFnWantNone: return "boolean" case guidanceFnExplore: return "json" } return "" } type numericOperands[T operandConstraint] struct { Left T `json:"left"` Right T `json:"right"` } type guidanceInfo struct { Data any `json:"guidance_data,omitempty"` Location *locationInfo `json:"location"` GuidanceType string `json:"guidance_type"` Message string `json:"message"` Id string `json:"id"` Maximize bool `json:"maximize"` Hit bool `json:"hit"` } type booleanGuidanceInfo struct { Data any `json:"guidance_data,omitempty"` Location *locationInfo `json:"location"` GuidanceType string `json:"guidance_type"` Message string `json:"message"` Id string `json:"id"` Maximize bool `json:"maximize"` Hit bool `json:"hit"` } func uses_maximize(gt guidanceFnType) bool { return gt == guidanceFnMaximize || gt == guidanceFnWantAll } func newOperands[T Number](left, right T) any { switch any(left).(type) { case int8, int16, int32: return numericOperands[int32]{int32(left), int32(right)} case int, int64: return numericOperands[int64]{int64(left), int64(right)} case uint8, uint16, uint32, uint, uint64, uintptr: return numericOperands[uint64]{uint64(left), uint64(right)} case float32, float64: return numericOperands[float64]{float64(left), float64(right)} } return nil } func build_numeric_guidance[T Number](gt guidanceFnType, message string, left, right T, loc *locationInfo, id string, hit bool) *guidanceInfo { operands := newOperands(left, right) if !hit { operands = nil } gI := guidanceInfo{ GuidanceType: get_guidance_type_string(gt), Message: message, Id: id, Location: loc, Maximize: uses_maximize(gt), Data: operands, Hit: hit, } return &gI } type namedBoolDictionary map[string]bool func build_boolean_guidance(gt guidanceFnType, message string, named_bools []NamedBool, loc *locationInfo, id string, hit bool) *booleanGuidanceInfo { var guidance_data any // To ensure the sequence and naming for the named_bool values if hit { named_bool_dictionary := namedBoolDictionary{} for _, named_bool := range named_bools { named_bool_dictionary[named_bool.First] = named_bool.Second } guidance_data = named_bool_dictionary } bgI := booleanGuidanceInfo{ GuidanceType: get_guidance_type_string(gt), Message: message, Id: id, Location: loc, Maximize: uses_maximize(gt), Data: guidance_data, Hit: hit, } return &bgI } func behavior_to_guidance(behavior string) guidanceFnType { guidance := guidanceFnExplore switch behavior { case "maximize": guidance = guidanceFnMaximize case "minimize": guidance = guidanceFnMinimize case "all": guidance = guidanceFnWantAll case "none": guidance = guidanceFnWantNone } return guidance } func numericGuidanceImpl[T Number](left, right T, message, id string, loc *locationInfo, guidanceFn guidanceFnType, hit bool) { tI := numeric_guidance_tracker.getTrackerEntry(id, gapTypeForOperand(left), uses_maximize(guidanceFn)) gI := build_numeric_guidance(guidanceFn, message, left, right, loc, id, hit) send_value_if_needed(tI, gI) } func booleanGuidanceImpl(named_bools []NamedBool, message, id string, loc *locationInfo, guidanceFn guidanceFnType, hit bool) { tI := boolean_guidance_tracker.getTrackerEntry(id) bgI := build_boolean_guidance(guidanceFn, message, named_bools, loc, id, hit) tI.send_value(bgI) } // NumericGuidanceRaw is a low-level method designed to be used by third-party frameworks. Regular users of the assert package should not call it. func NumericGuidanceRaw[T Number]( left, right T, message, id string, classname, funcname, filename string, line int, behavior string, hit bool, ) { loc := &locationInfo{classname, funcname, filename, line, columnUnknown} guidanceFn := behavior_to_guidance(behavior) numericGuidanceImpl(left, right, message, id, loc, guidanceFn, hit) } // BooleanGuidanceRaw is a low-level method designed to be used by third-party frameworks. Regular users of the assert package should not call it. func BooleanGuidanceRaw( named_bools []NamedBool, message, id string, classname, funcname, filename string, line int, behavior string, hit bool, ) { loc := &locationInfo{classname, funcname, filename, line, columnUnknown} guidanceFn := behavior_to_guidance(behavior) booleanGuidanceImpl(named_bools, message, id, loc, guidanceFn, hit) } func add_numeric_details[T Number](details map[string]any, left, right T) map[string]any { // ---------------------------------------------------- // Can not use maps.Clone() until go 1.21.0 or above // enhancedDetails := maps.Clone(details) // ---------------------------------------------------- enhancedDetails := map[string]any{} for k, v := range details { enhancedDetails[k] = v } enhancedDetails["left"] = left enhancedDetails["right"] = right return enhancedDetails } func add_boolean_details(details map[string]any, named_bools []NamedBool) map[string]any { // ---------------------------------------------------- // Can not use maps.Clone() until go 1.21.0 or above // enhancedDetails := maps.Clone(details) // ---------------------------------------------------- enhancedDetails := map[string]any{} for k, v := range details { enhancedDetails[k] = v } for _, named_bool := range named_bools { enhancedDetails[named_bool.First] = named_bool.Second } return enhancedDetails } // Equivalent to asserting Always(left > right, message, details). Information about left and right will automatically be added to the details parameter, with keys left and right. If you use this function for assertions that compare numeric quantities, you may help Antithesis find more bugs. func AlwaysGreaterThan[T Number](left, right T, message string, details map[string]any) { loc := newLocationInfo(offsetAPICaller) id := makeKey(message, loc) condition := left > right all_details := add_numeric_details(details, left, right) assertImpl(condition, message, all_details, loc, wasHit, mustBeHit, universalTest, alwaysDisplay, id) numericGuidanceImpl(left, right, message, id, loc, guidanceFnMinimize, wasHit) } // Equivalent to asserting Always(left >= right, message, details). Information about left and right will automatically be added to the details parameter, with keys left and right. If you use this function for assertions that compare numeric quantities, you may help Antithesis find more bugs. func AlwaysGreaterThanOrEqualTo[T Number](left, right T, message string, details map[string]any) { loc := newLocationInfo(offsetAPICaller) id := makeKey(message, loc) condition := left >= right all_details := add_numeric_details(details, left, right) assertImpl(condition, message, all_details, loc, wasHit, mustBeHit, universalTest, alwaysDisplay, id) numericGuidanceImpl(left, right, message, id, loc, guidanceFnMinimize, wasHit) } // Equivalent to asserting Sometimes(T left > T right, message, details). Information about left and right will automatically be added to the details parameter, with keys left and right. If you use this function for assertions that compare numeric quantities, you may help Antithesis find more bugs. func SometimesGreaterThan[T Number](left, right T, message string, details map[string]any) { loc := newLocationInfo(offsetAPICaller) id := makeKey(message, loc) condition := left > right all_details := add_numeric_details(details, left, right) assertImpl(condition, message, all_details, loc, wasHit, mustBeHit, existentialTest, sometimesDisplay, id) numericGuidanceImpl(left, right, message, id, loc, guidanceFnMaximize, wasHit) } // Equivalent to asserting Sometimes(T left >= T right, message, details). Information about left and right will automatically be added to the details parameter, with keys left and right. If you use this function for assertions that compare numeric quantities, you may help Antithesis find more bugs. func SometimesGreaterThanOrEqualTo[T Number](left, right T, message string, details map[string]any) { loc := newLocationInfo(offsetAPICaller) id := makeKey(message, loc) condition := left >= right all_details := add_numeric_details(details, left, right) assertImpl(condition, message, all_details, loc, wasHit, mustBeHit, existentialTest, sometimesDisplay, id) numericGuidanceImpl(left, right, message, id, loc, guidanceFnMaximize, wasHit) } // Equivalent to asserting Always(left < right, message, details). Information about left and right will automatically be added to the details parameter, with keys left and right. If you use this function for assertions that compare numeric quantities, you may help Antithesis find more bugs. func AlwaysLessThan[T Number](left, right T, message string, details map[string]any) { loc := newLocationInfo(offsetAPICaller) id := makeKey(message, loc) condition := left < right all_details := add_numeric_details(details, left, right) assertImpl(condition, message, all_details, loc, wasHit, mustBeHit, universalTest, alwaysDisplay, id) numericGuidanceImpl(left, right, message, id, loc, guidanceFnMaximize, wasHit) } // Equivalent to asserting Always(left <= right, message, details). Information about left and right will automatically be added to the details parameter, with keys left and right. If you use this function for assertions that compare numeric quantities, you may help Antithesis find more bugs. func AlwaysLessThanOrEqualTo[T Number](left, right T, message string, details map[string]any) { loc := newLocationInfo(offsetAPICaller) id := makeKey(message, loc) condition := left <= right all_details := add_numeric_details(details, left, right) assertImpl(condition, message, all_details, loc, wasHit, mustBeHit, universalTest, alwaysDisplay, id) numericGuidanceImpl(left, right, message, id, loc, guidanceFnMaximize, wasHit) } // Equivalent to asserting Sometimes(T left < T right, message, details). Information about left and right will automatically be added to the details parameter, with keys left and right. If you use this function for assertions that compare numeric quantities, you may help Antithesis find more bugs. func SometimesLessThan[T Number](left, right T, message string, details map[string]any) { loc := newLocationInfo(offsetAPICaller) id := makeKey(message, loc) condition := left < right all_details := add_numeric_details(details, left, right) assertImpl(condition, message, all_details, loc, wasHit, mustBeHit, existentialTest, sometimesDisplay, id) numericGuidanceImpl(left, right, message, id, loc, guidanceFnMinimize, wasHit) } // Equivalent to asserting Sometimes(T left <= T right, message, details). Information about left and right will automatically be added to the details parameter, with keys left and right. If you use this function for assertions that compare numeric quantities, you may help Antithesis find more bugs. func SometimesLessThanOrEqualTo[T Number](left, right T, message string, details map[string]any) { loc := newLocationInfo(offsetAPICaller) id := makeKey(message, loc) condition := left <= right all_details := add_numeric_details(details, left, right) assertImpl(condition, message, all_details, loc, wasHit, mustBeHit, existentialTest, sometimesDisplay, id) numericGuidanceImpl(left, right, message, id, loc, guidanceFnMinimize, wasHit) } // Asserts that every time this is called, at least one bool in named_bools is true. Equivalent to Always(named_bools[0].second || named_bools[1].second || ..., message, details). If you use this for assertions about the behavior of booleans, you may help Antithesis find more bugs. Information about named_bools will automatically be added to the details parameter, and the keys will be the names of the bools. func AlwaysSome(named_bools []NamedBool, message string, details map[string]any) { loc := newLocationInfo(offsetAPICaller) id := makeKey(message, loc) disjunction := false for _, named_bool := range named_bools { if named_bool.Second { disjunction = true break } } all_details := add_boolean_details(details, named_bools) assertImpl(disjunction, message, all_details, loc, wasHit, mustBeHit, universalTest, alwaysDisplay, id) booleanGuidanceImpl(named_bools, message, id, loc, guidanceFnWantNone, wasHit) } // Asserts that at least one time this is called, every bool in named_bools is true. Equivalent to Sometimes(named_bools[0].second && named_bools[1].second && ..., message, details). If you use this for assertions about the behavior of booleans, you may help Antithesis find more bugs. Information about named_bools will automatically be added to the details parameter, and the keys will be the names of the bools. func SometimesAll(named_bools []NamedBool, message string, details map[string]any) { loc := newLocationInfo(offsetAPICaller) id := makeKey(message, loc) conjunction := true for _, named_bool := range named_bools { if !named_bool.Second { conjunction = false break } } all_details := add_boolean_details(details, named_bools) assertImpl(conjunction, message, all_details, loc, wasHit, mustBeHit, existentialTest, sometimesDisplay, id) booleanGuidanceImpl(named_bools, message, id, loc, guidanceFnWantAll, wasHit) } golang-github-antithesishq-antithesis-sdk-go-0.7.0/assert/rich_assert_nop.go000066400000000000000000000025131516252053700273230ustar00rootroot00000000000000//go:build no_antithesis_sdk package assert func AlwaysGreaterThan[T Number](left, right T, message string, details map[string]any) {} func AlwaysGreaterThanOrEqualTo[T Number](left, right T, message string, details map[string]any) {} func SometimesGreaterThan[T Number](left, right T, message string, details map[string]any) {} func SometimesGreaterThanOrEqualTo[T Number](left, right T, message string, details map[string]any) {} func AlwaysLessThan[T Number](left, right T, message string, details map[string]any) {} func AlwaysLessThanOrEqualTo[T Number](left, right T, message string, details map[string]any) {} func SometimesLessThan[T Number](left, right T, message string, details map[string]any) {} func SometimesLessThanOrEqualTo[T Number](left, right T, message string, details map[string]any) {} func AlwaysSome(named_bool []NamedBool, message string, details map[string]any) {} func SometimesAll(named_bool []NamedBool, message string, details map[string]any) {} func NumericGuidanceRaw[T Number](left, right T, message, id string, classname, funcname, filename string, line int, behavior string, hit bool, ) { } func BooleanGuidanceRaw( named_bools []NamedBool, message, id string, classname, funcname, filename string, line int, behavior string, hit bool, ) { } golang-github-antithesishq-antithesis-sdk-go-0.7.0/assert/tracker.go000066400000000000000000000041431516252053700255750ustar00rootroot00000000000000//go:build !no_antithesis_sdk package assert import ( "runtime" "sync" "sync/atomic" "github.com/antithesishq/antithesis-sdk-go/internal" ) type trackerInfo struct { Filename string Classname string PassCount int FailCount int } type emitTracker map[string]*trackerInfo // assert_tracker (global) keeps track of the unique asserts evaluated var ( assertTracker emitTracker = make(emitTracker) trackerMutex sync.Mutex trackerInfoMutex sync.Mutex ) func (tracker emitTracker) getTrackerEntry(messageKey string, filename, classname string) *trackerInfo { var trackerEntry *trackerInfo var ok bool if tracker == nil { return nil } trackerMutex.Lock() defer trackerMutex.Unlock() if trackerEntry, ok = tracker[messageKey]; !ok { trackerEntry = newTrackerInfo(filename, classname) tracker[messageKey] = trackerEntry } return trackerEntry } func newTrackerInfo(filename, classname string) *trackerInfo { trackerInfo := trackerInfo{ PassCount: 0, FailCount: 0, Filename: filename, Classname: classname, } return &trackerInfo } func (ti *trackerInfo) emit(ai *assertInfo) { if ti == nil || ai == nil { return } // Registrations are just sent to voidstar hit := ai.Hit if !hit { emitAssert(ai) return } var err error cond := ai.Condition trackerInfoMutex.Lock() defer trackerInfoMutex.Unlock() if cond { if ti.PassCount == 0 { err = emitAssert(ai) } if err == nil { ti.PassCount++ } return } if ti.FailCount == 0 { err = emitAssert(ai) } if err == nil { ti.FailCount++ } } func versionMessage() { languageBlock := map[string]any{ "name": "Go", "version": runtime.Version(), } versionBlock := map[string]any{ "language": languageBlock, "sdk_version": internal.SDK_Version, "protocol_version": internal.Protocol_Version, } internal.Json_data(map[string]any{"antithesis_sdk": versionBlock}) } // package-level flag var hasEmitted atomic.Bool // initialzed to false func emitAssert(ai *assertInfo) error { if hasEmitted.CompareAndSwap(false, true) { versionMessage() } return internal.Json_data(wrappedAssertInfo{ai}) } golang-github-antithesishq-antithesis-sdk-go-0.7.0/default.nix000066400000000000000000000061771516252053700244670ustar00rootroot00000000000000{ pkgs ? import {}, doc2go ? pkgs.doc2go, lib ? pkgs.lib, go ? pkgs.go, pandoc ? pkgs.pandoc, stdenv ? pkgs.stdenv, }: let index_template = pkgs.writeText "doc_template.html" '' $body$ ''; docPages = { "assert" = { title = "package assert"; desc = "Docs page for antithesis sdk golang assert package"; }; random = { title = "package random"; desc = "Docs page for antithesis sdk golang random package"; }; lifecycle = { title = "package lifecycle"; desc = "Docs page for antithesis sdk golang lifecycle package"; }; }; docs = stdenv.mkDerivation { name = "go_sdk_docs"; src = ./.; # TODO: filter nativeBuildInputs = [ go pandoc (doc2go.overrideAttrs (old: { patches = (old.patches or [ ]) ++ [ ./doc2go-headers.patch ./doc2go-title.patch ./doc2go-meta-desc.patch ]; doCheck = false; })) ]; buildPhase = '' export HOME=$TMPDIR mkdir -p $out/docs # TODO: can add `-emded` to generate basic stubs for the docs with no styling to customize our own doc2go -home github.com/antithesishq/antithesis-sdk-go -out $out/docs ${lib.concatMapStringsSep " " (p: "./${p}") (lib.attrNames docPages)} ${lib.concatMapStringsSep "\n" ({ name, value }: let title = if value ? title then value.title else "package ${name}"; in '' substituteInPlace $out/docs/${name}/index.html --replace-fail "%META_DESCRIPTION%" "${value.desc}" substituteInPlace $out/docs/${name}/index.html --replace-fail "%TITLE%" "${title}" '') (lib.attrsToList docPages)} pandoc --template ${index_template} -o $out/index.html README.md ''; }; go_sdk = pkgs.buildGoModule { pname = "antithesis-go-sdk"; version = "v0.3.4"; src = ./.; vendorHash = "sha256-K/4hq42VooKVwHydh+SWB8YAzumkONpglfHyRvr82z8="; meta = { description = "Antithesis go sdk."; homepage = "https://github.com/antithesishq/antithesis-sdk-go"; platforms = lib.platforms.linux; licenses = lib.licenses.mit; }; }; go_sdk_no_antithesis = go_sdk.overrideAttrs (old: { pname = "antithesis-go-sdk-no-antithesis"; checkFlags = (old.checkFlags or []) ++ ["-tags=no_antithesis_sdk"]; }); in { inherit docs go_sdk go_sdk_no_antithesis; } golang-github-antithesishq-antithesis-sdk-go-0.7.0/doc2go-headers.patch000066400000000000000000000062361516252053700261260ustar00rootroot00000000000000diff --git a/internal/html/tmpl/package.html b/internal/html/tmpl/package.html index b081d51c30..551076b084 100644 --- a/internal/html/tmpl/package.html +++ b/internal/html/tmpl/package.html @@ -8,14 +8,14 @@ {{ define "NavbarExtra" }} | Index{{ end -}} {{ define "Body" -}} -

+

{{- with .BinName }}{{ . }}{{ else }}package {{ .Name }}{{ end -}} -

+ {{ .Import | code }} {{ .Doc | doc 3 -}} {{ template "examples" (dict "Level" 3 "Examples" .Examples) -}} -

Index

+

Index

{{ if or .Constants .Variables .Functions .Types -}}
    {{ if .Constants }}
  • Constants
  • {{ end -}} @@ -42,7 +42,7 @@ {{- end }} {{- with .AllExamples -}} -

    Examples

    +

    Examples

      {{ range . -}} {{- $id := printf "example-%s" .Parent.String -}} @@ -55,20 +55,20 @@ {{- end -}} {{ with .Constants -}} -

      Constants

      +

      Constants

      {{ range . }}{{ template "constOrVar" . }}{{ end -}} {{ end -}} {{ with .Variables -}} -

      Variables

      +

      Variables

      {{ range . }}{{ template "constOrVar" . }}{{ end -}} {{ end -}} {{ with .Functions -}} -

      Functions

      +

      Functions

      {{ range . -}} {{ if .Deprecated }}
      {{ end -}} -

      func {{ .Name }} {{- template "deprecatedTag" . }}

      +

      func {{ .Name }} {{- template "deprecatedTag" . }}

      {{- if .Deprecated }}
      {{ end }} {{ .Decl | code }} {{ .Doc | doc 4 -}} @@ -78,10 +78,10 @@ {{ end -}} {{ with .Types -}} -

      Types

      +

      Types

      {{ range . -}} {{ if .Deprecated }}
      {{ end -}} -

      type {{ .Name }} {{- template "deprecatedTag" . }}

      +

      type {{ .Name }} {{- template "deprecatedTag" . }}

      {{- if .Deprecated }}
      {{ end }} {{ .Decl | code }} {{ .Doc | doc 4 -}} @@ -120,7 +120,7 @@ {{ $id := .Name -}} {{ with .RecvType }}{{ $id = printf "%s.%s" . $id }}{{ end -}} {{ if .Deprecated }}
      {{ end -}} -

      func {{ with .Recv }}({{ . }}) {{end }}{{ .Name }} {{- template "deprecatedTag" . }}

      +

      func {{ with .Recv }}({{ . }}) {{end }}{{ .Name }} {{- template "deprecatedTag" . }}

      {{- if .Deprecated }}
      {{ end }} {{ .Decl | code }} {{ .Doc | doc 5 -}} golang-github-antithesishq-antithesis-sdk-go-0.7.0/doc2go-meta-desc.patch000066400000000000000000000006021516252053700263440ustar00rootroot00000000000000diff --git a/internal/html/tmpl/package.html b/internal/html/tmpl/package.html index 893dff9a2d..b3ec7886f7 100644 --- a/internal/html/tmpl/package.html +++ b/internal/html/tmpl/package.html @@ -2,6 +2,8 @@ %TITLE% + + {{ end -}} {{ define "PkgVersion" }}{{ with .PkgVersion }}{{ . }} | {{ end }}{{ end }} golang-github-antithesishq-antithesis-sdk-go-0.7.0/doc2go-title.patch000066400000000000000000000005171516252053700256300ustar00rootroot00000000000000diff --git a/internal/html/tmpl/package.html b/internal/html/tmpl/package.html index 551076b084..893dff9a2d 100644 --- a/internal/html/tmpl/package.html +++ b/internal/html/tmpl/package.html @@ -1,6 +1,6 @@ {{ define "Head" -}} - {{- with .BinName }}{{ . }}{{ else }}{{ .Name }}{{ end -}} + %TITLE% {{ end -}} golang-github-antithesishq-antithesis-sdk-go-0.7.0/go.mod000066400000000000000000000006221516252053700234160ustar00rootroot00000000000000module github.com/antithesishq/antithesis-sdk-go go 1.24.0 require golang.org/x/tools v0.42.0 require ( github.com/go-quicktest/qt v1.101.0 golang.org/x/mod v0.33.0 ) require ( github.com/google/go-cmp v0.6.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect golang.org/x/sync v0.19.0 // indirect ) golang-github-antithesishq-antithesis-sdk-go-0.7.0/go.sum000066400000000000000000000030701516252053700234430ustar00rootroot00000000000000github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang-github-antithesishq-antithesis-sdk-go-0.7.0/instrumentation/000077500000000000000000000000001516252053700255535ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/instrumentation/bitset.go000066400000000000000000000026561516252053700274050ustar00rootroot00000000000000package instrumentation import ( "sync" ) // bitSet is a rudimentary implementation suitable for // the Antithesis Go instrumentation wrappers. One // can set bits; one cannot unset them. It is 0-indexed, // although our edges begin at 1. The value type is // int, to be consistent with our "edge" type. Negative // index values will result in a panic. type bitSet struct { // There seems to be no performance differences // among the different integer types. slots []uint64 mutex sync.RWMutex } func (b *bitSet) slotAndBit(index int) (int, int) { // One assumes that the compiler will optimize these. return index / 64, index % 64 } func (b *bitSet) get(slot, bit int) bool { if slot >= len(b.slots) { return false } mask := (uint64(1) << bit) return (b.slots[slot] & mask) != 0 } // Get returns the value at this index. func (b *bitSet) Get(index int) bool { slot, bit := b.slotAndBit(index) b.mutex.RLock() defer b.mutex.RUnlock() return b.get(slot, bit) } // Set will only switch a bit on. func (b *bitSet) Set(index int) { slot, bit := b.slotAndBit(index) b.mutex.Lock() defer b.mutex.Unlock() if slot >= len(b.slots) { // Go takes care of the *capacity* under the covers. // So we don't need a tricky implementation, or // a fixed size. Expansion is cheap. extension := 1 + int(slot) - len(b.slots) b.slots = append(b.slots, make([]uint64, extension)...) } mask := (uint64(1) << bit) b.slots[slot] |= mask } golang-github-antithesishq-antithesis-sdk-go-0.7.0/instrumentation/notify.go000066400000000000000000000027261516252053700274210ustar00rootroot00000000000000//go:build !no_antithesis_sdk package instrumentation import ( "fmt" "os" "github.com/antithesishq/antithesis-sdk-go/assert" "github.com/antithesishq/antithesis-sdk-go/internal" ) var ( moduleInitialized = false moduleOffset uint64 edgesVisited = bitSet{} ) const instrumentor_tag = "github.com/antithesishq/antithesis-sdk-go/instrumentation" // InitializeModule should be called only once from a program. func InitializeModule(symbolTable string, edgeCount int) uint64 { if moduleInitialized { // We cannot support incorrect workflows. panic("InitializeModule() has already been called!") } executable, _ := os.Executable() details := map[string]any{ "executable": executable, "symbolTable": symbolTable, "edgeCount": edgeCount, } assert.Reachable("init_coverage_module() invoked", details) // WARN Re: integer type conversion, see https://github.com/golang/go/issues/29878 offset := internal.InitCoverage(uint64(edgeCount), symbolTable) moduleOffset = uint64(offset) moduleInitialized = true return moduleOffset } // Notify will be called from instrumented code. func Notify(edge int) { if !moduleInitialized { // We cannot support incorrect workflows. panic(fmt.Sprintf("%s.Notify() called before InitializeModule()", instrumentor_tag)) } if edgesVisited.Get(edge) { return } edgePlusOffset := uint64(edge) edgePlusOffset += moduleOffset mustCall := internal.Notify(edgePlusOffset) if !mustCall { edgesVisited.Set(edge) } } golang-github-antithesishq-antithesis-sdk-go-0.7.0/instrumentation/notify_noop.go000066400000000000000000000002371516252053700304470ustar00rootroot00000000000000//go:build no_antithesis_sdk package instrumentation func InitializeModule(symbolTable string, edgeCount int) uint64 { return 0 } func Notify(edge int) {} golang-github-antithesishq-antithesis-sdk-go-0.7.0/internal/000077500000000000000000000000001516252053700241245ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/internal/emit.go000066400000000000000000000041351516252053700254140ustar00rootroot00000000000000//go:build !no_antithesis_sdk package internal import ( "encoding/json" "log" "math/rand" "os" ) func Json_data(v any) error { if data, err := json.Marshal(v); err != nil { return err } else { handler.output(string(data)) return nil } } func Get_random() uint64 { return handler.random() } func Notify(edge uint64) bool { return handler.notify(edge) } func InitCoverage(num_edges uint64, symbols string) uint64 { return handler.init_coverage(num_edges, symbols) } type libHandler interface { output(message string) random() uint64 notify(edge uint64) bool init_coverage(num_edges uint64, symbols string) uint64 } const ( errorLogLinePrefix = "[* antithesis-sdk-go *]" ) var handler libHandler type localHandler struct { outputFile *os.File // can be nil } func (h *localHandler) output(message string) { msg_len := len(message) if msg_len == 0 { return } if h.outputFile != nil { h.outputFile.WriteString(message + "\n") } } func (h *localHandler) random() uint64 { return rand.Uint64() } func (h *localHandler) notify(edge uint64) bool { return false } func (h *localHandler) init_coverage(num_edges uint64, symbols string) uint64 { return 0 } func init() { handler = init_in_antithesis() if handler == nil { // Otherwise fallback to the local handler. handler = openLocalHandler() } } // If `localOutputEnvVar` is set to a non-empty path, attempt to open that path and truncate the file // to serve as the log file of the local handler. // Otherwise, we don't have a log file, and logging is a no-op in the local handler. func openLocalHandler() *localHandler { path, is_set := os.LookupEnv(localOutputEnvVar) if !is_set || len(path) == 0 { return &localHandler{nil} } // Open the file R/W (create if needed and possible) file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644) if err != nil { log.Printf("%s Failed to open path %s: %v", errorLogLinePrefix, path, err) file = nil } else if err = file.Truncate(0); err != nil { log.Printf("%s Failed to truncate file at %s: %v", errorLogLinePrefix, path, err) file = nil } return &localHandler{file} } golang-github-antithesishq-antithesis-sdk-go-0.7.0/internal/emit_test.go000066400000000000000000000027211516252053700264520ustar00rootroot00000000000000//go:build !no_antithesis_sdk package internal import ( "encoding/json" "os" "testing" ) var test_result bool func TestLocalHandlerFileOutput(t *testing.T) { path := os.TempDir() + string(os.PathSeparator) + "antithesis-test.log" os.Setenv(localOutputEnvVar, path) defer os.Unsetenv(localOutputEnvVar) handler = openLocalHandler() Json_data(map[string]string{ "test": "output", }) handler.(*localHandler).outputFile.Close() data, err := os.ReadFile(path) if err != nil { panic(err) } var result map[string]string if err = json.Unmarshal(data, &result); err != nil { panic(err) } if result["test"] != "output" { panic("JSON does not roundtrip") } } func TestLocalHandlerNop(t *testing.T) { os.Setenv(localOutputEnvVar, "") defer os.Unsetenv(localOutputEnvVar) handler = openLocalHandler() Json_data(map[string]string{ "test": "output", }) h, valid := handler.(*localHandler) if !valid { panic("Not using the local handler") } if h.outputFile != nil { panic("Should not be outputting to file") } } func TestVoidstarHandlerErr1(t *testing.T) { _, err := openSharedLib("path-not-exists") if err == nil { panic("Should failed to load library") } } func TestVoidstarHandlerErr2(t *testing.T) { _, err := openSharedLib(os.Args[0]) if err == nil { panic("Should failed to load library") } } func TestVoidstarHandlerErr3(t *testing.T) { _, err := openSharedLib("libc.so.6") if err == nil { panic("Should failed to load library") } } golang-github-antithesishq-antithesis-sdk-go-0.7.0/internal/sdk_const.go000066400000000000000000000007701516252053700264460ustar00rootroot00000000000000package internal // -------------------------------------------------------------------------------- // Versions // -------------------------------------------------------------------------------- const SDK_Version = "0.7.0" const Protocol_Version = "1.1.0" // -------------------------------------------------------------------------------- // Environment Vars // -------------------------------------------------------------------------------- const localOutputEnvVar = "ANTITHESIS_SDK_LOCAL_OUTPUT" golang-github-antithesishq-antithesis-sdk-go-0.7.0/internal/voidstar_handler.go000066400000000000000000000105331516252053700300050ustar00rootroot00000000000000//go:build !no_antithesis_sdk && linux && amd64 && cgo package internal import ( "fmt" "unsafe" "os" ) // -------------------------------------------------------------------------------- // To build and run an executable with this package // // CC=clang CGO_ENABLED=1 go run ./main.go // -------------------------------------------------------------------------------- // \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // // The commented lines below, and the `import "C"` line which must directly follow // the commented lines are used by CGO. They are load-bearing, and should not be // changed without first understanding how CGO uses them. // // \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // #cgo LDFLAGS: -ldl // // #include // #include // #include // #include // // typedef void (*go_fuzz_json_data_fn)(const char *data, size_t size); // void // go_fuzz_json_data(void *f, const char *data, size_t size) { // ((go_fuzz_json_data_fn)f)(data, size); // } // // typedef void (*go_fuzz_flush_fn)(void); // void // go_fuzz_flush(void *f) { // ((go_fuzz_flush_fn)f)(); // } // // typedef uint64_t (*go_fuzz_get_random_fn)(void); // uint64_t // go_fuzz_get_random(void *f) { // return ((go_fuzz_get_random_fn)f)(); // } // // typedef bool (*go_notify_coverage_fn)(size_t); // int // go_notify_coverage(void *f, size_t edges) { // bool b = ((go_notify_coverage_fn)f)(edges); // return b ? 1 : 0; // } // // typedef uint64_t (*go_init_coverage_fn)(size_t num_edges, const char *symbols); // uint64_t // go_init_coverage(void *f, size_t num_edges, const char *symbols) { // return ((go_init_coverage_fn)f)(num_edges, symbols); // } // import "C" const ( defaultNativeLibraryPath = "/usr/lib/libvoidstar.so" ) type voidstarHandler struct { fuzzJsonData unsafe.Pointer fuzzFlush unsafe.Pointer fuzzGetRandom unsafe.Pointer initCoverage unsafe.Pointer notifyCoverage unsafe.Pointer } func (h *voidstarHandler) output(message string) { msg_len := len(message) if msg_len == 0 { return } cstrMessage := C.CString(message) defer C.free(unsafe.Pointer(cstrMessage)) C.go_fuzz_json_data(h.fuzzJsonData, cstrMessage, C.ulong(msg_len)) C.go_fuzz_flush(h.fuzzFlush) } func (h *voidstarHandler) random() uint64 { return uint64(C.go_fuzz_get_random(h.fuzzGetRandom)) } func (h *voidstarHandler) init_coverage(num_edge uint64, symbols string) uint64 { cstrSymbols := C.CString(symbols) defer C.free(unsafe.Pointer(cstrSymbols)) return uint64(C.go_init_coverage(h.initCoverage, C.ulong(num_edge), cstrSymbols)) } func (h *voidstarHandler) notify(edge uint64) bool { ival := int(C.go_notify_coverage(h.notifyCoverage, C.ulong(edge))) return ival == 1 } // Attempt to load libvoidstar and some symbols from `path` func openSharedLib(path string) (*voidstarHandler, error) { cstrPath := C.CString(path) defer C.free(unsafe.Pointer(cstrPath)) dlError := func(message string) error { return fmt.Errorf("%s: (%s)", message, C.GoString(C.dlerror())) } sharedLib := C.dlopen(cstrPath, C.int(C.RTLD_NOW)) if sharedLib == nil { return nil, dlError("Can not load the Antithesis native library") } loadFunc := func(name string) (symbol unsafe.Pointer, err error) { cstrName := C.CString(name) defer C.free(unsafe.Pointer(cstrName)) if symbol = C.dlsym(sharedLib, cstrName); symbol == nil { err = dlError(fmt.Sprintf("Can not access symbol %s", name)) } return } fuzzJsonData, err := loadFunc("fuzz_json_data") if err != nil { return nil, err } fuzzFlush, err := loadFunc("fuzz_flush") if err != nil { return nil, err } fuzzGetRandom, err := loadFunc("fuzz_get_random") if err != nil { return nil, err } notifyCoverage, err := loadFunc("notify_coverage") if err != nil { return nil, err } initCoverage, err := loadFunc("init_coverage_module") if err != nil { return nil, err } return &voidstarHandler{fuzzJsonData, fuzzFlush, fuzzGetRandom, initCoverage, notifyCoverage}, nil } // If we have a file at `defaultNativeLibraryPath`, we load the shared library // (and panic on any error encountered during load). func init_in_antithesis() libHandler { if _, err := os.Stat(defaultNativeLibraryPath); err == nil { handler, err := openSharedLib(defaultNativeLibraryPath) if err != nil { panic(err) } return handler } return nil } golang-github-antithesishq-antithesis-sdk-go-0.7.0/internal/voidstar_handler_noop.go000066400000000000000000000002041516252053700310320ustar00rootroot00000000000000//go:build !no_antithesis_sdk && (!linux || !amd64 || !cgo) package internal func init_in_antithesis() libHandler { return nil } golang-github-antithesishq-antithesis-sdk-go-0.7.0/lifecycle/000077500000000000000000000000001516252053700242475ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/lifecycle/lifecycle.go000066400000000000000000000040271516252053700265400ustar00rootroot00000000000000//go:build !no_antithesis_sdk // Package lifecycle informs the Antithesis environment that particular test phases or milestones have been reached. It is part of the [Antithesis Go SDK], which enables Go applications to integrate with the [Antithesis platform]. // // Both functions take the parameter details: Optional additional information provided by the user to add context for assertion failures. The information that is logged will appear in the logs section of a [triage report]. Normally the values passed to details are evaluated at runtime. // // [Antithesis Go SDK]: https://antithesis.com/docs/using_antithesis/sdk/go/ // [Antithesis platform]: https://antithesis.com // [triage report]: https://antithesis.com/docs/reports/ package lifecycle import ( "github.com/antithesishq/antithesis-sdk-go/internal" ) // SetupComplete indicates to Antithesis that setup has completed. Call this function when your system and workload are fully initialized. After this function is called, Antithesis will take a snapshot of your system and begin [injecting faults]. // // Calling this function multiple times or from multiple processes will have no effect. Antithesis will treat the first time any process called this function as the moment that the setup was completed. // // [injecting faults]: https://antithesis.com/docs/environment/fault_injection/ func SetupComplete(details any) { statusBlock := map[string]any{ "status": "complete", "details": details, } internal.Json_data(map[string]any{"antithesis_setup": statusBlock}) } // SendEvent indicates to Antithesis that a certain event has been reached. It provides greater information about the ordering of events during the course of testing in Antithesis. // // In addition to details, you also provide an eventName, which is the name of the event that you are logging. This name will appear in the logs section of a [triage report]. // // [triage report]: https://antithesis.com/docs/reports/ func SendEvent(eventName string, details any) { internal.Json_data(map[string]any{eventName: details}) } golang-github-antithesishq-antithesis-sdk-go-0.7.0/lifecycle/lifecycle_noop.go000066400000000000000000000002231516252053700275650ustar00rootroot00000000000000//go:build no_antithesis_sdk package lifecycle func SetupComplete(details any) {} func SendEvent(eventName string, details any) {} golang-github-antithesishq-antithesis-sdk-go-0.7.0/random/000077500000000000000000000000001516252053700235705ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/random/random.go000066400000000000000000000026221516252053700254010ustar00rootroot00000000000000//go:build !no_antithesis_sdk // Package random is part of the [Antithesis Go SDK], which enables Go applications to integrate with the [Antithesis platform]. // // These functions should not be used to seed a conventional PRNG, and should not have their return values stored and used to make a decision at a later time. Doing either of these things makes it much harder for the Antithesis platform to control the history of your program's execution, and also makes it harder for Antithesis to learn which inputs provided at which times are most fruitful. Instead, you should call a function from the random package every time your program or [workload] needs to make a decision, at the moment that you need to make the decision. // // These functions are also safe to call outside the Antithesis environment, where they will fall back on values from [crypto/rand]. // // [Antithesis Go SDK]: https://antithesis.com/docs/using_antithesis/sdk/go/ // [Antithesis platform]: https://antithesis.com // [workload]: https://antithesis.com/docs/test_templates/first_test package random import ( "github.com/antithesishq/antithesis-sdk-go/internal" ) // GetRandom returns a uint64 value chosen by Antithesis. You should not store this value or use it to seed a PRNG, but should use it immediately. // // Deprecated: Use [Source] instead of calling this function directly. func GetRandom() uint64 { return internal.Get_random() } golang-github-antithesishq-antithesis-sdk-go-0.7.0/random/random_choice.go000066400000000000000000000010751516252053700267140ustar00rootroot00000000000000package random import "math/rand" // RandomChoice returns a randomly chosen item from a list of options. You should not store this value, but should use it immediately. // // This function is not purely for convenience. Signaling to the Antithesis platform that you intend to use a random value in a structured way enables it to provide more interesting choices over time. func RandomChoice[T any](things []T) T { numThings := len(things) if numThings == 0 { var nullThing T return nullThing } index := rand.New(Source()).Intn(numThings) return things[index] } golang-github-antithesishq-antithesis-sdk-go-0.7.0/random/random_choice_test.go000066400000000000000000000067431516252053700277620ustar00rootroot00000000000000package random import ( "testing" ) // To execute tests: // // go test -v github.com/antithesishq/antithesis-sdk-go/random func TestRandomChoice(t *testing.T) { type Thing struct { chosenCount int } choices := []any{ &Thing{}, &Thing{}, &Thing{}, &Thing{}, &Thing{}, } const N = 100 for i := 0; i < N; i++ { chosen := RandomChoice(choices) chosen.(*Thing).chosenCount += 1 } for i, anything := range choices { thing := anything.(*Thing) t.Logf("Thing %d/%d was chosen %d times", i+1, len(choices), thing.chosenCount) if thing.chosenCount == 0 { t.Fatalf("Some element was never chosen in %d random choices!", N) } } } func TestChoiceGenericCompatibility(t *testing.T) { type Thing struct { chosenCount int } choices := []any{ &Thing{}, &Thing{}, &Thing{}, &Thing{}, &Thing{}, } const N = 100 for i := 0; i < N; i++ { chosen := RandomChoice(choices) chosen.(*Thing).chosenCount += 1 } for i, anything := range choices { thing := anything.(*Thing) t.Logf("Thing %d/%d was chosen %d times", i+1, len(choices), thing.chosenCount) if thing.chosenCount == 0 { t.Fatalf("Some element was never chosen in %d random choices!", N) } } } func TestChoiceGenericMixedArrayCompatibility(t *testing.T) { type This struct { thisCount int } type That struct { thatCount int } choices := []any{ &This{}, &This{}, &That{}, &That{}, &That{}, } const N = 100 for i := 0; i < N; i++ { chosen := RandomChoice(choices) if t1, ok := chosen.(*This); ok { t1.thisCount += 1 } if t2, ok := chosen.(*That); ok { t2.thatCount += 1 } } for i, anything := range choices { if t1, ok := anything.(*This); ok { t.Logf("This at index %d of %d was chosen %d times", i+1, len(choices), t1.thisCount) if t1.thisCount == 0 { t.Fatalf("'This' element was never chosen in %d random choices!", N) } } if t2, ok := anything.(*That); ok { t.Logf("'That' at index %d of %d was chosen %d times", i+1, len(choices), t2.thatCount) if t2.thatCount == 0 { t.Fatalf("'That' element was never chosen in %d random choices!", N) } } } } func TestRandomChoiceGenericMixedPrimitives(t *testing.T) { choices := []any{ "Hello", 12.4, "How", true, "You", 10025, } counts := make(map[any]int) const N = 100 for i := 0; i < N; i++ { chosen := RandomChoice(choices) counts[chosen] += 1 } for i, s := range choices { count, present := counts[s] t.Logf("Item %v %d/%d was chosen %d times", s, i+1, len(choices), count) if !present { t.Fatalf("Some element was never chosen in %d random choices!", N) } } } func TestRandomChoiceGeneric(t *testing.T) { choices := []string{ "Hello", "World", "How", "Are", "You", "?", } counts := make(map[string]int) const N = 100 for i := 0; i < N; i++ { chosen := RandomChoice(choices) counts[chosen] += 1 } for i, s := range choices { count, present := counts[s] t.Logf("Item %d/%d was chosen %d times", i+1, len(choices), count) if !present { t.Fatalf("Some element was never chosen in %d random choices!", N) } } } func TestEmptyFloatChoiceGeneric(t *testing.T) { var choices []float64 got := RandomChoice(choices) want := float64(0.0) if got != want { t.Fatalf("Unexpected choice received - got %v want %f", got, want) } } func TestEmptyStringChoiceGeneric(t *testing.T) { var choices []string got := RandomChoice(choices) want := "" if got != want { t.Fatalf("Unexpected choice received - got %v want %s", got, want) } } golang-github-antithesishq-antithesis-sdk-go-0.7.0/random/random_local.go000066400000000000000000000003161516252053700265510ustar00rootroot00000000000000//go:build no_antithesis_sdk package random import ( crand "crypto/rand" "encoding/binary" ) func GetRandom() uint64 { var tmp [8]byte crand.Read(tmp[:]) return binary.LittleEndian.Uint64(tmp[:]) } golang-github-antithesishq-antithesis-sdk-go-0.7.0/random/source.go000066400000000000000000000011111516252053700254110ustar00rootroot00000000000000package random import ( "math" "math/rand" ) type source struct{} // Assert that source implements rand.Source64. var _ rand.Source64 = source{} func (source) Seed(int64) {} func (source) Int63() int64 { return int64(GetRandom() & (math.MaxUint64 >> 1)) } func (source) Uint64() uint64 { return GetRandom() } // Source initialises a source of pseudo-random data. // // Use this function to create a [math/rand.Rand] which provides feedback to the Antithesis platform. // // The returned source implements [math/rand.Source64]. func Source() rand.Source { return source{} } golang-github-antithesishq-antithesis-sdk-go-0.7.0/reformat.sh000077500000000000000000000021671516252053700244740ustar00rootroot00000000000000#! /bin/sh set -e go fmt -x github.com/antithesishq/antithesis-sdk-go/assert go fmt -x github.com/antithesishq/antithesis-sdk-go/instrumentation go fmt -x github.com/antithesishq/antithesis-sdk-go/internal go fmt -x github.com/antithesishq/antithesis-sdk-go/lifecycle go fmt -x github.com/antithesishq/antithesis-sdk-go/random go fmt -x github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor go fmt -x github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/cmd go fmt -x github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/common go fmt -x github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/instrumentor go fmt -x github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/assertions go build github.com/antithesishq/antithesis-sdk-go/assert go build github.com/antithesishq/antithesis-sdk-go/lifecycle go build github.com/antithesishq/antithesis-sdk-go/internal go build github.com/antithesishq/antithesis-sdk-go/random go build github.com/antithesishq/antithesis-sdk-go/instrumentation go install tools/antithesis-go-instrumentor/*.go golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/000077500000000000000000000000001516252053700234505ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/000077500000000000000000000000001516252053700307755ustar00rootroot00000000000000antithesis-go-instrumentor.go000066400000000000000000000100071516252053700365700ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentorpackage main import ( _ "embed" "fmt" "os" "strings" "github.com/antithesishq/antithesis-sdk-go/internal" "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/assertions" "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/cmd" "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/common" ) var logWriter *common.LogWriter //go:embed version.txt var versionText string func main() { var err error versionString := strings.TrimSpace(versionText) if strings.Contains(versionText, "%s") { versionString = fmt.Sprintf(versionString, internal.SDK_Version) } //-------------------------------------------------------------------------------- // Parse and validate command arguments // Establish global logging //-------------------------------------------------------------------------------- thisVersion := fmt.Sprintf("v%s", internal.SDK_Version) cmd_args := cmd.ParseArgs(versionString, thisVersion) if cmd_args.ShowVersion { fmt.Println(strings.TrimSpace(versionString)) os.Exit(0) } if cmd_args.InvalidArgs { os.Exit(1) } logWriter = common.GetLogWriter() logWriter.Printf("%s", strings.TrimSpace(versionString)) cmd_args.ShowArguments() //-------------------------------------------------------------------------------- // Verify Directories and Files are all as expected // Prepare instrumentation output directories //-------------------------------------------------------------------------------- var cmd_files *cmd.CommandFiles if cmd_files, err = cmd_args.NewCommandFiles(); err != nil { logWriter.Printf("%s", err.Error()) os.Exit(1) } var source_files []string if source_files, err = cmd_files.GetSourceFiles(); err != nil { logWriter.Printf("%s", err.Error()) os.Exit(1) } //-------------------------------------------------------------------------------- // Setup coverage processor //-------------------------------------------------------------------------------- cI := cmd_files.NewCoverageInstrumentor() source_dir := cmd_files.GetSourceDir() target_dir := cmd_files.GetTargetDir() //-------------------------------------------------------------------------------- // Pass 1: Coverage instrumentation (file-by-file) //-------------------------------------------------------------------------------- cmd_files.ShowDependentModules() for _, file_name := range source_files { if assertions.IsGeneratedFile(file_name) { logWriter.Printf("Skipping %s", file_name) continue } if instrumented_source := cI.InstrumentFile(file_name); instrumented_source != "" { cmd_files.WriteInstrumentedOutput(file_name, instrumented_source, cI) cmd_files.UpdateDependentModules(file_name) } } cmd_files.ShowDependentModules() //-------------------------------------------------------------------------------- // Wrap-up coverage instrumentation and generate notifier module //-------------------------------------------------------------------------------- edge_count := cI.WrapUp() if edge_count > 0 { notifierDir := cmd_files.GetNotifierDirectory() cI.WriteNotifierSource(notifierDir, edge_count) cmd_files.CreateNotifierModule() } //-------------------------------------------------------------------------------- // Pass 2: Assertion catalog generation (go/packages-based, per-binary) //-------------------------------------------------------------------------------- aScanner := assertions.NewAssertionScanner(logWriter.IsVerbose(), source_dir, target_dir) if err := aScanner.ScanAll(); err != nil { logWriter.Printf("Assertion scanning failed: %s", err.Error()) logWriter.Printf("Assertion catalogs will not be generated") } else if aScanner.HasAssertionsDefined() { aScanner.WriteAssertionCatalogs(cmd_args.VersionText) } cmd_files.WrapUp() //-------------------------------------------------------------------------------- // Summarize results in logger //-------------------------------------------------------------------------------- cI.SummarizeWork(len(source_files)) aScanner.SummarizeWork() } golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/000077500000000000000000000000001516252053700331675ustar00rootroot00000000000000assertion_hints.go000066400000000000000000000121411516252053700366520ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertionspackage assertions // A type for writing raw assertions. // GuidanceFnType allows the assertion to provide guidance to // the Antithesis platform when testing in Antithesis. // Regular users of the assert package should not use it. type GuidanceFnType int const ( GuidanceFnMaximize GuidanceFnType = iota // Maximize (left - right) values GuidanceFnMinimize // Minimize (left - right) values GuidanceFnWantAll // Encourages fuzzing explorations where boolean values are true GuidanceFnWantNone // Encourages fuzzing explorations where boolean values are false GuidanceFnExplore ) // -------------------------------------------------------------------------------- // Assertion Hints // -------------------------------------------------------------------------------- type AssertionFuncInfo struct { TargetFunc string AssertType string MustHit bool Condition bool MessageArg int } type AssertionHints map[string]*AssertionFuncInfo func SetupHintMap() AssertionHints { hintMap := make(AssertionHints) hintMap["Always"] = &AssertionFuncInfo{ TargetFunc: "Always", MustHit: true, AssertType: "always", Condition: false, MessageArg: 1, } hintMap["AlwaysOrUnreachable"] = &AssertionFuncInfo{ TargetFunc: "AlwaysOrUnreachable", MustHit: false, AssertType: "always", Condition: false, MessageArg: 1, } hintMap["Sometimes"] = &AssertionFuncInfo{ TargetFunc: "Sometimes", MustHit: true, AssertType: "sometimes", Condition: false, MessageArg: 1, } hintMap["Unreachable"] = &AssertionFuncInfo{ TargetFunc: "Unreachable", MustHit: false, AssertType: "reachability", Condition: false, MessageArg: 0, } hintMap["Reachable"] = &AssertionFuncInfo{ TargetFunc: "Reachable", MustHit: true, AssertType: "reachability", Condition: true, MessageArg: 0, } return hintMap } // -------------------------------------------------------------------------------- // Guidance Hints // -------------------------------------------------------------------------------- type GuidanceFuncInfo struct { AssertionFuncInfo GuidanceFn GuidanceFnType } type GuidanceHints map[string]*GuidanceFuncInfo func SetupGuidanceHintMap() GuidanceHints { hintMap := make(GuidanceHints) hintMap["AlwaysGreaterThan"] = &GuidanceFuncInfo{ AssertionFuncInfo: AssertionFuncInfo{ TargetFunc: "AlwaysGreaterThan", AssertType: "always", MustHit: true, Condition: false, MessageArg: 2, }, GuidanceFn: GuidanceFnMinimize, } hintMap["AlwaysGreaterThanOrEqualTo"] = &GuidanceFuncInfo{ AssertionFuncInfo: AssertionFuncInfo{ TargetFunc: "AlwaysGreaterThanOrEqualTo", AssertType: "always", MustHit: true, Condition: false, MessageArg: 2, }, GuidanceFn: GuidanceFnMinimize, } hintMap["SometimesGreaterThan"] = &GuidanceFuncInfo{ AssertionFuncInfo: AssertionFuncInfo{ TargetFunc: "SometimesGreaterThan", AssertType: "sometimes", MustHit: true, Condition: false, MessageArg: 2, }, GuidanceFn: GuidanceFnMaximize, } hintMap["SometimesGreaterThanOrEqualTo"] = &GuidanceFuncInfo{ AssertionFuncInfo: AssertionFuncInfo{ TargetFunc: "SometimesGreaterThanOrEqualTo", AssertType: "sometimes", MustHit: true, Condition: false, MessageArg: 2, }, GuidanceFn: GuidanceFnMaximize, } hintMap["AlwaysLessThan"] = &GuidanceFuncInfo{ AssertionFuncInfo: AssertionFuncInfo{ TargetFunc: "AlwaysLessThan", AssertType: "always", MustHit: true, Condition: false, MessageArg: 2, }, GuidanceFn: GuidanceFnMaximize, } hintMap["AlwaysLessThanOrEqualTo"] = &GuidanceFuncInfo{ AssertionFuncInfo: AssertionFuncInfo{ TargetFunc: "AlwaysLessThanOrEqualTo", AssertType: "always", MustHit: true, Condition: false, MessageArg: 2, }, GuidanceFn: GuidanceFnMaximize, } hintMap["SometimesLessThan"] = &GuidanceFuncInfo{ AssertionFuncInfo: AssertionFuncInfo{ TargetFunc: "SometimesLessThan", AssertType: "sometimes", MustHit: true, Condition: false, MessageArg: 2, }, GuidanceFn: GuidanceFnMinimize, } hintMap["SometimesLessThanOrEqualTo"] = &GuidanceFuncInfo{ AssertionFuncInfo: AssertionFuncInfo{ TargetFunc: "SometimesLessThanOrEqualTo", AssertType: "sometimes", MustHit: true, Condition: false, MessageArg: 2, }, GuidanceFn: GuidanceFnMinimize, } hintMap["AlwaysSome"] = &GuidanceFuncInfo{ AssertionFuncInfo: AssertionFuncInfo{ TargetFunc: "AlwaysSome", AssertType: "always", MustHit: true, Condition: false, MessageArg: 1, }, GuidanceFn: GuidanceFnWantNone, } hintMap["SometimesAll"] = &GuidanceFuncInfo{ AssertionFuncInfo: AssertionFuncInfo{ TargetFunc: "SometimesAll", AssertType: "sometimes", MustHit: true, Condition: false, MessageArg: 1, }, GuidanceFn: GuidanceFnWantAll, } return hintMap } func (m AssertionHints) HintsForName(name string) *AssertionFuncInfo { if v, ok := m[name]; ok { return v } return nil } func (m GuidanceHints) GuidanceHintsForName(name string) *GuidanceFuncInfo { if v, ok := m[name]; ok { return v } return nil } assertion_scanner.go000066400000000000000000000367651516252053700372000ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertionspackage assertions import ( "fmt" "go/ast" "go/types" "path/filepath" "strconv" "strings" "time" "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/common" "golang.org/x/tools/go/packages" ) type AntExpect struct { *AssertionFuncInfo Assertion string Message string Classname string Funcname string Receiver string Filename string Line int } type AntGuidance struct { *GuidanceFuncInfo Assertion string Message string Classname string Funcname string Receiver string Filename string Line int } // AssertionScanner scans Go packages using go/packages to find assertion // and guidance calls, producing per-binary catalog data. type AssertionScanner struct { assertionHintMap AssertionHints guidanceHintMap GuidanceHints logWriter *common.LogWriter baseInputDir string baseTargetDir string verbose bool filesCataloged int // Results: per-binary catalogs binaries []*binaryCatalog } // binaryCatalog represents a discovered main package and its reachable assertions. type binaryCatalog struct { dir string // absolute path of the main package directory relDir string // directory relative to the module root expects []*AntExpect guidance []*AntGuidance } func (bc *binaryCatalog) hasAssertions() bool { return len(bc.expects) > 0 || len(bc.guidance) > 0 } // packageResult caches the scan result for a single package. type packageResult struct { expects []*AntExpect guidance []*AntGuidance } // filter Guidance to just numeric func numericGuidance(guidance []*AntGuidance) []*AntGuidance { numeric_guidance := []*AntGuidance{} for _, aG := range guidance { gp := aG.GuidanceFn if gp == GuidanceFnMaximize || gp == GuidanceFnMinimize { numeric_guidance = append(numeric_guidance, aG) } } return numeric_guidance } // filter Guidance to just boolean func booleanGuidance(guidance []*AntGuidance) []*AntGuidance { boolean_guidance := []*AntGuidance{} for _, aG := range guidance { gp := aG.GuidanceFn if gp == GuidanceFnWantAll || gp == GuidanceFnWantNone { boolean_guidance = append(boolean_guidance, aG) } } return boolean_guidance } func NewAssertionScanner(verbose bool, sourceDir string, targetDir string) *AssertionScanner { logWriter := common.GetLogWriter() aScanner := AssertionScanner{ verbose: verbose, baseInputDir: sourceDir, baseTargetDir: targetDir, assertionHintMap: SetupHintMap(), guidanceHintMap: SetupGuidanceHintMap(), filesCataloged: 0, logWriter: logWriter, } return &aScanner } func (aScanner *AssertionScanner) GetLogger() *common.LogWriter { return aScanner.logWriter } // HasAssertionsDefined returns true if any binary has assertions. func (aScanner *AssertionScanner) HasAssertionsDefined() bool { for _, bc := range aScanner.binaries { if bc.hasAssertions() { return true } } return false } // WriteAssertionCatalogs writes one catalog per binary into the appropriate // directory under baseTargetDir. For assert-only mode, baseTargetDir == // baseInputDir. func (aScanner *AssertionScanner) WriteAssertionCatalogs(versionText string) { now := time.Now() createDate := now.Format("Mon Jan 2 15:04:05 MST 2006") for _, bc := range aScanner.binaries { if !bc.hasAssertions() { continue } expects := bc.expects numericGuidance := numericGuidance(bc.guidance) booleanGuidance := booleanGuidance(bc.guidance) genInfo := GenInfo{ ExpectedVals: expects, NumericGuidanceVals: numericGuidance, BooleanGuidanceVals: booleanGuidance, AssertPackageName: common.AssertPackageName(), VersionText: versionText, CreateDate: createDate, HasAssertions: len(expects) > 0, HasNumericGuidance: len(numericGuidance) > 0, HasBooleanGuidance: len(booleanGuidance) > 0, ConstMap: getConstMap(expects), logWriter: common.GetLogWriter(), } outputDir := filepath.Join(aScanner.baseTargetDir, bc.relDir) GenerateAssertionsCatalog(outputDir, &genInfo) } } func (aScanner *AssertionScanner) SummarizeWork() { numCataloged := aScanner.filesCataloged aScanner.logWriter.Printf("%d '.go' %s cataloged", numCataloged, common.Pluralize(numCataloged, "file")) numBinaries := len(aScanner.binaries) catalogsWritten := 0 for _, bc := range aScanner.binaries { if bc.hasAssertions() { catalogsWritten++ } } aScanner.logWriter.Printf("%d %s discovered, %d %s written", numBinaries, common.Pluralize(numBinaries, "binary"), catalogsWritten, common.Pluralize(catalogsWritten, "catalog")) } // ScanAll loads all packages under baseInputDir using go/packages, identifies // main packages, computes per-binary reachability, and scans for assertions. func (aScanner *AssertionScanner) ScanAll() error { cfg := &packages.Config{ Mode: packages.NeedName | packages.NeedFiles | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps | packages.NeedModule, Dir: aScanner.baseInputDir, } if aScanner.logWriter.VerboseLevel(1) { aScanner.logWriter.Printf("Loading packages from %s", aScanner.baseInputDir) } pkgs, err := packages.Load(cfg, "./...") if err != nil { return fmt.Errorf("go/packages load failed: %w", err) } // Check for package errors var loadErrors []string packages.Visit(pkgs, nil, func(pkg *packages.Package) { for _, e := range pkg.Errors { loadErrors = append(loadErrors, fmt.Sprintf("%s: %s", pkg.PkgPath, e.Msg)) } }) if len(loadErrors) > 0 { return fmt.Errorf("package load errors:\n %s", strings.Join(loadErrors, "\n ")) } // Identify main packages var mainPkgs []*packages.Package for _, pkg := range pkgs { if pkg.Name == "main" { mainPkgs = append(mainPkgs, pkg) } } if len(mainPkgs) == 0 { aScanner.logWriter.Printf("Warning: no main packages found in %s", aScanner.baseInputDir) return nil } if aScanner.logWriter.VerboseLevel(1) { aScanner.logWriter.Printf("Found %d main %s", len(mainPkgs), common.Pluralize(len(mainPkgs), "package")) for _, pkg := range mainPkgs { aScanner.logWriter.Printf(" main: %s", pkg.PkgPath) } } // For each main package, compute reachable packages and scan. // Cache per-package results so shared dependencies are only scanned once. assertPkgPath := common.AssertPackageName() pkgCache := make(map[string]*packageResult) for _, mainPkg := range mainPkgs { aScanner.logWriter.Printf("Cataloging %s", mainPkg.PkgPath) reachable := collectReachable(mainPkg) bc := &binaryCatalog{ dir: mainPkg.Dir, relDir: aScanner.relativeDir(mainPkg.Dir), } for _, pkg := range reachable { pr, ok := pkgCache[pkg.ID] if !ok { pr = &packageResult{} if aScanner.logWriter.VerboseLevel(1) { aScanner.logWriter.Printf("Scanning %s", pkg.PkgPath) } for _, file := range pkg.Syntax { aScanner.filesCataloged++ funcName := "" receiver := "" ast.Inspect(file, func(x ast.Node) bool { funcName, receiver = aScanner.node_inspector(x, pkg, assertPkgPath, pr, funcName, receiver) return true }) } pkgCache[pkg.ID] = pr } bc.expects = append(bc.expects, pr.expects...) bc.guidance = append(bc.guidance, pr.guidance...) } aScanner.binaries = append(aScanner.binaries, bc) if aScanner.logWriter.VerboseLevel(1) { aScanner.logWriter.Printf("Binary %s: %d assertions, %d guidance entries", bc.relDir, len(bc.expects), len(bc.guidance)) } } return nil } func (aScanner *AssertionScanner) node_inspector(x ast.Node, pkg *packages.Package, assertPkgPath string, data *packageResult, funcName string, receiver string) (string, string) { var func_decl *ast.FuncDecl var call_expr *ast.CallExpr var ok bool // Track current funcName and receiver (type) if func_decl, ok = x.(*ast.FuncDecl); ok { funcName = common.NAME_NOT_AVAILABLE if func_ident := func_decl.Name; func_ident != nil { funcName = func_ident.Name } receiver = "" if recv := func_decl.Recv; recv != nil { if num_fields := recv.NumFields(); num_fields > 0 { if field_list := recv.List; field_list != nil { if recv_type := field_list[0].Type; recv_type != nil { receiver = types.ExprString(recv_type) } } } } } if call_expr, ok = x.(*ast.CallExpr); ok { var sel_expr *ast.SelectorExpr if sel_expr, ok = call_expr.Fun.(*ast.SelectorExpr); ok { // Use type info to resolve the called function // in the SDK assert package obj := pkg.TypesInfo.Uses[sel_expr.Sel] if obj == nil { return funcName, receiver } fn, ok := obj.(*types.Func) if !ok { return funcName, receiver } if fn.Pkg() == nil || fn.Pkg().Path() != assertPkgPath { return funcName, receiver } target_func := fn.Name() full_position := pkg.Fset.Position(sel_expr.Pos()) relative_file_path := aScanner.relativeDir(full_position.Filename) packageName := pkg.PkgPath call_args := call_expr.Args if func_hints := aScanner.assertionHintMap.HintsForName(target_func); func_hints != nil { test_name := arg_at_index(call_args, func_hints.MessageArg) if test_name == common.NAME_NOT_AVAILABLE { generated_msg := fmt.Sprintf("%s[%d]", relative_file_path, full_position.Line) test_name = fmt.Sprintf("Message from %s", strconv.Quote(generated_msg)) } expect := AntExpect{ Assertion: target_func, Message: test_name, Classname: packageName, Funcname: funcName, Receiver: receiver, Filename: relative_file_path, Line: full_position.Line, AssertionFuncInfo: func_hints, } data.expects = append(data.expects, &expect) } if guidance_func_hints := aScanner.guidanceHintMap.GuidanceHintsForName(target_func); guidance_func_hints != nil { test_name := arg_at_index(call_args, guidance_func_hints.MessageArg) if test_name == common.NAME_NOT_AVAILABLE { generated_msg := fmt.Sprintf("%s[%d]", relative_file_path, full_position.Line) test_name = fmt.Sprintf("Message from %s", strconv.Quote(generated_msg)) } // The registration for the Guidance function itself guidance_expect := AntGuidance{ Assertion: target_func, Message: test_name, Classname: packageName, Funcname: funcName, Receiver: receiver, Filename: relative_file_path, Line: full_position.Line, GuidanceFuncInfo: guidance_func_hints, } data.guidance = append(data.guidance, &guidance_expect) // The Related Assertion derived from target_func("AlwaysGreaterThan") => derived_target_func("Always") expect := AntExpect{ Assertion: target_func_from_guidance(target_func), Message: test_name, Classname: packageName, Funcname: funcName, Receiver: receiver, Filename: relative_file_path, Line: full_position.Line, // NOTE: AssertionFuncInfo.TargetFunc is a guidance func name // and AssertionFuncInfo.MessageArg refers to a Guidance Function argument number. // // GenerateAssertionsCatalog() does not use either of TargetFunc or MessageArg // attributes of AssertionFuncInfo, so it is safe to pass the AssertionFuncInfo // from the guidance func here. AssertionFuncInfo: &guidance_func_hints.AssertionFuncInfo, } data.expects = append(data.expects, &expect) } // assertionHint } } return funcName, receiver } func (aScanner *AssertionScanner) relativeDir(dir string) string { rel, err := filepath.Rel(aScanner.baseInputDir, dir) if err != nil { return dir } return rel } // collectReachable returns all packages transitively reachable from pkg // that belong to the same module. Packages from the standard library or // other modules are skipped. func collectReachable(pkg *packages.Package) []*packages.Package { if pkg.Module == nil { return nil } modulePath := pkg.Module.Path visited := make(map[string]bool) var result []*packages.Package var walk func(p *packages.Package) walk = func(p *packages.Package) { if visited[p.ID] { return } visited[p.ID] = true if p.Module == nil || p.Module.Path != modulePath { return } result = append(result, p) for _, imp := range p.Imports { walk(imp) } } walk(pkg) return result } func target_func_from_guidance(guidance_func string) string { target_func := "" if strings.HasPrefix(guidance_func, "Always") { target_func = "Always" } else if strings.HasPrefix(guidance_func, "Sometimes") { target_func = "Sometimes" } return target_func } func arg_at_index(args []ast.Expr, idx int) string { if args == nil || idx < 0 || len(args) <= idx { return common.NAME_NOT_AVAILABLE } arg := args[idx] var basic_lit *ast.BasicLit var basic_lit2 *ast.BasicLit var ident *ast.Ident var value_spec *ast.ValueSpec var ok bool // A string literal was provided - nice if basic_lit, ok = arg.(*ast.BasicLit); ok { text, _ := strconv.Unquote(basic_lit.Value) return text } // Not so nice. // A reference to a const or a var or an indexed value was provided // // Dig in and see if is resolvable at compile-time // When a const is declared in another file, it might not be available here if ident, ok = arg.(*ast.Ident); ok { if ident.Obj == nil || ident.Obj.Decl == nil { return ident.String() } if value_spec, ok = ident.Obj.Decl.(*ast.ValueSpec); ok { values := value_spec.Values if len(values) > 0 { this_value := values[0] if basic_lit2, ok = this_value.(*ast.BasicLit); ok { const_text, _ := strconv.Unquote(basic_lit2.Value) return const_text } } } } return common.NAME_NOT_AVAILABLE } const ( Cond_false = iota Cond_true Was_hit Not_hit Must_be_hit Optionally_hit Universal_test Existential_test Reachability_test Num_conditions ) // -------------------------------------------------------------------------------- // The 'ConstMap' is used by GenerateAssertionsCatalog to define the // go constants referenced in the registration statements // that are generated. // // This will prevent go build warnings/errors related to defining // a const that is not actually used anywhere. // // Example, if none of the generated registrations use an AssertType // "reachability", then the corresponding go const should not be // output to the generated Assertions Catalog '.go' file. // -------------------------------------------------------------------------------- func getConstMap(expects []*AntExpect) map[string]bool { cond_tracker := make([]bool, Num_conditions) if len(expects) > 0 { cond_tracker[Not_hit] = true } for _, an_expect := range expects { pAFI := an_expect.AssertionFuncInfo if pAFI.MustHit { cond_tracker[Must_be_hit] = true } else { cond_tracker[Optionally_hit] = true } if pAFI.Condition { cond_tracker[Cond_true] = true } else { cond_tracker[Cond_false] = true } if pAFI.AssertType == "always" { cond_tracker[Universal_test] = true } if pAFI.AssertType == "sometimes" { cond_tracker[Existential_test] = true } if pAFI.AssertType == "reachability" { cond_tracker[Reachability_test] = true } } const_map := make(map[string]bool) const_map["condFalse"] = cond_tracker[Cond_false] const_map["condTrue"] = cond_tracker[Cond_true] const_map["wasHit"] = cond_tracker[Was_hit] const_map["notHit"] = cond_tracker[Not_hit] const_map["mustBeHit"] = cond_tracker[Must_be_hit] const_map["optionallyHit"] = cond_tracker[Optionally_hit] const_map["universalTest"] = cond_tracker[Universal_test] const_map["existentialTest"] = cond_tracker[Existential_test] const_map["reachabilityTest"] = cond_tracker[Reachability_test] return const_map } assertion_scanner_test.go000066400000000000000000000140131516252053700402150ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertionspackage assertions import ( "os" "os/exec" "path/filepath" "sort" "testing" qt "github.com/go-quicktest/qt" "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/common" ) func init() { // Ensure the global logWriter is initialized for tests. common.NewLogWriter("", 0) } func absTestdata(fixture string) string { p, err := filepath.Abs(filepath.Join("testdata", fixture)) if err != nil { panic(err) } return p } func TestSingleMain(t *testing.T) { dir := absTestdata("single_main") scanner := NewAssertionScanner(false, dir, dir) err := scanner.ScanAll() qt.Assert(t, qt.IsNil(err)) bins := scanner.binaries qt.Assert(t, qt.HasLen(bins, 1)) bc := bins[0] // single_main has 3 assertion calls: Always, Sometimes, Reachable qt.Check(t, qt.HasLen(bc.expects, 3)) msgs := collectMessages(bc.expects) qt.Check(t, qt.SliceContains(msgs, "always true")) qt.Check(t, qt.SliceContains(msgs, "sometimes true")) qt.Check(t, qt.SliceContains(msgs, "reached main")) } func TestMultiMain(t *testing.T) { dir := absTestdata("multi_main") scanner := NewAssertionScanner(false, dir, dir) err := scanner.ScanAll() qt.Assert(t, qt.IsNil(err)) bins := scanner.binaries qt.Assert(t, qt.HasLen(bins, 2)) // Sort by RelDir for deterministic assertions sort.Slice(bins, func(i, j int) bool { return bins[i].relDir < bins[j].relDir }) // cmd/a imports shared + aonly: 1 (a main) + 1 (shared) + 1 (aonly) = 3 assertions binA := bins[0] qt.Check(t, qt.Equals(binA.relDir, filepath.Join("cmd", "a"))) msgsA := collectMessages(binA.expects) qt.Check(t, qt.SliceContains(msgsA, "a main assertion")) qt.Check(t, qt.SliceContains(msgsA, "shared assertion")) qt.Check(t, qt.SliceContains(msgsA, "aonly assertion")) qt.Check(t, qt.HasLen(binA.expects, 3)) // cmd/b imports shared only: 1 (b main) + 1 (shared) = 2 assertions binB := bins[1] qt.Check(t, qt.Equals(binB.relDir, filepath.Join("cmd", "b"))) msgsB := collectMessages(binB.expects) qt.Check(t, qt.SliceContains(msgsB, "b main assertion")) qt.Check(t, qt.SliceContains(msgsB, "shared assertion")) qt.Check(t, qt.HasLen(binB.expects, 2)) // Cross-contamination check qt.Check(t, qt.Not(qt.SliceContains(msgsB, "aonly assertion"))) qt.Check(t, qt.Not(qt.SliceContains(msgsB, "a main assertion"))) qt.Check(t, qt.Not(qt.SliceContains(msgsA, "b main assertion"))) } func TestAliasedImport(t *testing.T) { dir := absTestdata("aliased_import") scanner := NewAssertionScanner(false, dir, dir) err := scanner.ScanAll() qt.Assert(t, qt.IsNil(err)) bins := scanner.binaries qt.Assert(t, qt.HasLen(bins, 1)) bc := bins[0] qt.Check(t, qt.HasLen(bc.expects, 2)) msgs := collectMessages(bc.expects) qt.Check(t, qt.SliceContains(msgs, "aliased always")) qt.Check(t, qt.SliceContains(msgs, "aliased unreachable")) } func TestNoMain(t *testing.T) { dir := absTestdata("no_main") scanner := NewAssertionScanner(false, dir, dir) err := scanner.ScanAll() qt.Assert(t, qt.IsNil(err)) bins := scanner.binaries qt.Check(t, qt.HasLen(bins, 0)) } func TestNoAssertions(t *testing.T) { dir := absTestdata("no_assertions") scanner := NewAssertionScanner(false, dir, dir) err := scanner.ScanAll() qt.Assert(t, qt.IsNil(err)) bins := scanner.binaries qt.Assert(t, qt.HasLen(bins, 1)) bc := bins[0] qt.Check(t, qt.HasLen(bc.expects, 0)) qt.Check(t, qt.HasLen(bc.guidance, 0)) // HasAssertionsDefined should be false qt.Check(t, qt.IsFalse(scanner.HasAssertionsDefined())) } // TestCatalogStability verifies that scanning a package which already // contains a generated catalog produces the same results as the first // scan. The catalog's assert.AssertRaw calls must not be picked up as // additional assertions. func TestCatalogStability(t *testing.T) { // Locate the repo root so we can set up a replace directive. repoRoot, err := filepath.Abs(filepath.Join("..", "..", "..")) qt.Assert(t, qt.IsNil(err)) // The SDK's internal package uses cgo. Disable it so that the test // does not require a C compiler. t.Setenv("CGO_ENABLED", "0") // Copy the single_main fixture into a temp directory with its own module. tmpDir := t.TempDir() src, err := os.ReadFile(absTestdata("single_main/main.go")) qt.Assert(t, qt.IsNil(err)) err = os.WriteFile(filepath.Join(tmpDir, "main.go"), src, 0644) qt.Assert(t, qt.IsNil(err)) sdkModule := "github.com/antithesishq/antithesis-sdk-go" for _, cmd := range [][]string{ {"go", "mod", "init", "example.com/stability-test"}, {"go", "mod", "edit", "-require", sdkModule + "@v0.0.0"}, {"go", "mod", "edit", "-replace", sdkModule + "=" + repoRoot}, {"go", "mod", "tidy"}, {"go", "mod", "vendor"}, } { c := exec.Command(cmd[0], cmd[1:]...) c.Dir = tmpDir out, err := c.CombinedOutput() qt.Assert(t, qt.IsNil(err), qt.Commentf("%s failed:\n%s", cmd, out)) } // First scan. scanner1 := NewAssertionScanner(false, tmpDir, tmpDir) err = scanner1.ScanAll() qt.Assert(t, qt.IsNil(err)) qt.Assert(t, qt.HasLen(scanner1.binaries, 1)) firstMsgs := collectMessages(scanner1.binaries[0].expects) firstGuidanceCount := len(scanner1.binaries[0].guidance) // Write the catalog into the temp directory (same as source). scanner1.WriteAssertionCatalogs("stability test") // Verify the catalog file was created. catalogPath := filepath.Join(tmpDir, common.GENERATED_CATALOG_FILE) _, err = os.Stat(catalogPath) qt.Assert(t, qt.IsNil(err)) // Second scan — the catalog is now present alongside main.go. scanner2 := NewAssertionScanner(false, tmpDir, tmpDir) err = scanner2.ScanAll() qt.Assert(t, qt.IsNil(err)) qt.Assert(t, qt.HasLen(scanner2.binaries, 1)) secondMsgs := collectMessages(scanner2.binaries[0].expects) secondGuidanceCount := len(scanner2.binaries[0].guidance) // The assertion counts must be identical. sort.Strings(firstMsgs) sort.Strings(secondMsgs) qt.Check(t, qt.DeepEquals(firstMsgs, secondMsgs)) qt.Check(t, qt.Equals(firstGuidanceCount, secondGuidanceCount)) } func collectMessages(expects []*AntExpect) []string { msgs := make([]string, len(expects)) for i, e := range expects { msgs[i] = e.Message } return msgs } catalog_output.go000066400000000000000000000160231516252053700364730ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertionspackage assertions import ( "fmt" "io" "os" "path/filepath" "strconv" "text/template" "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/common" ) type GenInfo struct { ConstMap map[string]bool logWriter *common.LogWriter AssertPackageName string VersionText string CreateDate string ExpectedVals []*AntExpect NumericGuidanceVals []*AntGuidance BooleanGuidanceVals []*AntGuidance HasAssertions bool HasNumericGuidance bool HasBooleanGuidance bool } func IsGeneratedFile(file_name string) bool { base_name := filepath.Base(file_name) return base_name == common.GENERATED_CATALOG_FILE } // expectOutputFile creates or opens the catalog output file in the given // directory. The file is named antithesis_catalog.go. func expectOutputFile(outputDir string, logWriter *common.LogWriter) (*os.File, error) { output_file_name := filepath.Join(outputDir, common.GENERATED_CATALOG_FILE) var file *os.File var err error if file, err = os.OpenFile(output_file_name, os.O_RDWR|os.O_CREATE, 0644); err != nil { file = nil } if file != nil { if err = file.Truncate(0); err != nil { file = nil } } if err == nil { logWriter.Printf("Assertion Catalog: %q\n", output_file_name) } else { logWriter.Printf("Unable to generate Assertion Catalog: %q\n", output_file_name) } return file, err } func assertionNameRepr(s string) string { if s == "Reachable" || s == "Unreachable" { return fmt.Sprintf("%s(message, details)", s) } return fmt.Sprintf("%s(cond, message, details)", s) } func numericGuidanceNameRepr(s string) string { return fmt.Sprintf("%s(left, right, message, details)", s) } func booleanGuidanceNameRepr(s string) string { return fmt.Sprintf("%s(pairs, message, details)", s) } func hitRepr(b bool) string { if !b { return "notHit" } return "wasHit" } func condRepr(b bool) string { if b { return "condTrue" } return "condFalse" } func mustHitRepr(b bool) string { if b { return "mustBeHit" } return "optionallyHit" } func textRepr(s string) string { return strconv.Quote(s) } func guidanceFnRepr(n GuidanceFnType) string { gp := "" switch n { case GuidanceFnMaximize: gp = "maximize" case GuidanceFnMinimize: gp = "minimize" case GuidanceFnExplore: gp = "explore" case GuidanceFnWantAll: gp = "all" case GuidanceFnWantNone: gp = "none" } return textRepr(gp) } func assertTypeRepr(s string) string { reprText := "reachabilityTest" switch s { case "always": reprText = "universalTest" case "sometimes": reprText = "existentialTest" case "reachability": reprText = "reachabilityTest" } return reprText } func usesConst(cm map[string]bool, c string) bool { return cm[c] } // GenerateAssertionsCatalog renders the assertion catalog into the given // output directory as antithesis_catalog.go. func GenerateAssertionsCatalog(outputDir string, genInfo *GenInfo) { var tmpl *template.Template var err error tmpl = template.New("expector") tmpl = tmpl.Funcs(template.FuncMap{ "hitRepr": hitRepr, "condRepr": condRepr, "mustHitRepr": mustHitRepr, "assertTypeRepr": assertTypeRepr, "assertionNameRepr": assertionNameRepr, "usesConst": usesConst, "textRepr": textRepr, "numericGuidanceNameRepr": numericGuidanceNameRepr, "booleanGuidanceNameRepr": booleanGuidanceNameRepr, "guidanceFnRepr": guidanceFnRepr, }) all_template_text := getExpectorText() + getNumericGuidanceText() + getBooleanGuidanceText() if tmpl, err = tmpl.Parse(all_template_text); err != nil { panic(err) } var outFile io.Writer if outFile, err = expectOutputFile(outputDir, genInfo.logWriter); err != nil { panic(err) } if err = tmpl.Execute(outFile, genInfo); err != nil { panic(err) } } func getExpectorText() string { const text = `// Code generated by antithesis-go-instrumentor; DO NOT EDIT. package main // ---------------------------------------------------- // {{.VersionText}} // // Assertion Catalog // // Generated on {{.CreateDate}} // ---------------------------------------------------- {{if .HasAssertions -}}import "{{.AssertPackageName}}"{{- end}} {{if .HasAssertions -}} func init() { {{if usesConst .ConstMap "condFalse"}} const condFalse = false{{- end}} {{if usesConst .ConstMap "condTrue"}} const condTrue = true {{- end}} const wasHit = true {{if usesConst .ConstMap "notHit"}} const notHit = !wasHit {{- end}} {{if usesConst .ConstMap "mustBeHit"}} const mustBeHit = true {{- end}} {{if usesConst .ConstMap "optionallyHit"}} const optionallyHit = false {{- end}} {{if usesConst .ConstMap "universalTest"}} const universalTest = "always" {{- end}} {{if usesConst .ConstMap "existentialTest"}} const existentialTest = "sometimes" {{- end}} {{if usesConst .ConstMap "reachabilityTest"}} const reachabilityTest = "reachability" {{- end}} var noDetails map[string]any = nil {{- range .ExpectedVals }} {{- $cond := condRepr .AssertionFuncInfo.Condition -}} {{- $didHit := hitRepr false -}} {{- $mustHit := mustHitRepr .AssertionFuncInfo.MustHit -}} {{- $assertionName := assertionNameRepr .Assertion -}} {{- $assertType := assertTypeRepr .AssertionFuncInfo.AssertType -}} {{- $message := textRepr .Message -}} {{- $classname := textRepr .Classname -}} {{- $funcname := textRepr .Funcname -}} {{- $filename := textRepr .Filename -}} {{- $displayname := textRepr .Assertion}} // {{$assertionName}} assert.AssertRaw({{$cond}}, {{$message}}, noDetails, {{$classname}}, {{$funcname}}, {{$filename}}, {{.Line}}, {{$didHit}}, {{$mustHit}}, {{$assertType}}, {{$displayname}}, {{$message}}) {{- end}} } {{- end}} ` return text } func getNumericGuidanceText() string { const text = ` {{if .HasNumericGuidance -}} func init() { const notHit = false const left = 0 const right = 0 {{- range .NumericGuidanceVals }} {{- $guidanceName := numericGuidanceNameRepr .Assertion -}} {{- $message := textRepr .Message -}} {{- $classname := textRepr .Classname -}} {{- $funcname := textRepr .Funcname -}} {{- $filename := textRepr .Filename -}} {{- $guidanceFn := guidanceFnRepr .GuidanceFuncInfo.GuidanceFn}} // {{$guidanceName}} assert.NumericGuidanceRaw(left, right, {{$message}}, {{$message}}, {{$classname}}, {{$funcname}}, {{$filename}}, {{.Line}}, {{$guidanceFn}}, notHit) {{- end}} } {{- end}} ` return text } func getBooleanGuidanceText() string { const text = ` {{if .HasBooleanGuidance -}} func init() { const notHit = false var named_bools = []assert.NamedBool{} {{- range .BooleanGuidanceVals }} {{- $guidanceName := booleanGuidanceNameRepr .Assertion -}} {{- $message := textRepr .Message -}} {{- $classname := textRepr .Classname -}} {{- $funcname := textRepr .Funcname -}} {{- $filename := textRepr .Filename -}} {{- $guidanceFn := guidanceFnRepr .GuidanceFuncInfo.GuidanceFn}} // {{$guidanceName}} assert.BooleanGuidanceRaw(named_bools, {{$message}}, {{$message}}, {{$classname}}, {{$funcname}}, {{$filename}}, {{.Line}}, {{$guidanceFn}}, notHit) {{- end}} } {{- end}} ` return text } catalog_output_test.go000066400000000000000000000063651516252053700375420ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertionspackage assertions import ( "os" "path/filepath" "testing" qt "github.com/go-quicktest/qt" "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/common" ) func TestCatalogContents(t *testing.T) { outputDir := t.TempDir() expects := []*AntExpect{ { AssertionFuncInfo: &AssertionFuncInfo{ TargetFunc: "Always", AssertType: "always", MustHit: true, Condition: false, MessageArg: 1, }, Assertion: "Always", Message: "test always", Classname: "example.com/mymod", Funcname: "main", Filename: "main.go", Line: 10, }, { AssertionFuncInfo: &AssertionFuncInfo{ TargetFunc: "Reachable", AssertType: "reachability", MustHit: true, Condition: true, MessageArg: 0, }, Assertion: "Reachable", Message: "test reachable", Classname: "example.com/mymod", Funcname: "init", Filename: "main.go", Line: 5, }, } genInfo := GenInfo{ ExpectedVals: expects, NumericGuidanceVals: nil, BooleanGuidanceVals: nil, AssertPackageName: common.AssertPackageName(), VersionText: "test version", CreateDate: "Mon Jan 1 00:00:00 UTC 2025", HasAssertions: true, HasNumericGuidance: false, HasBooleanGuidance: false, ConstMap: getConstMap(expects), logWriter: common.GetLogWriter(), } GenerateAssertionsCatalog(outputDir, &genInfo) // Verify the file was created outputPath := filepath.Join(outputDir, common.GENERATED_CATALOG_FILE) content, err := os.ReadFile(outputPath) qt.Assert(t, qt.IsNil(err)) text := string(content) qt.Check(t, qt.StringContains(text, "package main")) qt.Check(t, qt.StringContains(text, `import "github.com/antithesishq/antithesis-sdk-go/assert"`)) qt.Check(t, qt.StringContains(text, `assert.AssertRaw(`)) qt.Check(t, qt.StringContains(text, `"test always"`)) qt.Check(t, qt.StringContains(text, `"test reachable"`)) qt.Check(t, qt.StringContains(text, "test version")) } func TestCatalogNumericGuidance(t *testing.T) { outputDir := t.TempDir() numericGuidance := []*AntGuidance{ { GuidanceFuncInfo: &GuidanceFuncInfo{ AssertionFuncInfo: AssertionFuncInfo{ TargetFunc: "AlwaysGreaterThan", AssertType: "always", MustHit: true, Condition: false, MessageArg: 2, }, GuidanceFn: GuidanceFnMinimize, }, Assertion: "AlwaysGreaterThan", Message: "x > y", Classname: "example.com/mymod", Funcname: "compute", Filename: "compute.go", Line: 42, }, } genInfo := GenInfo{ ExpectedVals: nil, NumericGuidanceVals: numericGuidance, BooleanGuidanceVals: nil, AssertPackageName: common.AssertPackageName(), VersionText: "test", CreateDate: "now", HasAssertions: false, HasNumericGuidance: true, HasBooleanGuidance: false, ConstMap: make(map[string]bool), logWriter: common.GetLogWriter(), } GenerateAssertionsCatalog(outputDir, &genInfo) outputPath := filepath.Join(outputDir, common.GENERATED_CATALOG_FILE) content, err := os.ReadFile(outputPath) qt.Assert(t, qt.IsNil(err)) text := string(content) qt.Check(t, qt.StringContains(text, "assert.NumericGuidanceRaw(")) qt.Check(t, qt.StringContains(text, `"x > y"`)) } testdata/000077500000000000000000000000001516252053700347215ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertionsaliased_import/000077500000000000000000000000001516252053700377155ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdatamain.go000066400000000000000000000002621516252053700411700ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdata/aliased_importpackage main import ( a "github.com/antithesishq/antithesis-sdk-go/assert" ) func main() { a.Always(true, "aliased always", nil) a.Unreachable("aliased unreachable", nil) } multi_main/000077500000000000000000000000001516252053700370575ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdatacmd/000077500000000000000000000000001516252053700376225ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdata/multi_maina/000077500000000000000000000000001516252053700400425ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdata/multi_main/cmdmain.go000066400000000000000000000005771516252053700413260ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdata/multi_main/cmd/apackage main import ( "github.com/antithesishq/antithesis-sdk-go/assert" _ "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/assertions/testdata/multi_main/pkg/aonly" _ "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/assertions/testdata/multi_main/pkg/shared" ) func main() { assert.Always(true, "a main assertion", nil) } b/000077500000000000000000000000001516252053700400435ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdata/multi_main/cmdmain.go000066400000000000000000000004061516252053700413160ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdata/multi_main/cmd/bpackage main import ( "github.com/antithesishq/antithesis-sdk-go/assert" _ "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/assertions/testdata/multi_main/pkg/shared" ) func main() { assert.Always(true, "b main assertion", nil) } pkg/000077500000000000000000000000001516252053700376405ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdata/multi_mainaonly/000077500000000000000000000000001516252053700407625ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdata/multi_main/pkglib.go000066400000000000000000000002171516252053700420570ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdata/multi_main/pkg/aonlypackage aonly import ( "github.com/antithesishq/antithesis-sdk-go/assert" ) func Init() { assert.Sometimes(true, "aonly assertion", nil) } shared/000077500000000000000000000000001516252053700411065ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdata/multi_main/pkglib.go000066400000000000000000000002161516252053700422020ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdata/multi_main/pkg/sharedpackage shared import ( "github.com/antithesishq/antithesis-sdk-go/assert" ) func Init() { assert.Always(true, "shared assertion", nil) } no_assertions/000077500000000000000000000000001516252053700376075ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdatamain.go000066400000000000000000000001171516252053700410610ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdata/no_assertionspackage main import "fmt" func main() { fmt.Println("no assertions here") } no_main/000077500000000000000000000000001516252053700363415ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdatalib.go000066400000000000000000000002151516252053700374340ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdata/no_mainpackage lib import ( "github.com/antithesishq/antithesis-sdk-go/assert" ) func Check() { assert.Always(true, "library assertion", nil) } single_main/000077500000000000000000000000001516252053700372065ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdatamain.go000066400000000000000000000003351516252053700404620ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/assertions/testdata/single_mainpackage main import ( "github.com/antithesishq/antithesis-sdk-go/assert" ) func main() { assert.Always(true, "always true", nil) assert.Sometimes(true, "sometimes true", nil) assert.Reachable("reached main", nil) } golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/cmd/000077500000000000000000000000001516252053700315405ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/cmd/cmd_args.go000066400000000000000000000202131516252053700336440ustar00rootroot00000000000000package cmd import ( "flag" "fmt" "os" "os/exec" "path/filepath" "regexp" "strings" "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/common" "golang.org/x/mod/modfile" ) // Capitalized struct items are accessed outside this file type CommandArgs struct { logWriter *common.LogWriter excludeFile string symPrefix string inputDir string outputDir string instrumentorVersion string localSDKPath string VersionText string ShowVersion bool InvalidArgs bool wantsInstrumentor bool skipTestFiles bool skipProtoBufFiles bool } func ParseArgs(versionText string, thisVersion string) *CommandArgs { versionPtr := flag.Bool("version", false, "the current version of this application") exclusionsPtr := flag.String("exclude", "", "the path to a file listing files and directories to exclude from instrumentation (optional)") prefixPtr := flag.String("prefix", "", "a string to prepend to the symbol table (optional)") logfilePtr := flag.String("logfile", "", "file path to log into (default=stderr)") verbosePtr := flag.Int("V", 0, "verbosity level (default to 0)") assertOnlyPtr := flag.Bool("assert_only", false, "generate assertion catalog ONLY - no coverage instrumentation (default to false)") catalogDirPtr := flag.String("catalog_dir", "", "(deprecated, ignored)") instrVersionPtr := flag.String("instrumentor_version", thisVersion, "version of the SDK instrumentation package to require") localSDKPathPtr := flag.String("local_sdk_path", "", "path to the local Antithesis SDK") skipTestFilesPtr := flag.Bool("skip_test_files", false, "Skip instrumentation and cataloging for '*_test.go' files (default to false)") skipProtoBufFilesPtr := flag.Bool("skip_protobuf_files", false, "Skip instrumentation and cataloging for '*.pb.go' files (default to false)") flag.Parse() cmdArgs := CommandArgs{ InvalidArgs: false, ShowVersion: *versionPtr, } if cmdArgs.ShowVersion { return &cmdArgs } cmdArgs.logWriter = common.NewLogWriter(*logfilePtr, *verbosePtr) cmdArgs.wantsInstrumentor = !*assertOnlyPtr cmdArgs.symPrefix = strings.TrimSpace(*prefixPtr) cmdArgs.excludeFile = strings.TrimSpace(*exclusionsPtr) cmdArgs.instrumentorVersion = strings.TrimSpace(*instrVersionPtr) cmdArgs.localSDKPath = strings.TrimSpace(*localSDKPathPtr) cmdArgs.VersionText = versionText cmdArgs.skipTestFiles = *skipTestFilesPtr cmdArgs.skipProtoBufFiles = *skipProtoBufFilesPtr // Verify we have the expected number of positional arguments numArgsRequired := 1 if cmdArgs.wantsInstrumentor { numArgsRequired++ } if *catalogDirPtr != "" { cmdArgs.logWriter.Printf("Warning: -catalog_dir is deprecated and will be ignored") } if flag.NArg() < numArgsRequired { fmt.Fprintf(os.Stderr, "%s", strings.TrimSpace(versionText)) fmt.Fprintf(os.Stderr, "\n\n") fmt.Fprintf(os.Stderr, "For assertions support:\n") fmt.Fprintf(os.Stderr, " $ antithesis-go-instrumentor -assert_only [options] go_project_dir\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, " - The go_project_dir should contain a valid go.mod file\n") fmt.Fprintf(os.Stderr, "\n\n") fmt.Fprintf(os.Stderr, "For full instrumentations (including assertions support):\n") fmt.Fprintf(os.Stderr, " $ antithesis-go-instrumentor [options] go_project_dir target_dir\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, " - The go_project_dir should contain a valid go.mod file\n") fmt.Fprintf(os.Stderr, " - The target_dir should be an existing, but empty directory\n") fmt.Fprintf(os.Stderr, "\n\n") fmt.Fprintf(os.Stderr, "The Assertions catalog will be registered in a generated file:\n") fmt.Fprintf(os.Stderr, " antithesis_catalog.go\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, " - For assertions support, the catalog will be created in the go_project_dir\n") fmt.Fprintf(os.Stderr, " - For full instrumentation, the catalog will be created under the target_dir\n") fmt.Fprintf(os.Stderr, "\n\n") flag.Usage() cmdArgs.InvalidArgs = true return &cmdArgs } if cmdArgs.symPrefix != "" { m, _ := regexp.MatchString(`^[a-z]+$`, *prefixPtr) if !m { fmt.Fprint(os.Stderr, "A prefix must consist of lower-case ASCII letters.\n") cmdArgs.InvalidArgs = true return &cmdArgs } } cmdArgs.inputDir = flag.Arg(0) if cmdArgs.wantsInstrumentor { cmdArgs.outputDir = flag.Arg(1) } if !IsGoAvailable() { fmt.Fprint(os.Stderr, "Go toolchain not available\n") cmdArgs.InvalidArgs = true } return &cmdArgs } func (ca *CommandArgs) ShowArguments() { ca.logWriter.Printf("inputDir: %q", ca.inputDir) if ca.localSDKPath != "" { ca.logWriter.Printf("localSDKPath: %q", ca.localSDKPath) } if ca.wantsInstrumentor { ca.logWriter.Printf("outputDir: %q", ca.outputDir) if ca.excludeFile != "" { ca.logWriter.Printf("excludeFile: %q", ca.excludeFile) } if ca.symPrefix != "" { ca.logWriter.Printf("symPrefix: %q", ca.symPrefix) } } // Intentional: no need to show anything if not skipping if ca.skipTestFiles { ca.logWriter.Printf("skipTestFiles: %t", ca.skipTestFiles) } if ca.skipProtoBufFiles { ca.logWriter.Printf("skipProtoBufFiles: %t", ca.skipProtoBufFiles) } } func (ca *CommandArgs) NewCommandFiles() (cfx *CommandFiles, err error) { outputDirectory := "" customerInputDirectory := common.GetAbsoluteDirectory(ca.inputDir) if ca.wantsInstrumentor { outputDirectory = common.GetAbsoluteDirectory(ca.outputDir) err = common.ValidateDirectories(customerInputDirectory, outputDirectory) } symtablePrefix := "" if ca.symPrefix != "" { symtablePrefix = ca.symPrefix + "-" } if err == nil { if _, err = GetModuleName(customerInputDirectory); err != nil { err = fmt.Errorf("unable to obtain go module name from %q", customerInputDirectory) } } customerDirectory := "" notifierDirectory := "" symbolsDirectory := "" if ca.wantsInstrumentor { customerDirectory = filepath.Join(outputDirectory, common.INSTRUMENTED_SOURCE_FOLDER) notifierDirectory = filepath.Join(outputDirectory, common.NOTIFIER_FOLDER) symbolsDirectory = filepath.Join(outputDirectory, common.SYMBOLS_FOLDER) if err == nil { err = CreateOutputDirectories(customerDirectory, notifierDirectory, symbolsDirectory) } } if err != nil { return } catalogBaseDir := customerInputDirectory if ca.wantsInstrumentor { catalogBaseDir = customerDirectory } cfx = &CommandFiles{ outputDirectory: outputDirectory, inputDirectory: customerInputDirectory, customerDirectory: customerDirectory, notifierDirectory: notifierDirectory, symbolsDirectory: symbolsDirectory, catalogBaseDir: catalogBaseDir, excludeFile: ca.excludeFile, wantsInstrumentor: ca.wantsInstrumentor, symtablePrefix: symtablePrefix, instrumentorVersion: ca.instrumentorVersion, localSDKPath: ca.localSDKPath, logWriter: common.GetLogWriter(), skipTestFiles: ca.skipTestFiles, skipProtoBufFiles: ca.skipProtoBufFiles, } return } func GetModuleName(inputDir string) (moduleName string, err error) { var moduleData []byte moduleName = "" var f *modfile.File = nil moduleFilenamePath := filepath.Join(inputDir, "go.mod") if moduleData, err = os.ReadFile(moduleFilenamePath); err != nil { return } if f, err = modfile.ParseLax("go.mod", moduleData, nil); err == nil { moduleName = f.Module.Mod.Path } return } func CreateOutputDirectories(customerDirectory, notifierDirectory, symbolsDirectory string) (err error) { if err = os.Mkdir(customerDirectory, 0755); err != nil { return } if err = os.Mkdir(notifierDirectory, 0755); err != nil { return } err = os.Mkdir(symbolsDirectory, 0755) return } func IsGoAvailable() bool { cmd := exec.Command("go", "version") output, err := cmd.Output() if err != nil { return false } // go version is expected to output 1 line containing 4 space-delimited items // Typical output expected is: // // go version go1.21.5 linux/amd64 // // verify we get this 'shape' output parts := strings.Split(strings.TrimSpace(string(output)), " ") if len(parts) < 4 { return false } return (parts[0] == "go") && (parts[1] == "version") && strings.HasPrefix(parts[2], "go") } golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/cmd/cmd_files.go000066400000000000000000000277531516252053700340320ustar00rootroot00000000000000package cmd import ( "fmt" "io/fs" "os" "path/filepath" "strings" "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/common" "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/instrumentor" ) // Capitalized struct items are accessed outside this file type CommandFiles struct { // The set of exclusions that were obtained from // reading the 'excludeFile'. A map is used where // the value is always 'true', in lieu of a specific // 'set' abstraction not available for the version // of go used for this tool. exclusions map[string]bool // The modules inferred from scanning source files and // looking for files named go.mod // Each of these go.mod files may need the antithesis // module added as a dependency. dependentModules map[string]bool // Global logger logWriter *common.LogWriter // The name of the symbol table file, which incorporates // the overall 'filesHash' and 'symbtablePrefix' symbolTableFilename string // SHA256 Hash (48-bits worth) of all the files // in sourceFiles filesHash string // created and written during instrumentation. // Will contain the corresponding .tsv file expected // by the antithesis fuzzer symbolsDirectory string // The base directory where assertion catalog(s) will be written. // For assert-only mode, this is the inputDirectory. // For full instrumentation, this is the customerDirectory. // Per-binary catalogs are placed in subdirectories matching // each main package's relative position. catalogBaseDir string // The instrumentation (only) base output directory // Is required to exist, and to be empty prior to instrumentation // // After instrumentation, will contain the subdirectories for // 'symbols' and 'customer' outputDirectory string // A prefix used to distinguish symbol table filenames // that will be used by the antithesis fuzzer. symtablePrefix string // The base directry of a go module to be instrumented/cataloged // Contains a go.mod file inputDirectory string // Option file containing a list (one per line) of // any files or directories to be excluded from both // instumentation and assertion scanning. Empty lines // and lines beginining with '#' are ignored. excludeFile string // created and written to during instrumentation. // Will contain a copy of the inputDirectory, where All // non-excluded '.go' files are instrumented customerDirectory string // The version of SDK to use at runtime for CoverageInstrumentation instrumentorVersion string // The path to the SDK to use to create the notifier localSDKPath string // created and written to during instrumentation. // Will contain the antithesis notifier module (go.mod) and source (notifier.go) notifierDirectory string // All of the files (after exclusions) to be instrumented // and scanned for assertions that should appear in the // assertion catalog sourceFiles []string // Number of files skipped when creating the sourceFiles // list. filesSkipped int // Indicates that instrumentation is requested (true) // If set to (false) then perform assertion catalog scanning // without instrumentation, which is common // when execution is outside of the Antithesis environment wantsInstrumentor bool // Indicates that '*_test.go' files should be skipped (not instrumented) // default is false skipTestFiles bool // Indicates that '*.pb.go' files should be skipped (not instrumented) // default is false skipProtoBufFiles bool } func (cfx *CommandFiles) GetSourceFiles() (sourceFiles []string, err error) { sourceFiles = []string{} cfx.filesSkipped = 0 if err = cfx.ParseExclusionsFile(); err != nil { return } numSkipped := 0 if sourceFiles, numSkipped, err = cfx.FindSourceCode(); err != nil { return } cfx.filesSkipped = numSkipped cfx.filesHash = common.HashFileContent(sourceFiles) return } func (cfx *CommandFiles) NewCoverageInstrumentor() *instrumentor.CoverageInstrumentor { var file_instrumentor *instrumentor.Instrumentor var symTable *instrumentor.SymbolTable notifierModuleName := common.FullNotifierName(cfx.filesHash) if cfx.wantsInstrumentor { cfx.logWriter.Printf("Writing instrumented source to %s", cfx.customerDirectory) symTable = cfx.CreateSymbolTableWriter(cfx.filesHash) file_instrumentor = instrumentor.CreateInstrumentor(cfx.inputDirectory, notifierModuleName, symTable) } cI := instrumentor.CoverageInstrumentor{ GoInstrumentor: file_instrumentor, SymTable: symTable, UsingSymbols: cfx.UsingSymbols(), PreviousEdge: 0, FilesInstrumented: 0, FilesSkipped: cfx.filesSkipped, NotifierPackage: common.NotifierPackage(cfx.filesHash), } return &cI } func (cfx *CommandFiles) WrapUp() { if !cfx.wantsInstrumentor { return } notifierModule := common.FullNotifierName(cfx.filesHash) notifierRelPath := ".." localNotifier := filepath.Join(notifierRelPath, common.NOTIFIER_FOLDER) common.AddDependencies(cfx.inputDirectory, cfx.customerDirectory, cfx.instrumentorVersion, notifierModule, localNotifier) someOffset := "" for modFolder, used := range cfx.dependentModules { if used { someOffset = common.PathFromBaseDirectory(cfx.inputDirectory, modFolder) if someOffset != "" { destModuleFolder := filepath.Join(cfx.customerDirectory, someOffset) os.MkdirAll(destModuleFolder, 0777) basePath := filepath.Join(cfx.customerDirectory, someOffset) targPath := cfx.notifierDirectory if altDestModuleFolder, erx := filepath.Rel(basePath, targPath); erx == nil { common.AddDependencies(modFolder, destModuleFolder, cfx.instrumentorVersion, notifierModule, altDestModuleFolder) } } } } if err := common.CopyRecursiveDir(cfx.inputDirectory, cfx.customerDirectory); err == nil { cfx.logWriter.Printf("All other files copied unmodified from %s to %s", cfx.inputDirectory, cfx.customerDirectory) } else { cfx.logWriter.Printf("CopyRecursiveDir err: %s", err.Error()) } if cfx.logWriter.VerboseLevel(1) { common.ShowDirRecursive(cfx.customerDirectory, "instrumented files") } if cfx.localSDKPath == "" { common.FetchDependencies(cfx.customerDirectory) cfx.logWriter.Printf("Downloaded Antithesis dependencies") } } func (cfx *CommandFiles) GetSourceDir() string { return cfx.inputDirectory } // Full instrumentation targets the customerDirectory // Assertions only mode will target in-place (same as inputDirectory) func (cfx *CommandFiles) GetTargetDir() string { if cfx.wantsInstrumentor { return cfx.customerDirectory } return cfx.inputDirectory } func (cfx *CommandFiles) WriteInstrumentedOutput(fileName string, instrumentedSource string, cI *instrumentor.CoverageInstrumentor) { // skip over the base inputDirectory from the inputfilename, // and create the output directories needed skipLength := len(cfx.inputDirectory) outputPath := filepath.Join(cfx.customerDirectory, fileName[skipLength:]) outputSubdirectory := filepath.Dir(outputPath) os.MkdirAll(outputSubdirectory, 0755) if cfx.logWriter.VerboseLevel(1) { cfx.logWriter.Printf("Writing instrumented file %s with edges %d–%d", outputPath, cI.PreviousEdge, cI.GoInstrumentor.CurrentEdge) } if err := common.WriteTextFile(instrumentedSource, outputPath); err == nil { cI.FilesInstrumented++ } } func (cfx *CommandFiles) CreateNotifierModule() { notifierModuleName := common.NOTIFIER_MODULE_NAME if cfx.wantsInstrumentor { common.NotifierDependencies(cfx.notifierDirectory, notifierModuleName, cfx.instrumentorVersion, cfx.localSDKPath) } } func (cfx *CommandFiles) ParseExclusionsFile() (err error) { if cfx.excludeFile == "" { return } cfx.exclusions = map[string]bool{} var parsedExclusions map[string]bool parsedExclusions, err = ParseExclusionsFile(cfx.excludeFile, cfx.inputDirectory) if err == nil { cfx.exclusions = parsedExclusions } return } // FindSourceCode scans an input directory recursively for .go files, // skipping any files or directories specified in exclusions. func (cfx *CommandFiles) FindSourceCode() (paths []string, numSkipped int, err error) { paths = []string{} numSkipped = 0 cfx.dependentModules = map[string]bool{} cfx.logWriter.Printf("Scanning %s recursively for .go source", cfx.inputDirectory) // Files are read in lexical order, i.e. we can later deterministically // hash their content: https://pkg.go.dev/path/filepath#WalkDir err = filepath.WalkDir(cfx.inputDirectory, func(path string, info fs.DirEntry, erx error) error { if erx != nil { cfx.logWriter.Printf("Error %v in directory %s; skipping", erx, path) return erx } if b := filepath.Base(path); strings.HasPrefix(b, ".") { if cfx.logWriter.VerboseLevel(2) { cfx.logWriter.Printf("Ignoring 'dot' directory: %s", path) } if info.IsDir() { return fs.SkipDir } return nil } if b := filepath.Base(path); b == "testdata" { if cfx.logWriter.VerboseLevel(2) { cfx.logWriter.Printf("Ignoring 'testdata' directory: %s", path) } if info.IsDir() { return fs.SkipDir } return nil } if cfx.exclusions[path] { if info.IsDir() { cfx.logWriter.Printf("Ignoring excluded directory %s and its children", path) return fs.SkipDir } cfx.logWriter.Printf("Skipping excluded file %s", path) numSkipped++ return nil } if info.IsDir() { return nil } dir, baseFile := filepath.Split(path) ext := filepath.Ext(path) if ext != ".go" { if baseFile == "go.mod" { cfx.dependentModules[filepath.Clean(dir)] = false } numSkipped++ return nil } // This is the mandatory format of unit test file names. if cfx.skipTestFiles && strings.HasSuffix(baseFile, "_test.go") { if cfx.logWriter.VerboseLevel(1) { cfx.logWriter.Printf("Skipping test file %s", path) } numSkipped++ return nil } if cfx.skipProtoBufFiles && strings.HasSuffix(baseFile, ".pb.go") { if cfx.logWriter.VerboseLevel(1) { cfx.logWriter.Printf("Skipping generated file %s", path) } numSkipped++ return nil } paths = append(paths, path) return nil }) if err != nil { err = fmt.Errorf("error walking input directory %s: %v", cfx.inputDirectory, err) } return } func (cfx *CommandFiles) UsingSymbols() string { usingSymbols := "" if cfx.wantsInstrumentor { usingSymbols = cfx.symbolTableFilename } return usingSymbols } func (cfx *CommandFiles) CreateSymbolTableWriter(filesHash string) (symWriter *instrumentor.SymbolTable) { var err error cfx.symbolTableFilename = "" if cfx.wantsInstrumentor { symbolTableFileBasename := fmt.Sprintf("%s%s-%s", cfx.symtablePrefix, common.SYMBOLS_FILE_HASH_PREFIX, filesHash) cfx.symbolTableFilename = symbolTableFileBasename + common.SYMBOLS_FILE_SUFFIX symbolsPath := filepath.Join(cfx.symbolsDirectory, cfx.symbolTableFilename) symWriter, err = instrumentor.CreateSymbolTableFile(symbolsPath, symbolTableFileBasename) if err != nil { cfx.logWriter.Fatalf("Could not write symbol table header: %s", err.Error()) } } return } func (cfx *CommandFiles) GetNotifierDirectory() string { return cfx.notifierDirectory } func (cfx *CommandFiles) GetCatalogBaseDir() string { return cfx.catalogBaseDir } func (cfx *CommandFiles) ShowDependentModules() { isText := "" cfx.logWriter.Printf("") cfx.logWriter.Printf("Module Usage Summary") for modName, used := range cfx.dependentModules { isText = "is" if !used { isText = "is not" } cfx.logWriter.Printf("%s %s used", modName, isText) } cfx.logWriter.Printf("") } func (cfx *CommandFiles) UpdateDependentModules(file_name string) { ok := false isUsed := false this_dir := filepath.Clean(filepath.Dir(file_name)) for !ok { if cfx.logWriter.VerboseLevel(2) { cfx.logWriter.Printf("Checking if %q is a dependentModule", this_dir) } if this_dir == "." { break } isUsed, ok = cfx.dependentModules[this_dir] if ok { if !isUsed { cfx.dependentModules[this_dir] = true } return } else { old_dir := this_dir this_dir = filepath.Clean(filepath.Dir(this_dir)) ok = (old_dir == this_dir) } } cfx.logWriter.Printf("%q does not belong to a scanned module", file_name) } exclusions.go000066400000000000000000000032061516252053700342050ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/cmdpackage cmd import ( "bufio" "os" "path/filepath" "strings" "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/common" ) // ParseExclusionsFile reads the exclusions file, skipping lines beginning with // #. Golang does not have a set class, so, rather than waste space copy-pastaing // code from the interwebs, we'll just return a map. func ParseExclusionsFile(path string, inputDirectory string) (exclusions map[string]bool, err error) { exclusions = map[string]bool{} logWriter := common.GetLogWriter() var exclusionsFile *os.File if exclusionsFile, err = os.Open(path); err != nil { logWriter.Fatalf("Could not open exclusions %s: %v", path, err) return } defer exclusionsFile.Close() logWriter.Printf("Reading exclusions from %s; relative paths will be resolved to %s", path, inputDirectory) scanner := bufio.NewScanner(exclusionsFile) for scanner.Scan() { entry := scanner.Text() if strings.HasPrefix(entry, "#") || strings.TrimSpace(entry) == "" { continue } exclusion := entry if !filepath.IsAbs(entry) { exclusion = filepath.Join(inputDirectory, entry) } if exclusion, err = filepath.Abs(exclusion); err != nil { logWriter.Fatalf("Exclusion %s could not be resolved to an absolute path: %v", entry, err) return } if _, err = os.Stat(exclusion); err == nil { exclusions[exclusion] = true logWriter.Printf("Exclusion %s added as %s", entry, exclusion) } else { logWriter.Fatalf("File %s in exclusions does not exist or is inaccessible", entry) return } } if err = scanner.Err(); err != nil { logWriter.Fatalf("Error scanning file %s: %v", path, err) } return } golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/common/000077500000000000000000000000001516252053700322655ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/common/files.go000066400000000000000000000237721516252053700337310ustar00rootroot00000000000000package common import ( "crypto/sha256" "encoding/hex" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "strings" ) const ( HashBitsUsed = 48 HashBytesUsed = HashBitsUsed / 8 EncodedHashByteLength = HashBytesUsed * 2 ) // HashFileContent reads the binary content of // every file in paths (assumed to be in lexical order) // and returns the SHA-256 digest. func HashFileContent(paths []string) string { hasher := sha256.New() for _, path := range paths { bytes, err := os.ReadFile(path) if err != nil { logWriter.Fatalf("Error reading file %s: %v", path, err) } hasher.Write(bytes) } return hex.EncodeToString(hasher.Sum(nil))[0:EncodedHashByteLength] } func WriteTextFile(text, file_name string) (err error) { var f *os.File if f, err = os.Create(file_name); err != nil { logWriter.Printf("Error: could not create %s", file_name) quietClose(f) return } if _, err = f.WriteString(text); err != nil { quietClose(f) logWriter.Printf("Error: Could not write text to %s", file_name) } else { err = f.Close() if err != nil { logWriter.Printf("Error: Could not sync text to %s", file_name) } } return } func EnsureDirExists(dirName string) (err error) { var dirInfo os.FileInfo if dirInfo, err = os.Stat(dirName); err != nil { err = os.MkdirAll(dirName, 0777) return } // We stat-ed an existing directory or something else if !dirInfo.IsDir() { err = fmt.Errorf("%q is not a directory", dirName) } return } func OpenExistingDir(dirName string) (dir *os.File, err error) { var dirInfo os.FileInfo if dir, err = os.Open(dirName); err != nil { dir = nil return } if dirInfo, err = dir.Stat(); err != nil { quietClose(dir) dir = nil return } if !dirInfo.IsDir() { quietClose(dir) dir = nil err = fmt.Errorf("%q is not a directory", dirName) } return } // Use quietClose when there are files that should be closed, and // the caller has already decided to return `err`. Do not use // quietClose where the return value from Close(), on a writable file // is important to isolate and preserve. // // Ref: https://www.joeshaw.org/dont-defer-close-on-writable-files/ func quietClose(fps ...*os.File) { for _, fp := range fps { if fp != nil { fp.Close() } } } func CopyFile(from, to string) (err error) { var fromFile *os.File var toFile *os.File var fileInfo os.FileInfo if fromFile, err = os.Open(from); err != nil { quietClose(fromFile) return } if toFile, err = os.Create(to); err != nil { quietClose(fromFile, toFile) return } if _, err = io.Copy(toFile, fromFile); err != nil { quietClose(fromFile, toFile) return } if err = toFile.Sync(); err != nil { quietClose(fromFile, toFile) return } // Dont use quietClose so that Close() errors can be properly reported if fromFile != nil { if err = fromFile.Close(); err != nil { quietClose(toFile) return } } if toFile != nil { if err = toFile.Close(); err != nil { return } } if fileInfo, err = os.Stat(from); err != nil { return } err = os.Chmod(to, fileInfo.Mode()) return } func CopyRecursiveDir(from, to string) (err error) { var fromDir *os.File var fromDirEntries []os.DirEntry var fileInfo os.FileInfo if fromDir, err = OpenExistingDir(from); err != nil { return } // Make sure the 'to' path is a directory that exists if err = EnsureDirExists(to); err != nil { return } // Read the 'from' directory (and close its *File) if fromDirEntries, err = fromDir.ReadDir(0); err != nil { quietClose(fromDir) return } if err = fromDir.Close(); err != nil { return } for _, dirEntry := range fromDirEntries { if fileInfo, err = dirEntry.Info(); err != nil { return } filename := fileInfo.Name() fromPath := filepath.Join(from, filename) toPath := filepath.Join(to, filename) fileMode := fileInfo.Mode() if fileMode.IsDir() { if err = CopyRecursiveDir(fromPath, toPath); err != nil { return } continue } if (fileMode & os.ModeSymlink) == os.ModeSymlink { if _, err = os.Stat(toPath); errors.Is(err, os.ErrNotExist) { var target string if target, err = os.Readlink(fromPath); err != nil { return } if err = os.Symlink(target, toPath); err != nil { return } } } if fileMode.IsRegular() { if _, err = os.Stat(toPath); errors.Is(err, os.ErrNotExist) { if err = CopyFile(fromPath, toPath); err != nil { return } } } } return } func ShowDirRecursive(dir, desc string) { commandLine := fmt.Sprintf("ls -lR %s", dir) cmd := exec.Command("bash", "-c", commandLine) logWriter.Printf("") logWriter.Printf("Directory Listing (%s)", desc) logWriter.Printf("Executing %s", commandLine) allOutput, err := cmd.CombinedOutput() allText := strings.TrimSpace(string(allOutput)) lines := strings.Split(allText, "\n") for _, line := range lines { if len(line) > 0 { logWriter.Printf("ls: %s", line) } } if err != nil { logWriter.Printf("ls completed with %+v", err) } } func AddDependencies(customerInputDirectory, customerOutputDirectory, instrumentorVersion, notifierModule, localNotifier string) { destGoModFile := fmt.Sprintf("%s/go.mod", customerOutputDirectory) cmd1 := fmt.Sprintf("cd %s", customerInputDirectory) cmd2 := fmt.Sprintf("go mod edit -require=%s@v0.0.0 -replace=%s=%s -print > %s", notifierModule, notifierModule, localNotifier, destGoModFile) commandLine := fmt.Sprintf("(%s; %s)", cmd1, cmd2) cmd := exec.Command("bash", "-c", commandLine) logWriter.Printf("Adding Antithesis module as a dependency to %s", customerOutputDirectory) logWriter.Printf("Executing %s", commandLine) allOutput, err := cmd.CombinedOutput() allText := strings.TrimSpace(string(allOutput)) if len(allText) > 0 { lines := strings.Split(allText, "\n") for _, line := range lines { logWriter.Printf("go mod edit: %s", line) } } if err != nil { // Errors here are pretty mysterious. logWriter.Fatalf("%v", err) } } func FetchDependencies(customerOutputDirectory string) { commandLine := fmt.Sprintf("(cd %s; go mod tidy)", customerOutputDirectory) cmd := exec.Command("bash", "-c", commandLine) logWriter.Printf("") logWriter.Printf("Fetching Dependencies (go mod tidy)") logWriter.Printf("Executing %s", commandLine) allOutput, err := cmd.CombinedOutput() allText := strings.TrimSpace(string(allOutput)) if len(allText) > 0 { lines := strings.Split(allText, "\n") for _, line := range lines { logWriter.Printf("go mod tidy: %s", line) } } if err != nil { // Errors here are pretty mysterious. logWriter.Fatalf("%v", err) } } func NotifierDependencies(notifierOutputDirectory, notifierModuleName, instrumentorVersion, localSDKPath string) { dependencyRef := fmt.Sprintf("go get %s@%s", ANTITHESIS_SDK_MODULE, instrumentorVersion) if localSDKPath != "" { dependencyRef = fmt.Sprintf("go mod edit -require=%s@v0.0.0 -replace=%s=%s", ANTITHESIS_SDK_MODULE, ANTITHESIS_SDK_MODULE, localSDKPath) } commandLine := fmt.Sprintf("(cd %s; go mod init %s; %s; go mod tidy)", notifierOutputDirectory, notifierModuleName, dependencyRef) cmd := exec.Command("bash", "-c", commandLine) logWriter.Printf("") logWriter.Printf("Creating Notifier Module") logWriter.Printf("Executing %s", commandLine) allOutput, err := cmd.CombinedOutput() allText := strings.TrimSpace(string(allOutput)) if len(allText) > 0 { lines := strings.Split(allText, "\n") for _, line := range lines { logWriter.Printf("go mod (notifier): %s", line) } } if err != nil { // Errors here are pretty mysterious. logWriter.Fatalf("%v", err) } } // GetAbsoluteDirectory converts a path, whether a symlink or // a relative path, into an absolute path. func GetAbsoluteDirectory(path string) string { if absolute, e := filepath.Abs(path); e != nil { logWriter.Fatalf("Could not evaluate %s as an absolute path: %v", path, e) } else { if s, err := os.Stat(absolute); err != nil { logWriter.Fatalf("%v", err) } else { if !s.IsDir() { logWriter.Fatalf("%s is not a directory", absolute) } return absolute } } // This code will never be executed. return "" } func CanonicalizeDirectory(d string) string { target, e := filepath.EvalSymlinks(d) if e != nil { logWriter.Fatalf("filepath.EvalSymlinks(%s) failed: %v", d, e) } a, e := filepath.Abs(target) if e != nil { logWriter.Fatalf("filepath.Abs(%s) failed: %v", target, e) } return a } func confirmEmptyOutputDirectory(output string) { d, e := os.Open(output) if e != nil { logWriter.Fatalf("Could not open %s: %v", output, e) } defer d.Close() // See the documentation on File.Readdirnames(). if names, _ := d.Readdirnames(1); len(names) > 0 { logWriter.Fatalf("Output directory %s must be empty.", output) } } // ValidateDirectories checks that neither directory is a child of the other, // and of course that they're not the same. func ValidateDirectories(input, output string) (err error) { // Go does not have a type for filepaths, and will not do this for me: https://golang.org/src/path/filepath/path_unix.go?s=717:754#L16 // The UNIX kernel absolutely forbids slashes in filenames. So, quick and dirty: input = CanonicalizeDirectory(input) + "/" output = CanonicalizeDirectory(output) + "/" if strings.HasPrefix(output, input) { err = fmt.Errorf("input directory %s is a prefix of the output directory %s", input, output) return } if strings.HasPrefix(input, output) { err = fmt.Errorf("output directory %s is a prefix of the input directory %s", output, input) } return } // PathFromBaseDirectory gets the path of someDir relative to baseDir // // Example: // PathFromBaseDirectory("/home/ricky/etcd", "/home/ricky/etcd/server/test") // // ==> "server/test" func PathFromBaseDirectory(baseDir, someDir string) string { baseNorm := CanonicalizeDirectory(baseDir) someNorm := CanonicalizeDirectory(someDir) if baseNorm == someNorm { return "" } someOffset := someNorm pattern := filepath.Join(baseNorm, "*") if didMatch, _ := filepath.Match(pattern, someNorm); didMatch { lx := len(baseNorm) idx := lx + 1 if idx < len(someNorm) { someOffset = someNorm[idx:] } } return someOffset } golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/common/logger.go000066400000000000000000000031621516252053700340750ustar00rootroot00000000000000package common import ( "log" "os" "strings" ) // ------------------------------------------------------------ // Replaces glog // // If the verbosity at the call site is less than or equal to // level requested, the log will be enabled. Higher callsite // verbosity values are less likely to be output. // // if (2 <= verbosity) { log-is-enabled } // ------------------------------------------------------------ type LogWriter struct { logger *log.Logger verbosity int } // var logger *log.Logger // var verbosity int = 0 var logWriter *LogWriter func NewLogWriter(logfileName string, vLevel int) *LogWriter { if logWriter != nil { return logWriter } var erx error var fp *os.File wrx := os.Stderr logfilePath := strings.TrimSpace(logfileName) if logfilePath != "" { if fp, erx = os.Create(logfilePath); erx == nil { wrx = fp } } // Setting up the globals logger := log.New(wrx, "", log.LstdFlags|log.Lshortfile) verbosity := vLevel // Advise if the requested logfile was not created if erx != nil { logger.Printf("WARNING Unable to Create/Open requested logfile: %q", logfilePath) } logWriter = &LogWriter{logger, verbosity} return logWriter } func GetLogWriter() *LogWriter { return NewLogWriter("", 0) } func (lW *LogWriter) IsVerbose() bool { return (lW.verbosity > 0) } func (lW *LogWriter) VerboseLevel(v int) bool { return (v <= lW.verbosity) } func (lW *LogWriter) Printf(format string, v ...any) { lW.logger.Printf(format, v...) } func (lW *LogWriter) Fatal(v ...any) { lW.logger.Fatal(v...) } func (lW *LogWriter) Fatalf(format string, v ...any) { lW.logger.Fatalf(format, v...) } golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/common/names.go000066400000000000000000000023501516252053700337170ustar00rootroot00000000000000package common import "fmt" const ( NAME_NOT_AVAILABLE = "anonymous" ANTITHESIS_SDK_MODULE = "github.com/antithesishq/antithesis-sdk-go" ASSERT_PACKAGE = "assert" INSTRUMENTATION_PACKAGE = "instrumentation" NOTIFIER_MODULE_NAME = "antithesis.notifier" GENERATED_CATALOG_FILE = "antithesis_catalog.go" INSTRUMENTED_SOURCE_FOLDER = "customer" SYMBOLS_FOLDER = "symbols" SYMBOLS_FILE_HASH_PREFIX = "go" SYMBOLS_FILE_SUFFIX = ".sym.tsv" NOTIFIER_FOLDER = "notifier" GENERATED_NOTIFIER_SOURCE = "notifier.go" NOTIFIER_PACKAGE_PREFIX = "z" ) func SDKPackageName(packageName string) string { return fmt.Sprintf("%s/%s", ANTITHESIS_SDK_MODULE, packageName) } func AssertPackageName() string { return SDKPackageName(ASSERT_PACKAGE) } func InstrumentationPackageName() string { return SDKPackageName(INSTRUMENTATION_PACKAGE) } // package z4a1b45a05078 func NotifierPackage(filesHash string) string { return fmt.Sprintf("%s%s", NOTIFIER_PACKAGE_PREFIX, filesHash) } // require antithesis.notifier/z4a1b45a05078 func FullNotifierName(filesHash string) string { packageName := NotifierPackage(filesHash) return fmt.Sprintf("%s/%s", NOTIFIER_MODULE_NAME, packageName) } golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/common/text.go000066400000000000000000000002131516252053700335740ustar00rootroot00000000000000package common func Pluralize(val int, singularText string) string { if val == 1 { return singularText } return singularText + "s" } golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/e2e_test.go000066400000000000000000000117021516252053700330370ustar00rootroot00000000000000package main import ( "io/fs" "os" "os/exec" "path/filepath" "regexp" "strings" "testing" qt "github.com/go-quicktest/qt" ) func TestE2E(t *testing.T) { t.Setenv("CGO_ENABLED", "0") // Build the instrumentor binary. instrumentorBin := filepath.Join(t.TempDir(), "instrumentor") runCmd(t, ".", "go", "build", "-o", instrumentorBin, ".") // Absolute path to the SDK repo root (sdk/go/repo). sdkRoot, err := filepath.Abs(filepath.Join("..", "..")) qt.Assert(t, qt.IsNil(err)) // Copy input fixture to a temp dir so we don't modify testdata, and // rewrite the replace directive to use an absolute SDK path (the // relative path in the checked-in go.mod won't resolve from a temp dir). inputDir := filepath.Join(t.TempDir(), "input") copyDir(t, "testdata/input", inputDir) rewriteReplace(t, filepath.Join(inputDir, "go.mod"), sdkRoot) // Run the instrumentor with -local_sdk_path so it works in // sandboxed builds (e.g. Nix) where GOPROXY=off. outputDir := filepath.Join(t.TempDir(), "output") err = os.MkdirAll(outputDir, 0755) qt.Assert(t, qt.IsNil(err)) runCmd(t, ".", instrumentorBin, "-local_sdk_path", sdkRoot, inputDir, outputDir) expectedDir := "testdata/expected_output" // Compare files that should match after normalization. for _, relPath := range []string{ "customer/main.go", "customer/go.mod", "notifier/notifier.go", "notifier/go.mod", } { t.Run(relPath, func(t *testing.T) { expected, err := os.ReadFile(filepath.Join(expectedDir, relPath)) qt.Assert(t, qt.IsNil(err)) actual, err := os.ReadFile(filepath.Join(outputDir, relPath)) qt.Assert(t, qt.IsNil(err)) qt.Check(t, qt.Equals( normalizeContent(string(actual)), normalizeContent(string(expected)), )) }) } // notifier/go.sum won't exist because we use the local sdk // Compare symbol table (filename contains a content hash, so glob for it). t.Run("symbols", func(t *testing.T) { expectedFiles, err := filepath.Glob(filepath.Join(expectedDir, "symbols", "*.sym.tsv")) qt.Assert(t, qt.IsNil(err)) qt.Assert(t, qt.HasLen(expectedFiles, 1)) actualFiles, err := filepath.Glob(filepath.Join(outputDir, "symbols", "*.sym.tsv")) qt.Assert(t, qt.IsNil(err)) qt.Assert(t, qt.HasLen(actualFiles, 1)) expected, err := os.ReadFile(expectedFiles[0]) qt.Assert(t, qt.IsNil(err)) actual, err := os.ReadFile(actualFiles[0]) qt.Assert(t, qt.IsNil(err)) qt.Check(t, qt.Equals( normalizeContent(string(actual)), normalizeContent(string(expected)), )) }) } // runCmd runs a command and fails the test if it exits non-zero. func runCmd(t *testing.T, dir string, name string, args ...string) { t.Helper() cmd := exec.Command(name, args...) cmd.Dir = dir out, err := cmd.CombinedOutput() qt.Assert(t, qt.IsNil(err), qt.Commentf("%s %s failed:\n%s", name, strings.Join(args, " "), out)) } // copyDir recursively copies src to dst. func copyDir(t *testing.T, src, dst string) { t.Helper() err := filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } rel, err := filepath.Rel(src, path) if err != nil { return err } target := filepath.Join(dst, rel) if d.IsDir() { return os.MkdirAll(target, 0755) } data, err := os.ReadFile(path) if err != nil { return err } return os.WriteFile(target, data, 0644) }) qt.Assert(t, qt.IsNil(err)) } // rewriteReplace rewrites the replace directive for the SDK to use an // absolute path, so that go mod tidy works from any output location. func rewriteReplace(t *testing.T, gomodPath, sdkRoot string) { t.Helper() data, err := os.ReadFile(gomodPath) qt.Assert(t, qt.IsNil(err)) re := regexp.MustCompile(`(?m)^replace github\.com/antithesishq/antithesis-sdk-go => .+$`) updated := re.ReplaceAllString(string(data), "replace github.com/antithesishq/antithesis-sdk-go => "+sdkRoot) if string(data) == updated { t.Fatalf("replace directive not found in %s", gomodPath) } qt.Assert(t, qt.IsNil(os.WriteFile(gomodPath, []byte(updated), 0644))) } // Normalization regexes. var ( notifierHashRe = regexp.MustCompile(`z[0-9a-f]{12}\b`) symbolHashRe = regexp.MustCompile(`go-[0-9a-f]{12}\b`) sdkVersionRe = regexp.MustCompile(`antithesis-sdk-go v[\d.]+`) goVersionRe = regexp.MustCompile(`(?m)^go \d+\.\d+(?:\.\d+)?$`) absPathRe = regexp.MustCompile(`/[^\t\n]*/(?:testdata/input|input)/`) instrumentorRe = regexp.MustCompile(`# instrumentor = .+`) sdkReplaceRe = regexp.MustCompile(`(?m)^replace github\.com/antithesishq/antithesis-sdk-go => .+$`) ) func normalizeContent(s string) string { s = notifierHashRe.ReplaceAllString(s, "zHASH") s = symbolHashRe.ReplaceAllString(s, "go-HASH") s = sdkVersionRe.ReplaceAllString(s, "antithesis-sdk-go vX.Y.Z") s = goVersionRe.ReplaceAllString(s, "go X.Y.Z") s = absPathRe.ReplaceAllString(s, "INPUT_DIR/") s = instrumentorRe.ReplaceAllString(s, "# instrumentor = INSTRUMENTOR") s = sdkReplaceRe.ReplaceAllString(s, "replace github.com/antithesishq/antithesis-sdk-go => SDK_ROOT") return s } golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/instrumentor/000077500000000000000000000000001516252053700335465ustar00rootroot00000000000000file_instrumentor.go000066400000000000000000001143601516252053700375730ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/instrumentorpackage instrumentor import ( "fmt" "go/ast" "go/format" "go/parser" "go/token" "os" "regexp" "sort" "strconv" "strings" "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/common" "golang.org/x/tools/go/ast/astutil" ) // InstrumentationPackageAlias will be used to prevent any collisions // between possible other packages named "instrumentation". Underscore // characters are considered bad style, which is why I'm using them: // a collision is less likely. const InstrumentationPackageAlias = "__antithesis_instrumentation__" // AntithesisCallbackFunction is the name of the instrumentor-generated // callback function that delegates to wrapper.Notify() with the correct // arguments. Multiple definitions of this function will lead to a (desirable) // compile-time error. const AntithesisCallbackFunction = "Notify" var compilationRelevantCommentRegex, _ = regexp.Compile(`(?sm)^\s*(go:|\+build)`) // Capitalized struct items are accessed outside this file type CoverageInstrumentor struct { GoInstrumentor *Instrumentor SymTable *SymbolTable logWriter *common.LogWriter UsingSymbols string NotifierPackage string PreviousEdge int FilesInstrumented int FilesSkipped int } type NotifierInfo struct { logWriter *common.LogWriter InstrumentationPackageName string SymbolTableName string NotifierPackage string EdgeCount int } func (cI *CoverageInstrumentor) WriteNotifierSource(notifierDir string, edge_count int) { if cI.GoInstrumentor == nil { return } notifierInfo := NotifierInfo{ InstrumentationPackageName: common.InstrumentationPackageName(), SymbolTableName: cI.UsingSymbols, EdgeCount: edge_count, NotifierPackage: cI.NotifierPackage, logWriter: common.GetLogWriter(), } GenerateNotifierSource(notifierDir, ¬ifierInfo) } func (cI *CoverageInstrumentor) InstrumentFile(file_name string) string { if cI.GoInstrumentor == nil { return "" } if cI.logWriter == nil { cI.logWriter = common.GetLogWriter() } var err error instrumented := "" cI.logWriter.Printf("Instrumenting %s", file_name) cI.PreviousEdge = cI.GoInstrumentor.CurrentEdge if instrumented, err = cI.GoInstrumentor.Instrument(file_name); err != nil { cI.logWriter.Printf("Error: File %s produced error %s; simply copying source", file_name, err) return "" } return instrumented } func (cI *CoverageInstrumentor) WrapUp() (edge_count int) { var err error edge_count = 0 if cI.logWriter == nil { cI.logWriter = common.GetLogWriter() } if cI.GoInstrumentor != nil { if err = cI.SymTable.Close(); err != nil { cI.logWriter.Printf("Error Could not close symbol table %s: %s", cI.SymTable.Path, err) } cI.logWriter.Printf("Symbol table: %s", cI.SymTable.Path) edge_count = cI.GoInstrumentor.CurrentEdge } return } func (cI *CoverageInstrumentor) SummarizeWork(numFiles int) { if cI.GoInstrumentor == nil { return } if cI.logWriter == nil { cI.logWriter = common.GetLogWriter() } numFilesSkipped := (numFiles - cI.FilesInstrumented) + cI.FilesSkipped numEdges := cI.GoInstrumentor.CurrentEdge cI.logWriter.Printf("%d '.go' %s instrumented, %d %s skipped, %d %s identified", numFiles, common.Pluralize(numFiles, "file"), numFilesSkipped, common.Pluralize(numFilesSkipped, "file"), numEdges, common.Pluralize(numEdges, "edge")) } // IsFunctionExported checks the comments preceding a function declaration // for all known formats of export directive. func IsFunctionExported(group *ast.CommentGroup, name string) bool { if group == nil { return false } // No characters may precede or follow the directive. exportDeclaration := "//export " + name for _, comment := range group.List { if comment.Text == exportDeclaration { return true } } return false } // ExportsFunctions warns the caller that the the .go file includes // export directives in comments, which AST-rewriting may damage. func ExportsFunctions(file *ast.File, fset *token.FileSet) bool { foundExport := false finder := func(cursor *astutil.Cursor) bool { n := cursor.Node() switch n := n.(type) { case *ast.FuncDecl: if IsFunctionExported(n.Doc, n.Name.Name) { foundExport = true // Stop recursion. return false } } // By default, continue recursing. return true } astutil.Apply(file, finder, nil) return foundExport } // HasLinkname lets us exclude .go files that interact with other // languages. func HasLinkname(file *ast.File, fset *token.FileSet) bool { foundLinkname := false finder := func(cursor *astutil.Cursor) bool { n := cursor.Node() switch n := n.(type) { case *ast.FuncDecl: if n.Doc != nil { for _, comment := range n.Doc.List { if strings.Contains(comment.Text, "go:linkname") { foundLinkname = true return false } } } } return true } astutil.Apply(file, finder, nil) return foundLinkname } // Check to see if this particular node represents a something which requires // runtime-generated file names. If this is the case, we can't instrument this // because we have to statically set the path in the comments, and there's no way // to simultaneously: // // 1) Use //line directives to set the line numbers; and // 2) Let the runtime set the absolute/relative file path. // // The primary offenders are the runtime.(Caller|Callers) functions. See // https://github.com/golang/go/issues/26207 // for more details. func RequiresFileNameOrLineNumber(n ast.Node, fset *token.FileSet) bool { if n == nil { return false } call, callOk := n.(*ast.CallExpr) if !callOk { return false } if call.Fun == nil { return false } sel, selOk := call.Fun.(*ast.SelectorExpr) if !selOk { return false } if sel.X == nil || sel.Sel == nil { return false } x, xOk := sel.X.(*ast.Ident) if !xOk { return false } return x.Name == "runtime" && strings.Contains(sel.Sel.Name, "Caller") } func IsLineDirectiveCompatible(file *ast.File, fset *token.FileSet) bool { requiresFileNameOrLineNumber := false finder := func(cursor *astutil.Cursor) bool { n := cursor.Node() if RequiresFileNameOrLineNumber(n, fset) { requiresFileNameOrLineNumber = true return false } return true } astutil.Apply(file, finder, nil) return !requiresFileNameOrLineNumber } // The parser will strip compiler directives from CommentGroup.Text(), // so we need a separate loop to look for them. func commentContainsDirective(group *ast.CommentGroup) bool { for _, comment := range group.List { c := comment.Text //-style comment (no newline at the end) if c[1] == '/' { c = c[2:] if isDirective(c) { return true } } } // TODO It's possible that the above code suffices, but we can't know // that without more investigation. return compilationRelevantCommentRegex.MatchString(group.Text()) } func runeInclusive(r, from, to rune) bool { if r < from || r > to { return false } return true } // From go/ast/ast.go func isDirective(c string) bool { // TODO: Not sure if line directives may affect instrumentation so excluding // such comments for now if strings.HasPrefix(c, "line ") { return false } // "//line " is a line directive. // "//extern " is for gccgo. // "//export " is for cgo. // (The // has been removed.) if strings.HasPrefix(c, "line ") || strings.HasPrefix(c, "extern ") || strings.HasPrefix(c, "export ") { return true } // "//[a-z0-9]+:[a-z0-9]" // (The // has been removed.) colon := strings.Index(c, ":") if colon <= 0 || colon+1 >= len(c) { return false } for i := 0; i <= colon+1; i++ { if i == colon { continue } b := c[i] isValidRune := runeInclusive(rune(b), 'a', 'z') || runeInclusive(rune(b), '0', '9') if !isValidRune { return false } } return true } // Create a //line directive that the caller can add to the comments for the file/maybe associated with the Node. // Note that we can insert these even between go:embed directives and the variables into which Go will embed // the resource: https://pkg.go.dev/embed#hdr-Directives // "Only blank lines and ‘//’ line comments are permitted between the directive and the declaration." func (instrumentor *Instrumentor) createLineDirective(lineNumber int, node *ast.Node) *ast.CommentGroup { file := instrumentor.fset.File((*node).Pos()) p := instrumentor.fset.Position((*node).Pos()) currLine := p.Line if currLine == 1 { instrumentor.logWriter.Printf("Skipping inserting line position comment at very start of file") return nil } lineStartPos := file.LineStart(p.Line) if instrumentor.logWriter.VerboseLevel(2) { instrumentor.logWriter.Printf("Creating comment for node @ %v start of line %d at %v", (*node).Pos(), p.Line, lineStartPos) } if (*node).Pos() == lineStartPos { // This node is actually at the start of the line, so move the position back one to make sure there's no conflict. // Also, set the original lineStartPos to true in the map just to make sure we don't have another item on the same // line just re-create the problem. instrumentor.posLines[lineStartPos] = true lineStartPos-- } if _, ok := instrumentor.posLines[lineStartPos]; ok { // If we've already dropped a //line directive at this position in the file, don't create another one. return nil } // Tag that we've created a //line directive for this spot in the file. instrumentor.posLines[lineStartPos] = true newComment := ast.Comment{Text: fmt.Sprintf("\n//line %s:%d", instrumentor.shortName, int(lineNumber)), Slash: lineStartPos} commentGroup := ast.CommentGroup{List: []*ast.Comment{&newComment}} return &commentGroup } // TrimComments uses the CommentMap structure to discard // all comments not relevant to compilation. func (instrumentor *Instrumentor) TrimComments(path string, file *ast.File) { var commentGroups []*ast.CommentGroup commentMap := ast.NewCommentMap(instrumentor.fset, file, file.Comments) // We can't iterate over this hash map, because we need to // encounter the comments in file order. So we'll walk the // AST once. stripper := func(cursor *astutil.Cursor) bool { node := cursor.Node() groups := commentMap[node] switch n := node.(type) { // Node types with no comments can be skipped. // Only a few node types may retain their comments, // in case they have go: or CGO directives. case *ast.AssignStmt: case *ast.BasicLit: case *ast.Comment: case *ast.CommentGroup: case *ast.DeclStmt: case *ast.Field: n.Doc = nil n.Comment = nil case *ast.ExprStmt: case *ast.File: // This applies to +build directives. // See https://golang.org/cmd/go/#hdr-Build_constraints // Package documentation appears in the Doc field *and* // in the Comments field. for _, group := range groups { if commentContainsDirective(group) { commentGroups = append(commentGroups, group) } } case *ast.ForStmt: case *ast.FuncDecl: text := n.Doc.Text() if strings.Contains(text, "go:") { p := instrumentor.fset.Position(n.Pos()) instrumentor.logWriter.Printf("Warning: Function %s, file %s, line %d uses a go: directive. This file may have to be excluded from instrumentation.", n.Name.Name, path, p.Line) } n.Doc = nil case *ast.GenDecl: if n.Tok == token.IMPORT && n.Doc != nil && len(n.Specs) == 1 { // import "C" will have len(Specs) == 1 spec := n.Specs[0].(*ast.ImportSpec) if spec.Path.Value == "\"C\"" { p := instrumentor.fset.Position(spec.Pos()) instrumentor.logWriter.Printf("Warning: File %s, line %d imports a C declaration. This file may have to be excluded from instrumentation.", path, p.Line) commentGroups = append(commentGroups, n.Doc) } } else { n.Doc = nil // This code exists for the sake of go:embed. for _, group := range groups { if commentContainsDirective(group) { commentGroups = append(commentGroups, group) } } } case *ast.Ident: case *ast.IfStmt: case *ast.ImportSpec: n.Doc = nil n.Comment = nil case *ast.LabeledStmt: case *ast.RangeStmt: case *ast.ReturnStmt: case *ast.ValueSpec: // This code exists for the sake of go:embed. for _, group := range groups { if commentContainsDirective(group) { commentGroups = append(commentGroups, group) } } default: if instrumentor.logWriter.VerboseLevel(2) { instrumentor.logWriter.Printf("No comment revision for AST node of type %T\n", node) } } return true } // We don't need to output; we simply mutated the input AST's comments. astutil.Apply(file, stripper, nil) file.Comments = commentGroups } type functionLiteralFinder token.Pos func (f *functionLiteralFinder) Visit(node ast.Node) (w ast.Visitor) { if f.found() { return nil // Prune search. } switch n := node.(type) { case *ast.FuncLit: *f = functionLiteralFinder(n.Body.Lbrace) return nil // Prune search. } return f } func (f *functionLiteralFinder) found() bool { return token.Pos(*f) != token.NoPos } func hasFuncLiteral(n ast.Node) (bool, token.Pos) { if n == nil { return false, 0 } var literal functionLiteralFinder ast.Walk(&literal, n) return literal.found(), token.Pos(literal) } // Instrumentor *is* the Antithesis Go source-code instrumentor. type Instrumentor struct { nodeLines map[string]int posLines map[token.Pos]bool logWriter *common.LogWriter fset *token.FileSet SymbolTable *SymbolTable typeCounts map[string]int fullName string pkg string shortName string basePath string ShimPkg string funcStack Stack currPos []string comments []*ast.CommentGroup nodeStack Stack CurrentEdge int addLines bool } // CreateInstrumentor is the factory method. func CreateInstrumentor(basePath string, shimPkg string, table *SymbolTable) *Instrumentor { if len(basePath) > 0 { basePath = basePath + "/" } result := &Instrumentor{ basePath: basePath, fset: token.NewFileSet(), ShimPkg: shimPkg, SymbolTable: table, typeCounts: map[string]int{}, nodeLines: map[string]int{}, currPos: make([]string, 0), posLines: map[token.Pos]bool{}, logWriter: common.GetLogWriter(), } return result } // TODO: (justin.moore) See if we can get away with just re-parsing the file in-memory. func (instrumentor *Instrumentor) writeSource(source, path string) error { // Any errors here are fatal anyway, so I'm not checking. f, e := os.Create(path) if e != nil { instrumentor.logWriter.Printf("Warning: Could not create %s", path) return e } defer f.Close() _, e = f.WriteString(source) if e != nil { instrumentor.logWriter.Printf("Warning: Could not write instrumented source to %s", path) return e } if instrumentor.logWriter.VerboseLevel(1) { instrumentor.logWriter.Printf("Wrote instrumented output to %s", path) } return nil } // Instrument inserts calls to the Golang bridge to the Antithesis fuzzer. // Errors should be logged, but are generally not fatal, since the input // file can simply be copied to the output uninstrumented. If a file contains // no executable code (i.e. contains only variable declarations, exports, // or imports, an empty string is returned, so that the caller can simply // copy the input file. // TODO Return a * to a string, rather that returning the empty string to // signal "I didn't instrument this input." func (instrumentor *Instrumentor) Instrument(path string) (string, error) { var bytes []byte var e error var f *ast.File if bytes, e = os.ReadFile(path); e != nil { return "", e } instrumentor.fullName = path instrumentor.shortName = strings.TrimPrefix(path, instrumentor.basePath) startingEdge := instrumentor.CurrentEdge sourceCode := string(bytes) if f, e = parser.ParseFile(instrumentor.fset, path, sourceCode, parser.ParseComments); e != nil { return "", e } if ExportsFunctions(f, instrumentor.fset) { instrumentor.logWriter.Printf("File %s exports functions, and will not be instrumented", path) return "", nil } if HasLinkname(f, instrumentor.fset) { instrumentor.logWriter.Printf("File %s exports linknames, and will not be instrumented", path) return "", nil } instrumentor.TrimComments(path, f) // The first pass over the code. We're not adding lines, we're inserting the instrumentation callbacks and // taking note of where the various ast.Node objects are in the file. instrumentor.addLines = false instrumentor.comments = f.Comments instrumentor.resetTypeCounts(true) ast.Walk(instrumentor, f) f.Comments = instrumentor.comments if instrumentor.CurrentEdge == startingEdge { if instrumentor.logWriter.VerboseLevel(1) { instrumentor.logWriter.Printf("File %s has no code to be instrumented, and will simply be copied", path) } return "", nil } // If there's something in here which requires either file names or line directives to be set // at runtime (or otherwise is incompatible with static file/line directives), instrument but // do not add line annotations. if !IsLineDirectiveCompatible(f, instrumentor.fset) { instrumentor.logWriter.Printf("File %s has functions which are incompatible with //line directives. Will be instrumented but not //line-annotated.", path) // Note that we actually insert the instrumentation callback here. if sourceCode, e = instrumentor.formatInstrumentedAst(path, f, true); e != nil { return "", e } return sourceCode, nil } // Write the new AST out to a temp file on disk. This means that when we re-parse the file, it will // look like a completely new file and we don't have to worry about any state carrying through from // one parse to another. However, do not add in the import shim (the final 'false' parameter). iPath := path + ".instrumented-only.go" if sourceCode, e = instrumentor.formatInstrumentedAst(iPath, f, false); e != nil { return "", e } instrumentor.writeSource(sourceCode, iPath) if f, e = parser.ParseFile(instrumentor.fset, iPath, sourceCode, parser.ParseComments); e != nil { os.Remove(iPath) return "", e } // The second pass through the file. Add in the line directives. Reset the type counts, since we've // cached the mapping of node identifiers to the line on which they were originally placed. instrumentor.addLines = true instrumentor.comments = f.Comments instrumentor.resetTypeCounts(false) ast.Walk(instrumentor, f) sortComments(instrumentor.comments) f.Comments = instrumentor.comments // Create a string version of the final AST, and add in the shim (the final 'true' param). if sourceCode, e := instrumentor.formatInstrumentedAst(path, f, true); e == nil { os.Remove(iPath) return sourceCode, nil } // TODO What are we doing with the error value above? os.Remove(iPath) return "", nil } func (instrumentor *Instrumentor) resetTypeCounts(full bool) { instrumentor.typeCounts = map[string]int{} instrumentor.currPos = make([]string, 0) if full { // Clear out any state from any previous files. instrumentor.nodeLines = map[string]int{} } } func (instrumentor *Instrumentor) pushType(node ast.Node) { t := fmt.Sprintf("%T", node) count, ok := instrumentor.typeCounts[t] if !ok { count = 0 } // This will create an identifier of the form ${nodeType}@${index}, indicating the type of the node // and the number of other nodes in the AST which have had this type. E.g., // - *ast.AssignStmt@3 (the 4th assignment statement) // - *ast.Ident@24 (the 25th identifier) ts := fmt.Sprintf("%s@%d", t, count) // Append to the depth-first list of nodes we've traversed to get here. instrumentor.currPos = append(instrumentor.currPos, ts) instrumentor.typeCounts[t] = count + 1 } func (instrumentor *Instrumentor) popType() { if len(instrumentor.currPos) == 0 { return } instrumentor.currPos = instrumentor.currPos[:len(instrumentor.currPos)-1] } // Get a string representing the current position in the AST, using the node identifiers defined above. E.g., // // - *ast.File@0|*ast.FuncDecl@0|*ast.BlockStmt@0|*ast.AssignStmt@3 // this file | 1st function | 1st fn block | 4th assignment statement in the file // // This allows us to uniquely identify any node in the AST based on a deterministic depth-first search going // from the top of the file to the bottom. func (instrumentor *Instrumentor) currentPath() string { if len(instrumentor.currPos) == 0 { return "" } return strings.Join(instrumentor.currPos, "|") } // Stash the original line number associated with this particular node. We do that by mapping a unique // node identifier -- where it is in the AST, per our deterministic depth-first search -- to the line // number of that node in the original version of the file. func (instrumentor *Instrumentor) collectLine(node ast.Node) { path := instrumentor.currentPath() if len(path) == 0 { return } // The first Ident will pretty much always be the package name. Don't add a //line directive // since we'll likely get that in the wrong place, due to the "Package" object being // disconnected in the AST from the package name. if path == "*ast.File@0|*ast.Ident@0" { if instrumentor.logWriter.VerboseLevel(2) { instrumentor.logWriter.Printf("Skipping package name path %s for node (%T:%v)", path, node, node) } return } // For certain types of nodes we will not create line directives, therefore we can // just not collect the lines associated with those nodes. switch n := node.(type) { case *ast.File: return case *ast.CommentGroup, *ast.Comment: return case *ast.GenDecl: if n.Tok == token.IMPORT { // Don't annotate import statements return } } // Get where this node is in the original version of the file. p := instrumentor.fset.Position(node.Pos()) // Map the node to the original line number. if instrumentor.logWriter.VerboseLevel(3) { instrumentor.logWriter.Printf("collectLine(%T:%v:%s) => %d", node, node, path, p.Line) } instrumentor.nodeLines[path] = p.Line } // Given our current position in the AST, on which line number was this node located // in the original version of the file? If we don't know (e.g., path is empty, or we // didn't record the position for whatever reason) return -1. func (instrumentor *Instrumentor) getOriginalLine() int { path := instrumentor.currentPath() if len(path) == 0 { return -1 } if line, ok := instrumentor.nodeLines[path]; ok { return line } return -1 } func (instrumentor *Instrumentor) VisitAndInstrument(node ast.Node) ast.Visitor { if node == nil { instrumentor.popType() top, _ := instrumentor.nodeStack.Pop() if decl, isDecl := top.(*ast.FuncDecl); isDecl { if instrumentor.logWriter.VerboseLevel(2) { instrumentor.logWriter.Printf("AddCallbacks Popping function %s", decl.Name.Name) } instrumentor.funcStack.Pop() } else { if instrumentor.logWriter.VerboseLevel(2) { instrumentor.logWriter.Printf("AddCallbacks Popping node: %v (%T)", top, top) } } return instrumentor } if isInstrumentationCallback(node) { // It is possible for us to start traversing nodes that we've inserted ahead of ourselves, // so skip over those since we're not going to instrument the instrumentation, AND it will // throw off our accounting needed for collecting line numbers. return nil } instrumentor.pushType(node) instrumentor.collectLine(node) switch n := node.(type) { case *ast.FuncDecl: if n.Name.String() == "init" { // Don't instrument init functions. // They run regardless of what we do, so it is just noise. instrumentor.popType() return nil } case *ast.GenDecl: if n.Tok != token.VAR { instrumentor.popType() return nil // constants and types are not interesting } case *ast.BlockStmt: // If it's a switch or select, the body is a list of case clauses; don't tag the block itself. if len(n.List) > 0 { switch n.List[0].(type) { case *ast.CaseClause: // switch for _, n := range n.List { clause := n.(*ast.CaseClause) clause.Body = instrumentor.instrumentEdge(clause.Pos(), clause.End(), clause.Body, false) } return instrumentor case *ast.CommClause: // select for _, n := range n.List { clause := n.(*ast.CommClause) clause.Body = instrumentor.instrumentEdge(clause.Pos(), clause.End(), clause.Body, false) } return instrumentor } } n.List = instrumentor.instrumentEdge(n.Lbrace, n.Rbrace+1, n.List, true) // +1 to step past closing brace. case *ast.IfStmt: if n.Init != nil { ast.Walk(instrumentor, n.Init) } if n.Cond != nil { ast.Walk(instrumentor, n.Cond) } ast.Walk(instrumentor, n.Body) if n.Else == nil { // Add else because we want coverage for "not taken". n.Else = &ast.BlockStmt{ Lbrace: n.Body.End(), Rbrace: n.Body.End(), } } switch stmt := n.Else.(type) { case *ast.IfStmt: block := &ast.BlockStmt{ Lbrace: n.Body.End(), // Start at end of the "if" block so the covered part looks like it starts at the "else". List: []ast.Stmt{stmt}, Rbrace: stmt.End(), } n.Else = block case *ast.BlockStmt: stmt.Lbrace = n.Body.End() // Start at end of the "if" block so the covered part looks like it starts at the "else". default: instrumentor.logWriter.Fatalf("Unexpected node type in if : %v (%T)", n, n) } ast.Walk(instrumentor, n.Else) instrumentor.popType() return nil case *ast.ForStmt: // TODO: handle increment statement case *ast.SelectStmt: // Don't annotate an empty select - creates a syntax error. if n.Body == nil || len(n.Body.List) == 0 { instrumentor.popType() return nil } case *ast.SwitchStmt: hasDefault := false if n.Body == nil { n.Body = new(ast.BlockStmt) } for _, s := range n.Body.List { if cas, ok := s.(*ast.CaseClause); ok && cas.List == nil { hasDefault = true break } } if !hasDefault { // Add default case to get additional coverage. n.Body.List = append(n.Body.List, &ast.CaseClause{}) } // Don't annotate an empty switch - creates a syntax error. if n.Body == nil || len(n.Body.List) == 0 { instrumentor.popType() return nil } case *ast.TypeSwitchStmt: // Don't annotate an empty type switch - creates a syntax error. // TODO: add default case if n.Body == nil || len(n.Body.List) == 0 { instrumentor.popType() return nil } case *ast.BinaryExpr: if n.Op == token.LAND || n.Op == token.LOR { // Expand the right expression to a comparison with the intrinsic "true". Copy its position to these new nodes. compareYToTrue := &ast.BinaryExpr{X: n.Y, OpPos: n.Y.End(), Op: token.EQL, Y: ast.NewIdent("true")} // Wrap this comparison in a closure. closureWithInstrumentation := &ast.FuncLit{ Type: &ast.FuncType{Results: &ast.FieldList{List: []*ast.Field{{Type: ast.NewIdent("bool")}}}}, Body: &ast.BlockStmt{Lbrace: n.Y.End(), List: []ast.Stmt{&ast.ReturnStmt{Results: []ast.Expr{compareYToTrue}}}, Rbrace: n.OpPos}, } closureCallExpression := &ast.CallExpr{ Lparen: n.Y.End(), Fun: closureWithInstrumentation, Rparen: n.Y.End(), } // We have seen cases in which the value of this logical expression cannot be passed to a function // that takes a specialized Boolean type, and so the instrumented code cannot be compiled. This // comparison to "true" gets us around this oddity of the Go type system (or a bug in the compiler). compareClosureInvocationToTrue := &ast.BinaryExpr{X: closureCallExpression, OpPos: n.Y.End(), Op: token.EQL, Y: ast.NewIdent("true")} n.Y = compareClosureInvocationToTrue } case *ast.BadExpr: instrumentor.logWriter.Fatalf("Invalid input: %v (%T)", node, node) case *ast.BadDecl: instrumentor.logWriter.Fatalf("Invalid input: %v (%T)", node, node) } // If nil is returned, the children of the current node will not be visited. Now push the node so we can pop it later. if decl, isDecl := node.(*ast.FuncDecl); isDecl { if instrumentor.logWriter.VerboseLevel(2) { instrumentor.logWriter.Printf("AddCallbacks Entering function %s", decl.Name.Name) } instrumentor.funcStack.Push(node) } else { if instrumentor.logWriter.VerboseLevel(2) { instrumentor.logWriter.Printf("AddCallbacks Pushing node: %v (%T)", node, node) } } instrumentor.nodeStack.Push(node) return instrumentor } func (instrumentor *Instrumentor) VisitAndAddLines(node ast.Node) ast.Visitor { if node == nil { instrumentor.popType() top, _ := instrumentor.nodeStack.Pop() if decl, isDecl := top.(*ast.FuncDecl); isDecl { if instrumentor.logWriter.VerboseLevel(2) { instrumentor.logWriter.Printf("AddLines Popping function %s", decl.Name.Name) } instrumentor.funcStack.Pop() } else { if instrumentor.logWriter.VerboseLevel(2) { instrumentor.logWriter.Printf("AddLines Popping node: %v (%T)", top, top) } } return instrumentor } if isInstrumentationCallback(node) { return nil } instrumentor.pushType(node) lineNum := instrumentor.getOriginalLine() if lineNum > 0 { comment := instrumentor.createLineDirective(lineNum, &node) if comment != nil { if instrumentor.logWriter.VerboseLevel(3) { instrumentor.logWriter.Printf("Created line directive for %s line %d", instrumentor.currentPath(), lineNum) } instrumentor.comments = append(instrumentor.comments, comment) } else { if instrumentor.logWriter.VerboseLevel(3) { instrumentor.logWriter.Printf("Not creating line directive for line %d path %s", lineNum, instrumentor.currentPath()) } } } else { if instrumentor.logWriter.VerboseLevel(3) { instrumentor.logWriter.Printf("No line number available for %v=%v", node, instrumentor.currentPath()) } } switch n := node.(type) { case *ast.ExprStmt: case *ast.Ident: case *ast.FuncDecl: if n.Name.String() == "init" { instrumentor.popType() return nil } case *ast.GenDecl: if n.Tok != token.VAR { instrumentor.popType() return nil // constants and types have nothing under them not interesting } case *ast.IfStmt: if n.Init != nil { ast.Walk(instrumentor, n.Init) } if n.Cond != nil { ast.Walk(instrumentor, n.Cond) } ast.Walk(instrumentor, n.Body) if n.Else != nil { ast.Walk(instrumentor, n.Else) } instrumentor.popType() return nil case *ast.SelectStmt: // Don't visit an empty select if n.Body == nil || len(n.Body.List) == 0 { instrumentor.popType() return nil } case *ast.SwitchStmt: // Don't visit an empty switch if n.Body == nil || len(n.Body.List) == 0 { instrumentor.popType() return nil } case *ast.TypeSwitchStmt: // Don't visit an empty type switch if n.Body == nil || len(n.Body.List) == 0 { instrumentor.popType() return nil } } // If nil is returned, the children of the current node will not be visited. Now push the node so we can pop it later. if decl, isDecl := node.(*ast.FuncDecl); isDecl { if instrumentor.logWriter.VerboseLevel(2) { instrumentor.logWriter.Printf("AddLines Entering function %s", decl.Name.Name) } instrumentor.funcStack.Push(node) } else { if instrumentor.logWriter.VerboseLevel(2) { instrumentor.logWriter.Printf("AddLines Pushing node: %v (%T)", node, node) } } instrumentor.nodeStack.Push(node) return instrumentor } // Visit is part of the FileWalker interface. // TODO: (justin.moore) See how difficult it would be to merge the Visit sub-functions back into a // single Visit() function, and just switch on control flow based on the addLines boolean, rather // than duplicating most of the switch statement in each function. func (instrumentor *Instrumentor) Visit(node ast.Node) ast.Visitor { if instrumentor.logWriter.VerboseLevel(2) { instrumentor.logWriter.Printf("Visit(%v, %T:%v => %T:%v)", instrumentor.addLines, &node, &node, node, node) } if instrumentor.addLines { return instrumentor.VisitAndAddLines(node) } else { return instrumentor.VisitAndInstrument(node) } } func (instrumentor *Instrumentor) statementBoundary(s ast.Stmt) token.Pos { // Control flow statements are easy. switch s := s.(type) { case *ast.BlockStmt: // Treat blocks like basic blocks to avoid overlapping counters. return s.Lbrace case *ast.IfStmt: found, pos := hasFuncLiteral(s.Init) if found { return pos } found, pos = hasFuncLiteral(s.Cond) if found { return pos } return s.Body.Lbrace case *ast.ForStmt: found, pos := hasFuncLiteral(s.Init) if found { return pos } found, pos = hasFuncLiteral(s.Cond) if found { return pos } found, pos = hasFuncLiteral(s.Post) if found { return pos } return s.Body.Lbrace case *ast.LabeledStmt: return instrumentor.statementBoundary(s.Stmt) case *ast.RangeStmt: found, pos := hasFuncLiteral(s.X) if found { return pos } return s.Body.Lbrace case *ast.SwitchStmt: found, pos := hasFuncLiteral(s.Init) if found { return pos } found, pos = hasFuncLiteral(s.Tag) if found { return pos } return s.Body.Lbrace case *ast.SelectStmt: return s.Body.Lbrace case *ast.TypeSwitchStmt: found, pos := hasFuncLiteral(s.Init) if found { return pos } return s.Body.Lbrace } found, pos := hasFuncLiteral(s) if found { return pos } return s.End() } func (instrumentor *Instrumentor) instrumentEdge(pos, blockEnd token.Pos, list []ast.Stmt, extendToClosingBrace bool) []ast.Stmt { // Special case: make sure we add a counter to an empty block. Can't do this below // or we will add a counter to an empty statement list after, say, a return statement. if len(list) == 0 { return []ast.Stmt{instrumentor.newEdge(pos, blockEnd)} } // We have a block (statement list), but it may have several basic blocks due to the // appearance of statements that affect the flow of control. var newList []ast.Stmt for { // Find first statement that affects flow of control (break, continue, if, etc.). // It will be the last statement of this basic block. var last int end := blockEnd for last = 0; last < len(list); last++ { end = instrumentor.statementBoundary(list[last]) if instrumentor.endsBasicSourceBlock(list[last]) { extendToClosingBrace = false // Block is broken up now. last++ break } } if extendToClosingBrace { end = blockEnd } if pos != end { // Can have no source to cover if e.g. blocks abut. newList = append(newList, instrumentor.newEdge(pos, end)) } newList = append(newList, list[0:last]...) list = list[last:] if len(list) == 0 { break } pos = list[0].Pos() } return newList } func (instrumentor *Instrumentor) endsBasicSourceBlock(s ast.Stmt) bool { switch s := s.(type) { case *ast.BlockStmt: // Treat blocks like basic blocks to avoid overlapping counters. return true case *ast.BranchStmt: return true case *ast.ForStmt: return true case *ast.IfStmt: return true case *ast.LabeledStmt: return instrumentor.endsBasicSourceBlock(s.Stmt) case *ast.RangeStmt: return true case *ast.SwitchStmt: return true case *ast.SelectStmt: return true case *ast.TypeSwitchStmt: return true case *ast.ExprStmt: // Calls to panic change the flow. // We really should verify that "panic" is the predefined function, // but without type checking we can't and the likelihood of it being // an actual problem is vanishingly small. if call, ok := s.X.(*ast.CallExpr); ok { if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "panic" && len(call.Args) == 1 { return true } } } found, _ := hasFuncLiteral(s) return found } func (instrumentor *Instrumentor) newEdge(start, end token.Pos) ast.Stmt { instrumentor.CurrentEdge++ s := instrumentor.fset.Position(start) e := instrumentor.fset.Position(end) maybe_decl, _ := instrumentor.funcStack.Peek() decl, isDecl := maybe_decl.(*ast.FuncDecl) fname := "" if isDecl { fname = decl.Name.Name } err := instrumentor.SymbolTable.WritePosition(SymbolTablePosition{ Path: instrumentor.fullName, Function: fname, StartLine: s.Line, StartColumn: s.Column, EndLine: e.Line, EndColumn: e.Column, Edge: instrumentor.CurrentEdge, }) if err != nil { instrumentor.logWriter.Fatalf("Could not write symbol table line: %s", err.Error()) } idx := &ast.BasicLit{ Kind: token.INT, Value: strconv.Itoa(instrumentor.CurrentEdge), } caller := &ast.SelectorExpr{ X: ast.NewIdent(InstrumentationPackageAlias), Sel: ast.NewIdent(AntithesisCallbackFunction), } return &ast.ExprStmt{ X: &ast.CallExpr{ Fun: caller, Args: []ast.Expr{idx}, }, } } // In the AST we'll see a SelectExpr which will have: // - a package equal to InstrumentationPackageAlias // - a function name equal to AntithesisCallbackFunction // // After those the next thing we see should be an integer literal. I.e., // // __antithesis_instrumentation__.Notify(6) // | Package name | Fn |^~ Integer literal // // In the AST that will be: *ast.SelectorExpr:&{__antithesis_instrumentation__ Notify}) // - Selector{X=(*ast.Ident:__antithesis_instrumentation__), Sel=(*ast.Ident:Notify)} // Followed by: *ast.BasicLit:&{18366 INT 1}) // - BasicLit{Kind=(token.Token:INT), Value=(string:1)} func isInstrumentationCallback(n ast.Node) bool { node, isExp := n.(*ast.ExprStmt) if !isExp { return false } if node.X == nil { return false } call, callOk := node.X.(*ast.CallExpr) if !callOk { return false } if call.Fun == nil || call.Args == nil { return false } sel, selOk := call.Fun.(*ast.SelectorExpr) if !selOk { return false } if sel.X == nil || sel.Sel == nil { return false } x, xOk := sel.X.(*ast.Ident) if !xOk { return false } return x.Name == InstrumentationPackageAlias && sel.Sel.Name == AntithesisCallbackFunction } func (instrumentor *Instrumentor) formatInstrumentedAst(inputPath string, astFile *ast.File, addShim bool) (string, error) { if addShim { astutil.AddNamedImport(instrumentor.fset, astFile, InstrumentationPackageAlias, instrumentor.ShimPkg) } writer := strings.Builder{} formatError := format.Node(&writer, instrumentor.fset, astFile) if formatError != nil { instrumentor.logWriter.Printf("Error: Could not write instrumented AST from %s: %v", inputPath, formatError) return "", formatError } source := writer.String() if _, parseError := parser.ParseFile(&token.FileSet{}, inputPath, source, parser.ParseComments); parseError != nil { instrumentor.logWriter.Printf("Error: Instrumented source for %s could not be parsed; simply copying original: %s", inputPath, parseError) return "", parseError } return source, nil } func sortComments(comments []*ast.CommentGroup) { sort.Slice(comments, func(i, j int) bool { // Get the position of the first comment in each. // Based on the documentation of the Golang AST package, we can assume that // the List element will have non-zero length: // https://pkg.go.dev/go/ast#CommentGroup iFirst := comments[i].List[0] jFirst := comments[j].List[0] // The Slash member is the position of "/" starting the comment: // https://pkg.go.dev/go/ast#Comment return iFirst.Slash < jFirst.Slash }) } node_stack.go000066400000000000000000000012601516252053700361270ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/instrumentorpackage instrumentor import ( "go/ast" ) type Stack []ast.Node // IsEmpty: check if stack is empty func (s *Stack) IsEmpty() bool { return len(*s) == 0 } // Push a new integer onto the stack func (s *Stack) Push(x ast.Node) { *s = append(*s, x) } // Pop: remove and return top element of stack, return false if stack is empty func (s *Stack) Pop() (ast.Node, bool) { if s.IsEmpty() { return nil, false } i := len(*s) - 1 x := (*s)[i] *s = (*s)[:i] return x, true } // Peek: return top element of stack, return false if stack is empty func (s *Stack) Peek() (ast.Node, bool) { if s.IsEmpty() { return nil, false } i := len(*s) - 1 x := (*s)[i] return x, true } notifier_output.go000066400000000000000000000031351516252053700372570ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/instrumentorpackage instrumentor import ( "io" "os" "path" "text/template" "github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor/common" ) func GenerateNotifierSource(notifierDir string, notifierInfo *NotifierInfo) { var tmpl *template.Template var err error tmpl = template.New("notifier") if tmpl, err = tmpl.Parse(getNotifierText()); err != nil { panic(err) } var outFile io.Writer if outFile, err = notifierOutputFile(notifierDir, notifierInfo.logWriter); err != nil { panic(err) } if err = tmpl.Execute(outFile, notifierInfo); err != nil { panic(err) } } func getNotifierText() string { const text = `package {{.NotifierPackage}} // ---------------------------------------------------- // Generated by Antithesis instrumentor - do not modify // ---------------------------------------------------- import "{{.InstrumentationPackageName}}" func init() { instrumentation.InitializeModule("{{.SymbolTableName}}", {{.EdgeCount}}) } func Notify(edge int) { instrumentation.Notify(edge) } ` return text } func notifierOutputFile(dir_name string, logWriter *common.LogWriter) (*os.File, error) { output_file_name := path.Join(dir_name, common.GENERATED_NOTIFIER_SOURCE) var file *os.File var err error if file, err = os.OpenFile(output_file_name, os.O_RDWR|os.O_CREATE, 0644); err != nil { file = nil } if file != nil { if err = file.Truncate(0); err != nil { file = nil } } if err == nil { logWriter.Printf("Notifier file: %q\n", output_file_name) } else { logWriter.Printf("Unable to generate Notifier file: %q\n", output_file_name) } return file, err } symbol_table.go000066400000000000000000000102311516252053700364670ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/instrumentorpackage instrumentor import ( "bufio" "fmt" "os" "strings" ) // SymbolTable is the serialization of the // edges that the instrumentor finds and // instruments. type SymbolTable struct { Path string writer symbolTableWriter executable string } type SymbolTablePosition struct { Path string Function string StartLine int StartColumn int EndLine int EndColumn int Edge int } // CreateSymbolTableFile opens an Antithesis-standard .symbols.tsv file on disk. func CreateSymbolTableFile(symbolTablePath, instrumentedModule string) (symbolTable *SymbolTable, err error) { var w *fileSymbolTableWriter if w, err = createFileSymbolTableWriter(symbolTablePath); err != nil { return } // There can be an error if the file has been moved! executable, _ := os.Executable() symbolTable = &SymbolTable{ Path: symbolTablePath, writer: w, executable: executable, } if err = symbolTable.writeHeader(instrumentedModule); err != nil { symbolTable = nil } return } // CreateInMemorySymbolTable creates an in memory symbol table for testing. func CreateInMemorySymbolTable(symbolTablePath, instrumentedModule string) *SymbolTable { w := createInMemorySymbolTableWriter() symbolTable := &SymbolTable{Path: symbolTablePath, writer: w, executable: "goinstrumentor"} symbolTable.writeHeader(instrumentedModule) return symbolTable } // WriteHeader writes the Antithesis-standard symbol table header. func (t *SymbolTable) writeHeader(module string) error { if err := t.writer.WriteLine("# language = Go"); err != nil { return err } if err := t.writer.WriteLine("# instrumentor = " + t.executable); err != nil { return err } if err := t.writer.WriteLine("# module = " + module); err != nil { return err } return t.writer.WriteLine("file\tfunction\tbegin_line\tbegin_column\tend_line\tend_column\taddress") } // WritePosition describes a callback to the Antithesis instrumentation. func (t *SymbolTable) WritePosition(p SymbolTablePosition) error { line := fmt.Sprintf("%s\t%s\t%d\t%d\t%d\t%d\t%d", p.Path, p.Function, p.StartLine, p.StartColumn, p.EndLine, p.EndColumn, p.Edge) return t.writer.WriteLine(line) } // Close closes the underlying file resources. func (t *SymbolTable) Close() error { return t.writer.Close() } func (t *SymbolTable) String() string { return t.writer.String() } // -------------------------------------------------------------------------------- // symbolTableWriter // -------------------------------------------------------------------------------- type symbolTableWriter interface { WriteLine(s string) error Close() error String() string } type fileSymbolTableWriter struct { f *os.File writer *bufio.Writer } type inMemorySymbolTableWriter struct { writer strings.Builder } // -------------------------------------------------------------------------------- // fileSymbolTableWriter // -------------------------------------------------------------------------------- func createFileSymbolTableWriter(name string) (symWriter *fileSymbolTableWriter, err error) { var f *os.File if f, err = os.Create(name); err != nil { return } symWriter = &fileSymbolTableWriter{ f: f, writer: bufio.NewWriter(f), } return } func (w *fileSymbolTableWriter) WriteLine(s string) error { _, err := w.writer.WriteString(s + "\n") if err != nil { return err } return w.writer.Flush() } func (w *fileSymbolTableWriter) Close() error { err := w.writer.Flush() if err != nil { return err } return w.f.Close() } func (fileSymbolTableWriter) String() string { // logger.Fatalf("fileSymbolTableWriter does not support String method") return "" } // -------------------------------------------------------------------------------- // inMemorySymbolTableWriter // -------------------------------------------------------------------------------- func createInMemorySymbolTableWriter() symbolTableWriter { return &inMemorySymbolTableWriter{} } func (w *inMemorySymbolTableWriter) WriteLine(s string) error { _, err := w.writer.WriteString(s + "\n") return err } func (w *inMemorySymbolTableWriter) Close() error { return nil } func (w *inMemorySymbolTableWriter) String() string { return w.writer.String() } golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/testdata/000077500000000000000000000000001516252053700326065ustar00rootroot00000000000000expected_output/000077500000000000000000000000001516252053700357505ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/testdatacustomer/000077500000000000000000000000001516252053700376115ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/testdata/expected_outputantithesis_catalog.go000066400000000000000000000020321516252053700440020ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/testdata/expected_output/customer// Code generated by antithesis-go-instrumentor; DO NOT EDIT. package main // ---------------------------------------------------- // Antithesis LLC Go Instrumentor 0.6.0 // // Assertion Catalog // // Generated on Thu Mar 19 17:22:56 EDT 2026 // ---------------------------------------------------- import "github.com/antithesishq/antithesis-sdk-go/assert" func init() { const condFalse = false const condTrue = true const wasHit = true const notHit = !wasHit const mustBeHit = true const universalTest = "always" const reachabilityTest = "reachability" var noDetails map[string]any = nil // Reachable(message, details) assert.AssertRaw(condTrue, "reached the else branch", noDetails, "example.com/e2e-test", "foo", "main.go", 14, notHit, mustBeHit, reachabilityTest, "Reachable", "reached the else branch") // Always(cond, message, details) assert.AssertRaw(condFalse, "always in main", noDetails, "example.com/e2e-test", "main", "main.go", 20, notHit, mustBeHit, universalTest, "Always", "always in main") } go.mod000066400000000000000000000004141516252053700407160ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/testdata/expected_output/customermodule example.com/e2e-test go 1.24.0 require ( antithesis.notifier/zad602425a68e v0.0.0 github.com/antithesishq/antithesis-sdk-go v0.0.0 ) replace github.com/antithesishq/antithesis-sdk-go => ../../../.. replace antithesis.notifier/zad602425a68e => ../notifier main.go000066400000000000000000000014301516252053700410620ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/testdata/expected_output/customerpackage main import ( "fmt" __antithesis_instrumentation__ "antithesis.notifier/zad602425a68e" "github.com/antithesishq/antithesis-sdk-go/assert" ) //line main.go:9 func foo(b bool) { __antithesis_instrumentation__.Notify(1) //line main.go:10 if b { __antithesis_instrumentation__.Notify(2) //line main.go:11 fmt.Println("b is true") //line main.go:12 } else { __antithesis_instrumentation__.Notify(3) //line main.go:13 fmt.Println("b is false") //line main.go:14 assert.Reachable("reached the else branch", nil) } } //line main.go:18 func main() { __antithesis_instrumentation__.Notify(4) //line main.go:19 fmt.Println("Hello, world!") //line main.go:20 assert.Always(true, "always in main", nil) //line main.go:21 foo(true) //line main.go:22 foo(false) } notifier/000077500000000000000000000000001516252053700375675ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/testdata/expected_outputgo.mod000066400000000000000000000002421516252053700406730ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/testdata/expected_output/notifiermodule antithesis.notifier go 1.25.7 require github.com/antithesishq/antithesis-sdk-go v0.6.0 replace github.com/antithesishq/antithesis-sdk-go => ../../../.. go.sum000066400000000000000000000003111516252053700407150ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/testdata/expected_output/notifiergithub.com/antithesishq/antithesis-sdk-go v0.6.0 h1:v/YViLhFYkZOEEof4AXjD5AgGnGM84YHF4RqEwp6I2g= github.com/antithesishq/antithesis-sdk-go v0.6.0/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= notifier.go000066400000000000000000000006161516252053700417400ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/testdata/expected_output/notifierpackage zad602425a68e // ---------------------------------------------------- // Generated by Antithesis instrumentor - do not modify // ---------------------------------------------------- import "github.com/antithesishq/antithesis-sdk-go/instrumentation" func init() { instrumentation.InitializeModule("go-ad602425a68e.sym.tsv", 4) } func Notify(edge int) { instrumentation.Notify(edge) } symbols/000077500000000000000000000000001516252053700374405ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/testdata/expected_outputgo-ad602425a68e.sym.tsv000066400000000000000000000011031516252053700431360ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/testdata/expected_output/symbols# language = Go # instrumentor = /home/mgibson/.cache/go-build/ca/ca4860825b26207757f57f555f13d604cbff5fe2be2ae801dab5f65ba43647cd-d/antithesis-go-instrumentor # module = go-ad602425a68e file function begin_line begin_column end_line end_column address star_sdk/tools/antithesis-go-instrumentor/testdata/input/main.go foo 9 18 10 7 1 star_sdk/tools/antithesis-go-instrumentor/testdata/input/main.go foo 10 7 12 3 2 star_sdk/tools/antithesis-go-instrumentor/testdata/input/main.go foo 12 3 15 3 3 star_sdk/tools/antithesis-go-instrumentor/testdata/input/main.go main 18 13 23 2 4 golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/testdata/input/000077500000000000000000000000001516252053700337455ustar00rootroot00000000000000go.mod000066400000000000000000000002431516252053700347730ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/testdata/inputmodule example.com/e2e-test go 1.24.0 require github.com/antithesishq/antithesis-sdk-go v0.0.0 replace github.com/antithesishq/antithesis-sdk-go => ../../../.. main.go000066400000000000000000000005341516252053700351430ustar00rootroot00000000000000golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/testdata/inputpackage main import ( "fmt" "github.com/antithesishq/antithesis-sdk-go/assert" ) func foo(b bool) { if b { fmt.Println("b is true") } else { fmt.Println("b is false") assert.Reachable("reached the else branch", nil) } } func main() { fmt.Println("Hello, world!") assert.Always(true, "always in main", nil) foo(true) foo(false) } golang-github-antithesishq-antithesis-sdk-go-0.7.0/tools/antithesis-go-instrumentor/version.txt000066400000000000000000000000421516252053700332170ustar00rootroot00000000000000Antithesis LLC Go Instrumentor %s