saga/saga_generators.go (148 lines of code) (raw):
package saga
import (
"fmt"
"github.com/leanovate/gopter"
"github.com/leanovate/gopter/gen"
log "github.com/sirupsen/logrus"
"github.com/twitter/scoot/common/thrifthelpers"
"github.com/twitter/scoot/scheduler/api/thrift/gen-go/scoot"
"math"
)
//
// Saga Generators contains Generator methods that are useful
// when doing property based testing
//
// Randomly generates an Id that is valid for
// use as a sagaId or taskId
func genId(genParams *gopter.GenParameters) string {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
length := int(genParams.NextUint64()%20) + 1
result := make([]byte, length)
for i := 0; i < length; i++ {
result[i] = chars[genParams.Rng.Intn(len(chars))]
}
return string(result)
}
// Randomly generates a valid SagaState, with a random job def if includeJob
func genSagaState(genParams *gopter.GenParameters, includeJob bool) *SagaState {
sagaId := genId(genParams)
data, _ := gen.SliceOf(gen.UInt8()).Sample()
if !includeJob {
data = []uint8{}
}
job := data.([]byte)
state, err := makeSagaState(sagaId, job)
if err != nil {
log.Info(err)
}
// is saga aborted or not
isAborted := genParams.NextBool()
state.sagaAborted = isAborted
//number of tasks to run in this saga
numTasks := int(genParams.NextUint64() % 100)
for i := 0; i < numTasks; i++ {
taskId := genId(genParams)
flags := TaskStarted
// randomly decide if task has been completed
if genParams.NextBool() {
flags = flags | TaskCompleted
err := genTaskCompletedData(state, taskId, genParams)
if err != nil {
log.Infof(fmt.Sprintf("Error generating complete task data for %s, %s", taskId, err.Error()))
}
}
if isAborted {
// randomly decide if comp tasks have started/completed
if genParams.NextBool() {
flags = flags | CompTaskStarted
if genParams.NextBool() {
flags = flags | CompTaskCompleted
}
}
}
state.taskState[taskId] = flags
}
// check if saga is in completed state then coin flip to decide if we actually log
// the end complete message
isCompleted := true
for _, id := range state.GetTaskIds() {
if state.IsSagaAborted() {
if !(state.IsTaskStarted(id) && state.IsCompTaskStarted(id) && state.IsCompTaskCompleted(id)) {
isCompleted = false
break
}
} else {
if !(state.IsTaskStarted(id) && state.IsTaskCompleted(id)) {
isCompleted = false
break
}
}
}
if isCompleted && genParams.NextBool() {
state.sagaCompleted = true
}
return state
}
func genTaskCompletedData(state *SagaState, taskID string, genParams *gopter.GenParameters) error {
runStatus := scoot.RunStatus{}
runStatus.RunId = taskID
var statusVal scoot.RunStatusState
// status must be complete or higher
statusVal = scoot.RunStatusState(int64(math.Trunc(math.Abs(float64(genParams.NextInt64()%5)))) + 3)
// don't let the status be nil
if &statusVal == nil {
statusVal = scoot.RunStatusState_COMPLETE
} else {
statusVal = scoot.RunStatusState(statusVal)
}
runStatus.Status = statusVal
exitCodeStatus := int32(genParams.NextInt64() % 4)
var ok int32 = 0
var notOk int32 = -1
if exitCodeStatus == 0 {
runStatus.ExitCode = &ok
} else {
runStatus.ExitCode = ¬Ok
}
t1 := fmt.Sprintf("error %d", genParams.NextInt64())
runStatus.Error = &t1
t2 := fmt.Sprintf("error URI %d", genParams.NextInt64())
runStatus.ErrUri = &t2
t3 := fmt.Sprintf("out URI %d", genParams.NextInt64())
runStatus.OutUri = &t3
messageAsBytes, err := thrifthelpers.JsonSerialize(&runStatus)
if err != nil {
return err
}
state.addTaskData(taskID, EndTask, messageAsBytes)
return nil
}
// Generator for a valid SagaId or TaskId
func GenId() gopter.Gen {
return func(genParams *gopter.GenParameters) *gopter.GenResult {
id := genId(genParams)
genResult := gopter.NewGenResult(id, gopter.NoShrinker)
return genResult
}
}
// Generator for a Valid Saga State, with a job def if includeJob
func GenSagaState(includeJob bool) gopter.Gen {
return func(genParams *gopter.GenParameters) *gopter.GenResult {
state := genSagaState(genParams, includeJob)
genResult := gopter.NewGenResult(state, gopter.NoShrinker)
return genResult
}
}
type StateTaskPair struct {
state *SagaState
taskId string
}
func (p StateTaskPair) String() string {
return fmt.Sprintf("{ TaskId: %v, SagaState: %s }", p.taskId, p.state)
}
// Generator for a SagaState and TaskId, returns a StateTaskPair
// SagaState is always valid. TaskId may or may not be part of the saga
func GenSagaStateAndTaskId() gopter.Gen {
return func(genParams *gopter.GenParameters) *gopter.GenResult {
state := genSagaState(genParams, true)
id := genId(genParams)
if genParams.NextBool() {
ids := state.GetTaskIds()
switch len(ids) {
case 0:
//do nothing just use randomly generated id
case 1:
id = ids[0]
default:
index := genParams.NextUint64() % uint64(len(ids))
id = ids[index]
}
}
result := StateTaskPair{
state: state,
taskId: id,
}
genResult := gopter.NewGenResult(result, gopter.NoShrinker)
return genResult
}
}