cosmos-sdk/simsx/reporter.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
}