261 lines
6.9 KiB
Go
261 lines
6.9 KiB
Go
package simsx
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
|
|
)
|
|
|
|
// SimulationReporter is an interface for reporting the result of a simulation run.
|
|
type SimulationReporter interface {
|
|
WithScope(msg sdk.Msg, optionalSkipHook ...SkipHook) SimulationReporter
|
|
Skip(comment string)
|
|
Skipf(comment string, args ...any)
|
|
// IsSkipped returns true when skipped or completed
|
|
IsSkipped() bool
|
|
ToLegacyOperationMsg() simtypes.OperationMsg
|
|
// Fail complete with failure
|
|
Fail(err error, comments ...string)
|
|
// Success complete with success
|
|
Success(msg sdk.Msg, comments ...string)
|
|
// Close returns error captured on fail
|
|
Close() error
|
|
Comment() string
|
|
}
|
|
|
|
var _ SimulationReporter = &BasicSimulationReporter{}
|
|
|
|
type ReporterStatus uint8
|
|
|
|
const (
|
|
undefined ReporterStatus = iota
|
|
skipped ReporterStatus = iota
|
|
completed ReporterStatus = iota
|
|
)
|
|
|
|
func (s ReporterStatus) String() string {
|
|
switch s {
|
|
case skipped:
|
|
return "skipped"
|
|
case completed:
|
|
return "completed"
|
|
default:
|
|
return "undefined"
|
|
}
|
|
}
|
|
|
|
// SkipHook is an interface that represents a callback hook used triggered on skip operations.
|
|
// It provides a single method `Skip` that accepts variadic arguments. This interface is implemented
|
|
// by Go stdlib testing.T and testing.B
|
|
type SkipHook interface {
|
|
Skip(args ...any)
|
|
}
|
|
|
|
var _ SkipHook = SkipHookFn(nil)
|
|
|
|
type SkipHookFn func(args ...any)
|
|
|
|
func (s SkipHookFn) Skip(args ...any) {
|
|
s(args...)
|
|
}
|
|
|
|
type BasicSimulationReporter struct {
|
|
skipCallbacks []SkipHook
|
|
completedCallback func(reporter *BasicSimulationReporter)
|
|
module string
|
|
msgTypeURL string
|
|
|
|
status atomic.Uint32
|
|
|
|
cMX sync.RWMutex
|
|
comments []string
|
|
error error
|
|
|
|
summary *ExecutionSummary
|
|
}
|
|
|
|
// NewBasicSimulationReporter constructor that accepts an optional callback hook that is called on state transition to skipped status
|
|
// A typical implementation for this hook is testing.T or testing.B.
|
|
func NewBasicSimulationReporter(optionalSkipHook ...SkipHook) *BasicSimulationReporter {
|
|
r := &BasicSimulationReporter{
|
|
skipCallbacks: optionalSkipHook,
|
|
summary: NewExecutionSummary(),
|
|
}
|
|
r.completedCallback = func(child *BasicSimulationReporter) {
|
|
r.summary.Add(child.module, child.msgTypeURL, ReporterStatus(child.status.Load()), child.Comment())
|
|
}
|
|
return r
|
|
}
|
|
|
|
// WithScope is a method of the BasicSimulationReporter type that creates a new instance of SimulationReporter
|
|
// with an additional scope specified by the input `msg`. The msg is used to set type, module and binary data as
|
|
// context for the legacy operation.
|
|
// The WithScope method acts as a constructor to initialize state and has to be called before using the instance
|
|
// in DeliverSimsMsg.
|
|
//
|
|
// The method accepts an optional `optionalSkipHook` parameter
|
|
// that can be used to add a callback hook that is triggered on skip operations additional to any parent skip hook.
|
|
// This method returns the newly created
|
|
// SimulationReporter instance.
|
|
func (x *BasicSimulationReporter) WithScope(msg sdk.Msg, optionalSkipHook ...SkipHook) SimulationReporter {
|
|
typeURL := sdk.MsgTypeURL(msg)
|
|
r := &BasicSimulationReporter{
|
|
skipCallbacks: append(x.skipCallbacks, optionalSkipHook...),
|
|
completedCallback: x.completedCallback,
|
|
error: x.error,
|
|
msgTypeURL: typeURL,
|
|
module: sdk.GetModuleNameFromTypeURL(typeURL),
|
|
comments: slices.Clone(x.comments),
|
|
}
|
|
r.status.Store(x.status.Load())
|
|
return r
|
|
}
|
|
|
|
func (x *BasicSimulationReporter) Skip(comment string) {
|
|
x.toStatus(skipped, comment)
|
|
}
|
|
|
|
func (x *BasicSimulationReporter) Skipf(comment string, args ...any) {
|
|
x.Skip(fmt.Sprintf(comment, args...))
|
|
}
|
|
|
|
func (x *BasicSimulationReporter) IsSkipped() bool {
|
|
return ReporterStatus(x.status.Load()) > undefined
|
|
}
|
|
|
|
func (x *BasicSimulationReporter) ToLegacyOperationMsg() simtypes.OperationMsg {
|
|
switch ReporterStatus(x.status.Load()) {
|
|
case skipped:
|
|
return simtypes.NoOpMsg(x.module, x.msgTypeURL, x.Comment())
|
|
case completed:
|
|
x.cMX.RLock()
|
|
err := x.error
|
|
x.cMX.RUnlock()
|
|
if err == nil {
|
|
return simtypes.NewOperationMsgBasic(x.module, x.msgTypeURL, x.Comment(), true)
|
|
} else {
|
|
return simtypes.NewOperationMsgBasic(x.module, x.msgTypeURL, x.Comment(), false)
|
|
}
|
|
default:
|
|
x.Fail(errors.New("operation aborted before msg was executed"))
|
|
return x.ToLegacyOperationMsg()
|
|
}
|
|
}
|
|
|
|
func (x *BasicSimulationReporter) Fail(err error, comments ...string) {
|
|
if !x.toStatus(completed, comments...) {
|
|
return
|
|
}
|
|
x.cMX.Lock()
|
|
defer x.cMX.Unlock()
|
|
x.error = err
|
|
}
|
|
|
|
func (x *BasicSimulationReporter) Success(msg sdk.Msg, comments ...string) {
|
|
if !x.toStatus(completed, comments...) {
|
|
return
|
|
}
|
|
if msg == nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
func (x *BasicSimulationReporter) Close() error {
|
|
x.completedCallback(x)
|
|
x.cMX.RLock()
|
|
defer x.cMX.RUnlock()
|
|
return x.error
|
|
}
|
|
|
|
func (x *BasicSimulationReporter) toStatus(next ReporterStatus, comments ...string) bool {
|
|
oldStatus := ReporterStatus(x.status.Load())
|
|
if oldStatus > next {
|
|
panic(fmt.Sprintf("can not switch from status %s to %s", oldStatus, next))
|
|
}
|
|
if !x.status.CompareAndSwap(uint32(oldStatus), uint32(next)) {
|
|
return false
|
|
}
|
|
x.cMX.Lock()
|
|
newComments := append(x.comments, comments...)
|
|
x.comments = newComments
|
|
x.cMX.Unlock()
|
|
|
|
if oldStatus != skipped && next == skipped {
|
|
prettyComments := strings.Join(newComments, ", ")
|
|
for _, hook := range x.skipCallbacks {
|
|
hook.Skip(prettyComments)
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (x *BasicSimulationReporter) Comment() string {
|
|
x.cMX.RLock()
|
|
defer x.cMX.RUnlock()
|
|
return strings.Join(x.comments, ", ")
|
|
}
|
|
|
|
func (x *BasicSimulationReporter) Summary() *ExecutionSummary {
|
|
return x.summary
|
|
}
|
|
|
|
type ExecutionSummary struct {
|
|
mx sync.RWMutex
|
|
counts map[string]int // module to count
|
|
skipReasons map[string]map[string]int // msg type to reason->count
|
|
}
|
|
|
|
func NewExecutionSummary() *ExecutionSummary {
|
|
return &ExecutionSummary{counts: make(map[string]int), skipReasons: make(map[string]map[string]int)}
|
|
}
|
|
|
|
func (s *ExecutionSummary) Add(module, url string, status ReporterStatus, comment string) {
|
|
s.mx.Lock()
|
|
defer s.mx.Unlock()
|
|
combinedKey := fmt.Sprintf("%s_%s", module, status.String())
|
|
s.counts[combinedKey] += 1
|
|
if status == completed {
|
|
return
|
|
}
|
|
r, ok := s.skipReasons[url]
|
|
if !ok {
|
|
r = make(map[string]int)
|
|
s.skipReasons[url] = r
|
|
}
|
|
r[comment] += 1
|
|
}
|
|
|
|
func (s *ExecutionSummary) String() string {
|
|
s.mx.RLock()
|
|
defer s.mx.RUnlock()
|
|
keys := slices.Sorted(maps.Keys(s.counts))
|
|
var sb strings.Builder
|
|
for _, key := range keys {
|
|
sb.WriteString(fmt.Sprintf("%s: %d\n", key, s.counts[key]))
|
|
}
|
|
if len(s.skipReasons) != 0 {
|
|
sb.WriteString("\nSkip reasons:\n")
|
|
}
|
|
for m, c := range s.skipReasons {
|
|
values := maps.Values(c)
|
|
keys := maps.Keys(c)
|
|
sb.WriteString(fmt.Sprintf("%d\t%s: %q\n", sum(slices.Collect(values)), m, slices.Collect(keys)))
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
func sum(values []int) int {
|
|
var r int
|
|
for _, v := range values {
|
|
r += v
|
|
}
|
|
return r
|
|
}
|