mirror of
https://github.com/rocky-linux/peridot.git
synced 2024-11-27 15:36:25 +00:00
817 lines
37 KiB
Go
817 lines
37 KiB
Go
|
// The MIT License
|
||
|
//
|
||
|
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
|
||
|
//
|
||
|
// Copyright (c) 2020 Uber Technologies, Inc.
|
||
|
//
|
||
|
// 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.
|
||
|
|
||
|
package internal
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"reflect"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/stretchr/testify/mock"
|
||
|
commonpb "go.temporal.io/api/common/v1"
|
||
|
enumspb "go.temporal.io/api/enums/v1"
|
||
|
|
||
|
"go.temporal.io/api/serviceerror"
|
||
|
"go.temporal.io/sdk/converter"
|
||
|
"go.temporal.io/sdk/internal/common/metrics"
|
||
|
"go.temporal.io/sdk/log"
|
||
|
)
|
||
|
|
||
|
type (
|
||
|
// EncodedValues is a type alias used to encapsulate/extract encoded arguments from workflow/activity.
|
||
|
EncodedValues struct {
|
||
|
values *commonpb.Payloads
|
||
|
dataConverter converter.DataConverter
|
||
|
}
|
||
|
|
||
|
// ErrorDetailsValues is a type alias used hold error details objects.
|
||
|
ErrorDetailsValues []interface{}
|
||
|
|
||
|
// WorkflowTestSuite is the test suite to run unit tests for workflow/activity.
|
||
|
WorkflowTestSuite struct {
|
||
|
logger log.Logger
|
||
|
metricsHandler metrics.Handler
|
||
|
contextPropagators []ContextPropagator
|
||
|
header *commonpb.Header
|
||
|
}
|
||
|
|
||
|
// TestWorkflowEnvironment is the environment that you use to test workflow
|
||
|
TestWorkflowEnvironment struct {
|
||
|
mock mock.Mock
|
||
|
impl *testWorkflowEnvironmentImpl
|
||
|
}
|
||
|
|
||
|
// TestActivityEnvironment is the environment that you use to test activity
|
||
|
TestActivityEnvironment struct {
|
||
|
impl *testWorkflowEnvironmentImpl
|
||
|
}
|
||
|
|
||
|
// MockCallWrapper is a wrapper to mock.Call. It offers the ability to wait on workflow's clock instead of wall clock.
|
||
|
MockCallWrapper struct {
|
||
|
call *mock.Call
|
||
|
env *TestWorkflowEnvironment
|
||
|
|
||
|
runFn func(args mock.Arguments)
|
||
|
waitDuration func() time.Duration
|
||
|
}
|
||
|
)
|
||
|
|
||
|
func newEncodedValues(values *commonpb.Payloads, dc converter.DataConverter) converter.EncodedValues {
|
||
|
if dc == nil {
|
||
|
dc = converter.GetDefaultDataConverter()
|
||
|
}
|
||
|
return &EncodedValues{values, dc}
|
||
|
}
|
||
|
|
||
|
// Get extract data from encoded data to desired value type. valuePtr is pointer to the actual value type.
|
||
|
func (b EncodedValues) Get(valuePtr ...interface{}) error {
|
||
|
if !b.HasValues() {
|
||
|
return ErrNoData
|
||
|
}
|
||
|
return b.dataConverter.FromPayloads(b.values, valuePtr...)
|
||
|
}
|
||
|
|
||
|
// HasValues return whether there are values
|
||
|
func (b EncodedValues) HasValues() bool {
|
||
|
return b.values != nil
|
||
|
}
|
||
|
|
||
|
// Get extract data from encoded data to desired value type. valuePtr is pointer to the actual value type.
|
||
|
func (b ErrorDetailsValues) Get(valuePtr ...interface{}) error {
|
||
|
if !b.HasValues() {
|
||
|
return ErrNoData
|
||
|
}
|
||
|
if len(valuePtr) > len(b) {
|
||
|
return ErrTooManyArg
|
||
|
}
|
||
|
for i, item := range valuePtr {
|
||
|
reflect.ValueOf(item).Elem().Set(reflect.ValueOf(b[i]))
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// HasValues return whether there are values.
|
||
|
func (b ErrorDetailsValues) HasValues() bool {
|
||
|
return b != nil && len(b) != 0
|
||
|
}
|
||
|
|
||
|
// NewTestWorkflowEnvironment creates a new instance of TestWorkflowEnvironment. Use the returned TestWorkflowEnvironment
|
||
|
// to run your workflow in the test environment.
|
||
|
func (s *WorkflowTestSuite) NewTestWorkflowEnvironment() *TestWorkflowEnvironment {
|
||
|
return &TestWorkflowEnvironment{impl: newTestWorkflowEnvironmentImpl(s, nil)}
|
||
|
}
|
||
|
|
||
|
// NewTestActivityEnvironment creates a new instance of TestActivityEnvironment. Use the returned TestActivityEnvironment
|
||
|
// to run your activity in the test environment.
|
||
|
func (s *WorkflowTestSuite) NewTestActivityEnvironment() *TestActivityEnvironment {
|
||
|
return &TestActivityEnvironment{impl: newTestWorkflowEnvironmentImpl(s, nil)}
|
||
|
}
|
||
|
|
||
|
// SetLogger sets the logger for this WorkflowTestSuite. If you don't set logger, test suite will create a default logger
|
||
|
// with Debug level logging enabled.
|
||
|
func (s *WorkflowTestSuite) SetLogger(logger log.Logger) {
|
||
|
s.logger = logger
|
||
|
}
|
||
|
|
||
|
// GetLogger gets the logger for this WorkflowTestSuite.
|
||
|
func (s *WorkflowTestSuite) GetLogger() log.Logger {
|
||
|
return s.logger
|
||
|
}
|
||
|
|
||
|
// SetMetricsHandler sets the metrics handler for this WorkflowTestSuite. If you don't set handler, test suite will use
|
||
|
// a noop handler.
|
||
|
func (s *WorkflowTestSuite) SetMetricsHandler(metricsHandler metrics.Handler) {
|
||
|
s.metricsHandler = metricsHandler
|
||
|
}
|
||
|
|
||
|
// SetContextPropagators sets the context propagators for this WorkflowTestSuite. If you don't set context propagators,
|
||
|
// test suite will not use context propagators
|
||
|
func (s *WorkflowTestSuite) SetContextPropagators(ctxProps []ContextPropagator) {
|
||
|
s.contextPropagators = ctxProps
|
||
|
}
|
||
|
|
||
|
// SetHeader sets the headers for this WorkflowTestSuite. If you don't set header, test suite will not pass headers to
|
||
|
// the workflow
|
||
|
func (s *WorkflowTestSuite) SetHeader(header *commonpb.Header) {
|
||
|
s.header = header
|
||
|
}
|
||
|
|
||
|
// RegisterActivity registers activity implementation with TestWorkflowEnvironment
|
||
|
func (t *TestActivityEnvironment) RegisterActivity(a interface{}) {
|
||
|
t.impl.RegisterActivity(a)
|
||
|
}
|
||
|
|
||
|
// RegisterActivityWithOptions registers activity implementation with TestWorkflowEnvironment
|
||
|
func (t *TestActivityEnvironment) RegisterActivityWithOptions(a interface{}, options RegisterActivityOptions) {
|
||
|
t.impl.RegisterActivityWithOptions(a, options)
|
||
|
}
|
||
|
|
||
|
// ExecuteActivity executes an activity. The tested activity will be executed synchronously in the calling goroutinue.
|
||
|
// Caller should use EncodedValue.Get() to extract strong typed result value.
|
||
|
func (t *TestActivityEnvironment) ExecuteActivity(activityFn interface{}, args ...interface{}) (converter.EncodedValue, error) {
|
||
|
return t.impl.executeActivity(activityFn, args...)
|
||
|
}
|
||
|
|
||
|
// ExecuteLocalActivity executes a local activity. The tested activity will be executed synchronously in the calling goroutinue.
|
||
|
// Caller should use EncodedValue.Get() to extract strong typed result value.
|
||
|
func (t *TestActivityEnvironment) ExecuteLocalActivity(activityFn interface{}, args ...interface{}) (val converter.EncodedValue, err error) {
|
||
|
return t.impl.executeLocalActivity(activityFn, args...)
|
||
|
}
|
||
|
|
||
|
// SetWorkerOptions sets the WorkerOptions that will be use by TestActivityEnvironment. TestActivityEnvironment will
|
||
|
// use options of BackgroundActivityContext, MaxConcurrentSessionExecutionSize, and WorkflowInterceptorChainFactories on the WorkerOptions.
|
||
|
// Other options are ignored.
|
||
|
// Note: WorkerOptions is defined in internal package, use public type worker.Options instead.
|
||
|
func (t *TestActivityEnvironment) SetWorkerOptions(options WorkerOptions) *TestActivityEnvironment {
|
||
|
t.impl.setWorkerOptions(options)
|
||
|
return t
|
||
|
}
|
||
|
|
||
|
// SetDataConverter sets data converter.
|
||
|
func (t *TestActivityEnvironment) SetDataConverter(dataConverter converter.DataConverter) *TestActivityEnvironment {
|
||
|
t.impl.setDataConverter(dataConverter)
|
||
|
return t
|
||
|
}
|
||
|
|
||
|
// SetIdentity sets identity.
|
||
|
func (t *TestActivityEnvironment) SetIdentity(identity string) *TestActivityEnvironment {
|
||
|
t.impl.setIdentity(identity)
|
||
|
return t
|
||
|
}
|
||
|
|
||
|
// SetContextPropagators sets context propagators.
|
||
|
func (t *TestActivityEnvironment) SetContextPropagators(contextPropagators []ContextPropagator) *TestActivityEnvironment {
|
||
|
t.impl.setContextPropagators(contextPropagators)
|
||
|
return t
|
||
|
}
|
||
|
|
||
|
// SetHeader sets header.
|
||
|
func (t *TestActivityEnvironment) SetHeader(header *commonpb.Header) {
|
||
|
t.impl.header = header
|
||
|
}
|
||
|
|
||
|
// SetTestTimeout sets the wall clock timeout for this activity test run. When test timeout happen, it means activity is
|
||
|
// taking too long.
|
||
|
func (t *TestActivityEnvironment) SetTestTimeout(idleTimeout time.Duration) *TestActivityEnvironment {
|
||
|
t.impl.testTimeout = idleTimeout
|
||
|
return t
|
||
|
}
|
||
|
|
||
|
// SetHeartbeatDetails sets the heartbeat details to be returned from activity.GetHeartbeatDetails()
|
||
|
func (t *TestActivityEnvironment) SetHeartbeatDetails(details interface{}) {
|
||
|
t.impl.setHeartbeatDetails(details)
|
||
|
}
|
||
|
|
||
|
// SetWorkerStopChannel sets the worker stop channel to be returned from activity.GetWorkerStopChannel(context)
|
||
|
// To test your activity on worker stop, you can provide a go channel with this function and call ExecuteActivity().
|
||
|
// Then call close(channel) to test the activity worker stop logic.
|
||
|
func (t *TestActivityEnvironment) SetWorkerStopChannel(c chan struct{}) {
|
||
|
t.impl.setWorkerStopChannel(c)
|
||
|
}
|
||
|
|
||
|
// RegisterWorkflow registers workflow implementation with the TestWorkflowEnvironment
|
||
|
func (e *TestWorkflowEnvironment) RegisterWorkflow(w interface{}) {
|
||
|
e.impl.RegisterWorkflow(w)
|
||
|
}
|
||
|
|
||
|
// RegisterWorkflowWithOptions registers workflow implementation with the TestWorkflowEnvironment
|
||
|
func (e *TestWorkflowEnvironment) RegisterWorkflowWithOptions(w interface{}, options RegisterWorkflowOptions) {
|
||
|
if len(e.mock.ExpectedCalls) > 0 {
|
||
|
panic("RegisterWorkflow calls cannot follow mock related ones like OnWorkflow or similar")
|
||
|
}
|
||
|
e.impl.RegisterWorkflowWithOptions(w, options)
|
||
|
}
|
||
|
|
||
|
// RegisterActivity registers activity implementation with TestWorkflowEnvironment
|
||
|
func (e *TestWorkflowEnvironment) RegisterActivity(a interface{}) {
|
||
|
e.impl.RegisterActivity(a)
|
||
|
}
|
||
|
|
||
|
// RegisterActivityWithOptions registers activity implementation with TestWorkflowEnvironment
|
||
|
func (e *TestWorkflowEnvironment) RegisterActivityWithOptions(a interface{}, options RegisterActivityOptions) {
|
||
|
if len(e.mock.ExpectedCalls) > 0 {
|
||
|
panic("RegisterActivity calls cannot follow mock related ones like OnActivity or similar")
|
||
|
}
|
||
|
e.impl.RegisterActivityWithOptions(a, options)
|
||
|
}
|
||
|
|
||
|
// SetStartTime sets the start time of the workflow. This is optional, default start time will be the wall clock time when
|
||
|
// workflow starts. Start time is the workflow.Now(ctx) time at the beginning of the workflow.
|
||
|
func (e *TestWorkflowEnvironment) SetStartTime(startTime time.Time) {
|
||
|
e.impl.setStartTime(startTime)
|
||
|
}
|
||
|
|
||
|
// OnActivity setup a mock call for activity. Parameter activity must be activity function (func) or activity name (string).
|
||
|
// You must call Return() with appropriate parameters on the returned *MockCallWrapper instance. The supplied parameters to
|
||
|
// the Return() call should either be a function that has exact same signature as the mocked activity, or it should be
|
||
|
// mock values with the same types as the mocked activity function returns.
|
||
|
// Example: assume the activity you want to mock has function signature as:
|
||
|
// func MyActivity(ctx context.Context, msg string) (string, error)
|
||
|
// You can mock it by return a function with exact same signature:
|
||
|
// t.OnActivity(MyActivity, mock.Anything, mock.Anything).Return(func(ctx context.Context, msg string) (string, error) {
|
||
|
// // your mock function implementation
|
||
|
// return "", nil
|
||
|
// })
|
||
|
// OR return mock values with same types as activity function's return types:
|
||
|
// t.OnActivity(MyActivity, mock.Anything, mock.Anything).Return("mock_result", nil)
|
||
|
//
|
||
|
// Note, when using a method reference with a receiver as an activity, the receiver must be an instance the same as if
|
||
|
// it was being using in RegisterActivity so the parameter types are accurate. In Go, a method reference of
|
||
|
// (*MyStruct).MyFunc makes the first parameter *MyStruct which will not work, whereas a method reference of
|
||
|
// new(MyStruct).MyFunc will.
|
||
|
func (e *TestWorkflowEnvironment) OnActivity(activity interface{}, args ...interface{}) *MockCallWrapper {
|
||
|
fType := reflect.TypeOf(activity)
|
||
|
var call *mock.Call
|
||
|
switch fType.Kind() {
|
||
|
case reflect.Func:
|
||
|
fnType := reflect.TypeOf(activity)
|
||
|
if err := validateFnFormat(fnType, false); err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
fnName := getActivityFunctionName(e.impl.registry, activity)
|
||
|
e.impl.registry.RegisterActivityWithOptions(activity, RegisterActivityOptions{DisableAlreadyRegisteredCheck: true})
|
||
|
call = e.mock.On(fnName, args...)
|
||
|
|
||
|
case reflect.String:
|
||
|
name := activity.(string)
|
||
|
_, ok := e.impl.registry.GetActivity(name)
|
||
|
if !ok {
|
||
|
registered := strings.Join(e.impl.registry.getRegisteredActivityTypes(), ", ")
|
||
|
panic(fmt.Sprintf("activity \""+name+"\" is not registered with the TestWorkflowEnvironment, "+
|
||
|
"registered types are: %v", registered))
|
||
|
}
|
||
|
call = e.mock.On(name, args...)
|
||
|
default:
|
||
|
panic("activity must be function or string")
|
||
|
}
|
||
|
|
||
|
return e.wrapCall(call)
|
||
|
}
|
||
|
|
||
|
// ErrMockStartChildWorkflowFailed is special error used to indicate the mocked child workflow should fail to start.
|
||
|
// This error is also exposed as public as testsuite.ErrMockStartChildWorkflowFailed
|
||
|
var ErrMockStartChildWorkflowFailed = fmt.Errorf("start child workflow failed: %v", enumspb.START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_WORKFLOW_ALREADY_EXISTS)
|
||
|
|
||
|
// OnWorkflow setup a mock call for workflow. Parameter workflow must be workflow function (func) or workflow name (string).
|
||
|
// You must call Return() with appropriate parameters on the returned *MockCallWrapper instance. The supplied parameters to
|
||
|
// the Return() call should either be a function that has exact same signature as the mocked workflow, or it should be
|
||
|
// mock values with the same types as the mocked workflow function returns.
|
||
|
// Example: assume the workflow you want to mock has function signature as:
|
||
|
// func MyChildWorkflow(ctx workflow.Context, msg string) (string, error)
|
||
|
// You can mock it by return a function with exact same signature:
|
||
|
// t.OnWorkflow(MyChildWorkflow, mock.Anything, mock.Anything).Return(func(ctx workflow.Context, msg string) (string, error) {
|
||
|
// // your mock function implementation
|
||
|
// return "", nil
|
||
|
// })
|
||
|
// OR return mock values with same types as workflow function's return types:
|
||
|
// t.OnWorkflow(MyChildWorkflow, mock.Anything, mock.Anything).Return("mock_result", nil)
|
||
|
// You could also setup mock to simulate start child workflow failure case by returning ErrMockStartChildWorkflowFailed
|
||
|
// as error.
|
||
|
func (e *TestWorkflowEnvironment) OnWorkflow(workflow interface{}, args ...interface{}) *MockCallWrapper {
|
||
|
fType := reflect.TypeOf(workflow)
|
||
|
var call *mock.Call
|
||
|
switch fType.Kind() {
|
||
|
case reflect.Func:
|
||
|
fnType := reflect.TypeOf(workflow)
|
||
|
if err := validateFnFormat(fnType, true); err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
fnName, _ := getWorkflowFunctionName(e.impl.registry, workflow)
|
||
|
if alias, ok := e.impl.registry.getWorkflowAlias(fnName); ok {
|
||
|
fnName = alias
|
||
|
}
|
||
|
call = e.mock.On(fnName, args...)
|
||
|
case reflect.String:
|
||
|
call = e.mock.On(workflow.(string), args...)
|
||
|
default:
|
||
|
panic("activity must be function or string")
|
||
|
}
|
||
|
|
||
|
return e.wrapCall(call)
|
||
|
}
|
||
|
|
||
|
const mockMethodForSignalExternalWorkflow = "workflow.SignalExternalWorkflow"
|
||
|
const mockMethodForRequestCancelExternalWorkflow = "workflow.RequestCancelExternalWorkflow"
|
||
|
const mockMethodForGetVersion = "workflow.GetVersion"
|
||
|
const mockMethodForUpsertSearchAttributes = "workflow.UpsertSearchAttributes"
|
||
|
|
||
|
// OnSignalExternalWorkflow setup a mock for sending signal to external workflow.
|
||
|
// This TestWorkflowEnvironment handles sending signals between the workflows that are started from the root workflow.
|
||
|
// For example, sending signals between parent and child workflows. Or sending signals between 2 child workflows.
|
||
|
// However, it does not know what to do if your tested workflow code is sending signal to external unknown workflows.
|
||
|
// In that case, you will need to setup mock for those signal calls.
|
||
|
// Some examples of how to setup mock:
|
||
|
//
|
||
|
// * mock for specific target workflow that matches specific signal name and signal data
|
||
|
// env.OnSignalExternalWorkflow("test-namespace", "test-workflow-id1", "test-runid1", "test-signal", "test-data").Return(nil).Once()
|
||
|
// * mock for anything and succeed the send
|
||
|
// env.OnSignalExternalWorkflow(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
||
|
// * mock for anything and fail the send
|
||
|
// env.OnSignalExternalWorkflow(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("unknown external workflow")).Once()
|
||
|
// * mock function for SignalExternalWorkflow
|
||
|
// env.OnSignalExternalWorkflow(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(
|
||
|
// func(namespace, workflowID, runID, signalName string, arg interface{}) error {
|
||
|
// // you can do differently based on the parameters
|
||
|
// return nil
|
||
|
// })
|
||
|
func (e *TestWorkflowEnvironment) OnSignalExternalWorkflow(namespace, workflowID, runID, signalName, arg interface{}) *MockCallWrapper {
|
||
|
call := e.mock.On(mockMethodForSignalExternalWorkflow, namespace, workflowID, runID, signalName, arg)
|
||
|
return e.wrapCall(call)
|
||
|
}
|
||
|
|
||
|
// OnRequestCancelExternalWorkflow setup a mock for cancellation of external workflow.
|
||
|
// This TestWorkflowEnvironment handles cancellation of workflows that are started from the root workflow.
|
||
|
// For example, cancellation sent from parent to child workflows. Or cancellation between 2 child workflows.
|
||
|
// However, it does not know what to do if your tested workflow code is sending cancellation to external unknown workflows.
|
||
|
// In that case, you will need to setup mock for those cancel calls.
|
||
|
// Some examples of how to setup mock:
|
||
|
//
|
||
|
// * mock for specific target workflow that matches specific workflow ID and run ID
|
||
|
// env.OnRequestCancelExternalWorkflow("test-namespace", "test-workflow-id1", "test-runid1").Return(nil).Once()
|
||
|
// * mock for anything and succeed the cancellation
|
||
|
// env.OnRequestCancelExternalWorkflow(mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
||
|
// * mock for anything and fail the cancellation
|
||
|
// env.OnRequestCancelExternalWorkflow(mock.Anything, mock.Anything, mock.Anything).Return(errors.New("unknown external workflow")).Once()
|
||
|
// * mock function for RequestCancelExternalWorkflow
|
||
|
// env.OnRequestCancelExternalWorkflow(mock.Anything, mock.Anything, mock.Anything).Return(
|
||
|
// func(namespace, workflowID, runID) error {
|
||
|
// // you can do differently based on the parameters
|
||
|
// return nil
|
||
|
// })
|
||
|
func (e *TestWorkflowEnvironment) OnRequestCancelExternalWorkflow(namespace, workflowID, runID string) *MockCallWrapper {
|
||
|
call := e.mock.On(mockMethodForRequestCancelExternalWorkflow, namespace, workflowID, runID)
|
||
|
return e.wrapCall(call)
|
||
|
}
|
||
|
|
||
|
// OnGetVersion setup a mock for workflow.GetVersion() call. By default, if mock is not setup, the GetVersion call from
|
||
|
// workflow code will always return the maxSupported version. Make it not possible to test old version branch. With this
|
||
|
// mock support, it is possible to test code branch for different versions.
|
||
|
//
|
||
|
// Note: mock can be setup for a specific changeID. Or if mock.Anything is used as changeID then all calls to GetVersion
|
||
|
// will be mocked. Mock for a specific changeID has higher priority over mock.Anything.
|
||
|
func (e *TestWorkflowEnvironment) OnGetVersion(changeID string, minSupported, maxSupported Version) *MockCallWrapper {
|
||
|
call := e.mock.On(getMockMethodForGetVersion(changeID), changeID, minSupported, maxSupported)
|
||
|
return e.wrapCall(call)
|
||
|
}
|
||
|
|
||
|
// OnUpsertSearchAttributes setup a mock for workflow.UpsertSearchAttributes call.
|
||
|
// If mock is not setup, the UpsertSearchAttributes call will only validate input attributes.
|
||
|
// If mock is setup, all UpsertSearchAttributes calls in workflow have to be mocked.
|
||
|
func (e *TestWorkflowEnvironment) OnUpsertSearchAttributes(attributes map[string]interface{}) *MockCallWrapper {
|
||
|
call := e.mock.On(mockMethodForUpsertSearchAttributes, attributes)
|
||
|
return e.wrapCall(call)
|
||
|
}
|
||
|
|
||
|
func (e *TestWorkflowEnvironment) wrapCall(call *mock.Call) *MockCallWrapper {
|
||
|
callWrapper := &MockCallWrapper{call: call, env: e}
|
||
|
call.Run(e.impl.getMockRunFn(callWrapper))
|
||
|
return callWrapper
|
||
|
}
|
||
|
|
||
|
// Once indicates that the mock should only return the value once.
|
||
|
func (c *MockCallWrapper) Once() *MockCallWrapper {
|
||
|
return c.Times(1)
|
||
|
}
|
||
|
|
||
|
// Twice indicates that the mock should only return the value twice.
|
||
|
func (c *MockCallWrapper) Twice() *MockCallWrapper {
|
||
|
return c.Times(2)
|
||
|
}
|
||
|
|
||
|
// Times indicates that the mock should only return the indicated number of times.
|
||
|
func (c *MockCallWrapper) Times(i int) *MockCallWrapper {
|
||
|
c.call.Times(i)
|
||
|
return c
|
||
|
}
|
||
|
|
||
|
// Never indicates that the mock should not be called.
|
||
|
func (c *MockCallWrapper) Never() *MockCallWrapper {
|
||
|
c.call.Maybe()
|
||
|
c.call.Panic(fmt.Sprintf("unexpected call: %s(%s)", c.call.Method, c.call.Arguments.String()))
|
||
|
return c
|
||
|
}
|
||
|
|
||
|
// Maybe indicates that the mock call is optional. Not calling an optional method
|
||
|
// will not cause an error while asserting expectations.
|
||
|
func (c *MockCallWrapper) Maybe() *MockCallWrapper {
|
||
|
c.call.Maybe()
|
||
|
return c
|
||
|
}
|
||
|
|
||
|
// Run sets a handler to be called before returning. It can be used when mocking a method such as unmarshalers that
|
||
|
// takes a pointer to a struct and sets properties in such struct.
|
||
|
func (c *MockCallWrapper) Run(fn func(args mock.Arguments)) *MockCallWrapper {
|
||
|
c.runFn = fn
|
||
|
return c
|
||
|
}
|
||
|
|
||
|
// After sets how long to wait on workflow's clock before the mock call returns.
|
||
|
func (c *MockCallWrapper) After(d time.Duration) *MockCallWrapper {
|
||
|
c.waitDuration = func() time.Duration { return d }
|
||
|
return c
|
||
|
}
|
||
|
|
||
|
// AfterFn sets a function which will tell how long to wait on workflow's clock before the mock call returns.
|
||
|
func (c *MockCallWrapper) AfterFn(fn func() time.Duration) *MockCallWrapper {
|
||
|
c.waitDuration = fn
|
||
|
return c
|
||
|
}
|
||
|
|
||
|
// Return specifies the return arguments for the expectation.
|
||
|
func (c *MockCallWrapper) Return(returnArguments ...interface{}) *MockCallWrapper {
|
||
|
c.call.Return(returnArguments...)
|
||
|
return c
|
||
|
}
|
||
|
|
||
|
// Panic specifies if the function call should fail and the panic message
|
||
|
func (c *MockCallWrapper) Panic(msg string) *MockCallWrapper {
|
||
|
c.call.Panic(msg)
|
||
|
return c
|
||
|
}
|
||
|
|
||
|
// ExecuteWorkflow executes a workflow, wait until workflow complete. It will fail the test if workflow is blocked and
|
||
|
// cannot complete within TestTimeout (set by SetTestTimeout()).
|
||
|
func (e *TestWorkflowEnvironment) ExecuteWorkflow(workflowFn interface{}, args ...interface{}) {
|
||
|
e.impl.mock = &e.mock
|
||
|
e.impl.executeWorkflow(workflowFn, args...)
|
||
|
}
|
||
|
|
||
|
// Now returns the current workflow time (a.k.a workflow.Now() time) of this TestWorkflowEnvironment.
|
||
|
func (e *TestWorkflowEnvironment) Now() time.Time {
|
||
|
return e.impl.Now()
|
||
|
}
|
||
|
|
||
|
// SetWorkerOptions sets the WorkerOptions that will be use by TestActivityEnvironment. TestActivityEnvironment will
|
||
|
// use options of BackgroundActivityContext, MaxConcurrentSessionExecutionSize, and WorkflowInterceptorChainFactories on the WorkerOptions.
|
||
|
// Other options are ignored.
|
||
|
// Note: WorkerOptions is defined in internal package, use public type worker.Options instead.
|
||
|
func (e *TestWorkflowEnvironment) SetWorkerOptions(options WorkerOptions) *TestWorkflowEnvironment {
|
||
|
e.impl.setWorkerOptions(options)
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetStartWorkflowOptions sets StartWorkflowOptions used to specify workflow execution timeout and task queue.
|
||
|
// Note that StartWorkflowOptions is defined in an internal package, use client.StartWorkflowOptions instead.
|
||
|
func (e *TestWorkflowEnvironment) SetStartWorkflowOptions(options StartWorkflowOptions) *TestWorkflowEnvironment {
|
||
|
e.impl.setStartWorkflowOptions(options)
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetDataConverter sets data converter.
|
||
|
func (e *TestWorkflowEnvironment) SetDataConverter(dataConverter converter.DataConverter) *TestWorkflowEnvironment {
|
||
|
e.impl.setDataConverter(dataConverter)
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetContextPropagators sets context propagators.
|
||
|
func (e *TestWorkflowEnvironment) SetContextPropagators(contextPropagators []ContextPropagator) *TestWorkflowEnvironment {
|
||
|
e.impl.setContextPropagators(contextPropagators)
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetHeader sets header.
|
||
|
func (e *TestWorkflowEnvironment) SetHeader(header *commonpb.Header) {
|
||
|
e.impl.header = header
|
||
|
}
|
||
|
|
||
|
// SetIdentity sets identity.
|
||
|
func (e *TestWorkflowEnvironment) SetIdentity(identity string) *TestWorkflowEnvironment {
|
||
|
e.impl.setIdentity(identity)
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetDetachedChildWait, if true, will make ExecuteWorkflow wait on all child
|
||
|
// workflows to complete even if their close policy is set to abandon or request
|
||
|
// cancel, meaning they are "detached". If false, ExecuteWorkflow will block
|
||
|
// until only all attached child workflows have completed. This is useful when
|
||
|
// testing endless detached child workflows, as without it ExecuteWorkflow may
|
||
|
// not return while detached children are still running.
|
||
|
//
|
||
|
// Default is true.
|
||
|
func (e *TestWorkflowEnvironment) SetDetachedChildWait(detachedChildWait bool) *TestWorkflowEnvironment {
|
||
|
e.impl.setDetachedChildWaitDisabled(!detachedChildWait)
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetWorkerStopChannel sets the activity worker stop channel to be returned from activity.GetWorkerStopChannel(context)
|
||
|
// You can use this function to set the activity worker stop channel and use close(channel) to test your activity execution
|
||
|
// from workflow execution.
|
||
|
func (e *TestWorkflowEnvironment) SetWorkerStopChannel(c chan struct{}) {
|
||
|
e.impl.setWorkerStopChannel(c)
|
||
|
}
|
||
|
|
||
|
// SetTestTimeout sets the idle timeout based on wall clock for this tested workflow. Idle is when workflow is blocked
|
||
|
// waiting on events (including timer, activity, child workflow, signal etc). If there is no event happening longer than
|
||
|
// this idle timeout, the test framework would stop the workflow and return timeout error.
|
||
|
// This is based on real wall clock time, not the workflow time (a.k.a workflow.Now() time).
|
||
|
func (e *TestWorkflowEnvironment) SetTestTimeout(idleTimeout time.Duration) *TestWorkflowEnvironment {
|
||
|
e.impl.testTimeout = idleTimeout
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetWorkflowRunTimeout sets the run timeout for this tested workflow. This test framework uses mock clock internally
|
||
|
// and when workflow is blocked on timer, it will auto forward the mock clock. Use SetWorkflowRunTimeout() to enforce a
|
||
|
// workflow run timeout to return timeout error when the workflow mock clock is moved head of the timeout.
|
||
|
// This is based on the workflow time (a.k.a workflow.Now() time).
|
||
|
func (e *TestWorkflowEnvironment) SetWorkflowRunTimeout(runTimeout time.Duration) *TestWorkflowEnvironment {
|
||
|
e.impl.runTimeout = runTimeout
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetOnActivityStartedListener sets a listener that will be called before activity starts execution.
|
||
|
// Note: ActivityInfo is defined in internal package, use public type activity.Info instead.
|
||
|
func (e *TestWorkflowEnvironment) SetOnActivityStartedListener(
|
||
|
listener func(activityInfo *ActivityInfo, ctx context.Context, args converter.EncodedValues)) *TestWorkflowEnvironment {
|
||
|
e.impl.onActivityStartedListener = listener
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetOnActivityCompletedListener sets a listener that will be called after an activity is completed.
|
||
|
// Note: ActivityInfo is defined in internal package, use public type activity.Info instead.
|
||
|
func (e *TestWorkflowEnvironment) SetOnActivityCompletedListener(
|
||
|
listener func(activityInfo *ActivityInfo, result converter.EncodedValue, err error)) *TestWorkflowEnvironment {
|
||
|
e.impl.onActivityCompletedListener = listener
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetOnActivityCanceledListener sets a listener that will be called after an activity is canceled.
|
||
|
// Note: ActivityInfo is defined in internal package, use public type activity.Info instead.
|
||
|
func (e *TestWorkflowEnvironment) SetOnActivityCanceledListener(
|
||
|
listener func(activityInfo *ActivityInfo)) *TestWorkflowEnvironment {
|
||
|
e.impl.onActivityCanceledListener = listener
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetOnActivityHeartbeatListener sets a listener that will be called when activity heartbeat.
|
||
|
// Note: ActivityInfo is defined in internal package, use public type activity.Info instead.
|
||
|
func (e *TestWorkflowEnvironment) SetOnActivityHeartbeatListener(
|
||
|
listener func(activityInfo *ActivityInfo, details converter.EncodedValues)) *TestWorkflowEnvironment {
|
||
|
e.impl.onActivityHeartbeatListener = listener
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetOnChildWorkflowStartedListener sets a listener that will be called before a child workflow starts execution.
|
||
|
// Note: WorkflowInfo is defined in internal package, use public type workflow.Info instead.
|
||
|
func (e *TestWorkflowEnvironment) SetOnChildWorkflowStartedListener(
|
||
|
listener func(workflowInfo *WorkflowInfo, ctx Context, args converter.EncodedValues)) *TestWorkflowEnvironment {
|
||
|
e.impl.onChildWorkflowStartedListener = listener
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetOnChildWorkflowCompletedListener sets a listener that will be called after a child workflow is completed.
|
||
|
// Note: WorkflowInfo is defined in internal package, use public type workflow.Info instead.
|
||
|
func (e *TestWorkflowEnvironment) SetOnChildWorkflowCompletedListener(
|
||
|
listener func(workflowInfo *WorkflowInfo, result converter.EncodedValue, err error)) *TestWorkflowEnvironment {
|
||
|
e.impl.onChildWorkflowCompletedListener = listener
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetOnChildWorkflowCanceledListener sets a listener that will be called when a child workflow is canceled.
|
||
|
// Note: WorkflowInfo is defined in internal package, use public type workflow.Info instead.
|
||
|
func (e *TestWorkflowEnvironment) SetOnChildWorkflowCanceledListener(
|
||
|
listener func(workflowInfo *WorkflowInfo)) *TestWorkflowEnvironment {
|
||
|
e.impl.onChildWorkflowCanceledListener = listener
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetOnTimerScheduledListener sets a listener that will be called before a timer is scheduled.
|
||
|
func (e *TestWorkflowEnvironment) SetOnTimerScheduledListener(
|
||
|
listener func(timerID string, duration time.Duration)) *TestWorkflowEnvironment {
|
||
|
e.impl.onTimerScheduledListener = listener
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetOnTimerFiredListener sets a listener that will be called after a timer is fired.
|
||
|
func (e *TestWorkflowEnvironment) SetOnTimerFiredListener(listener func(timerID string)) *TestWorkflowEnvironment {
|
||
|
e.impl.onTimerFiredListener = listener
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetOnTimerCanceledListener sets a listener that will be called after a timer is canceled
|
||
|
func (e *TestWorkflowEnvironment) SetOnTimerCanceledListener(listener func(timerID string)) *TestWorkflowEnvironment {
|
||
|
e.impl.onTimerCanceledListener = listener
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetOnLocalActivityStartedListener sets a listener that will be called before local activity starts execution.
|
||
|
// Note: ActivityInfo is defined in internal package, use public type activity.Info instead.
|
||
|
func (e *TestWorkflowEnvironment) SetOnLocalActivityStartedListener(
|
||
|
listener func(activityInfo *ActivityInfo, ctx context.Context, args []interface{})) *TestWorkflowEnvironment {
|
||
|
e.impl.onLocalActivityStartedListener = listener
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetOnLocalActivityCompletedListener sets a listener that will be called after local activity is completed.
|
||
|
// Note: ActivityInfo is defined in internal package, use public type activity.Info instead.
|
||
|
func (e *TestWorkflowEnvironment) SetOnLocalActivityCompletedListener(
|
||
|
listener func(activityInfo *ActivityInfo, result converter.EncodedValue, err error)) *TestWorkflowEnvironment {
|
||
|
e.impl.onLocalActivityCompletedListener = listener
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// SetOnLocalActivityCanceledListener sets a listener that will be called after local activity is canceled.
|
||
|
// Note: ActivityInfo is defined in internal package, use public type activity.Info instead.
|
||
|
func (e *TestWorkflowEnvironment) SetOnLocalActivityCanceledListener(
|
||
|
listener func(activityInfo *ActivityInfo)) *TestWorkflowEnvironment {
|
||
|
e.impl.onLocalActivityCanceledListener = listener
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
// IsWorkflowCompleted check if test is completed or not
|
||
|
func (e *TestWorkflowEnvironment) IsWorkflowCompleted() bool {
|
||
|
return e.impl.isWorkflowCompleted
|
||
|
}
|
||
|
|
||
|
// GetWorkflowResult extracts the encoded result from test workflow, it returns error if the extraction failed.
|
||
|
func (e *TestWorkflowEnvironment) GetWorkflowResult(valuePtr interface{}) error {
|
||
|
if !e.impl.isWorkflowCompleted {
|
||
|
panic("workflow is not completed")
|
||
|
}
|
||
|
if e.impl.testError != nil || e.impl.testResult == nil || valuePtr == nil {
|
||
|
return e.impl.testError
|
||
|
}
|
||
|
return e.impl.testResult.Get(valuePtr)
|
||
|
}
|
||
|
|
||
|
// GetWorkflowResultByID extracts the encoded result from workflow by ID, it returns error if the extraction failed.
|
||
|
func (e *TestWorkflowEnvironment) GetWorkflowResultByID(workflowID string, valuePtr interface{}) error {
|
||
|
if workflowHandle, ok := e.impl.runningWorkflows[workflowID]; ok {
|
||
|
if !workflowHandle.env.isWorkflowCompleted {
|
||
|
panic("workflow is not completed")
|
||
|
}
|
||
|
if workflowHandle.env.testError != nil || workflowHandle.env.testResult == nil || valuePtr == nil {
|
||
|
return e.impl.testError
|
||
|
}
|
||
|
return e.impl.testResult.Get(valuePtr)
|
||
|
}
|
||
|
return serviceerror.NewNotFound(fmt.Sprintf("Workflow %v not exists", workflowID))
|
||
|
}
|
||
|
|
||
|
// GetWorkflowError return the error from test workflow
|
||
|
func (e *TestWorkflowEnvironment) GetWorkflowError() error {
|
||
|
return e.impl.testError
|
||
|
}
|
||
|
|
||
|
// GetWorkflowErrorByID return the error from test workflow
|
||
|
func (e *TestWorkflowEnvironment) GetWorkflowErrorByID(workflowID string) error {
|
||
|
if workflowHandle, ok := e.impl.runningWorkflows[workflowID]; ok {
|
||
|
return workflowHandle.env.testError
|
||
|
}
|
||
|
return serviceerror.NewNotFound(fmt.Sprintf("Workflow %v not exists", workflowID))
|
||
|
}
|
||
|
|
||
|
// CompleteActivity complete an activity that had returned activity.ErrResultPending error
|
||
|
func (e *TestWorkflowEnvironment) CompleteActivity(taskToken []byte, result interface{}, err error) error {
|
||
|
return e.impl.CompleteActivity(taskToken, result, err)
|
||
|
}
|
||
|
|
||
|
// CancelWorkflow requests cancellation (through workflow Context) to the currently running test workflow.
|
||
|
func (e *TestWorkflowEnvironment) CancelWorkflow() {
|
||
|
e.impl.cancelWorkflow(func(result *commonpb.Payloads, err error) {})
|
||
|
}
|
||
|
|
||
|
// SignalWorkflow sends signal to the currently running test workflow.
|
||
|
func (e *TestWorkflowEnvironment) SignalWorkflow(name string, input interface{}) {
|
||
|
e.impl.signalWorkflow(name, input, true)
|
||
|
}
|
||
|
|
||
|
// SignalWorkflowSkippingWorkflowTask sends signal to the currently running test workflow without invoking workflow code.
|
||
|
// Used to test processing of multiple buffered signals before completing workflow.
|
||
|
// It must be followed by SignalWorkflow, CancelWorkflow or CompleteActivity to force a workflow task.
|
||
|
func (e *TestWorkflowEnvironment) SignalWorkflowSkippingWorkflowTask(name string, input interface{}) {
|
||
|
e.impl.signalWorkflow(name, input, false)
|
||
|
}
|
||
|
|
||
|
// SignalWorkflowByID sends signal to the currently running test workflow.
|
||
|
func (e *TestWorkflowEnvironment) SignalWorkflowByID(workflowID, signalName string, input interface{}) error {
|
||
|
return e.impl.signalWorkflowByID(workflowID, signalName, input)
|
||
|
}
|
||
|
|
||
|
// QueryWorkflow queries to the currently running test workflow and returns result synchronously.
|
||
|
func (e *TestWorkflowEnvironment) QueryWorkflow(queryType string, args ...interface{}) (converter.EncodedValue, error) {
|
||
|
return e.impl.queryWorkflow(queryType, args...)
|
||
|
}
|
||
|
|
||
|
// QueryWorkflowByID queries a child workflow by its ID and returns the result synchronously
|
||
|
func (e *TestWorkflowEnvironment) QueryWorkflowByID(workflowID, queryType string, args ...interface{}) (converter.EncodedValue, error) {
|
||
|
return e.impl.queryWorkflowByID(workflowID, queryType, args...)
|
||
|
}
|
||
|
|
||
|
// RegisterDelayedCallback creates a new timer with specified delayDuration using workflow clock (not wall clock). When
|
||
|
// the timer fires, the callback will be called. By default, this test suite uses mock clock which automatically move
|
||
|
// forward to fire next timer when workflow is blocked. Use this API to make some event (like activity completion,
|
||
|
// signal or workflow cancellation) at desired time.
|
||
|
//
|
||
|
// Use 0 delayDuration to send a signal to simulate SignalWithStart. Note that a 0 duration delay will *not* work with
|
||
|
// Queries, as the workflow will not have had a chance to register any query handlers.
|
||
|
func (e *TestWorkflowEnvironment) RegisterDelayedCallback(callback func(), delayDuration time.Duration) {
|
||
|
e.impl.registerDelayedCallback(callback, delayDuration)
|
||
|
}
|
||
|
|
||
|
// SetActivityTaskQueue set the affinity between activity and taskqueue. By default, activity can be invoked by any taskqueue
|
||
|
// in this test environment. Use this SetActivityTaskQueue() to set affinity between activity and a taskqueue. Once
|
||
|
// activity is set to a particular taskqueue, that activity will only be available to that taskqueue.
|
||
|
func (e *TestWorkflowEnvironment) SetActivityTaskQueue(taskqueue string, activityFn ...interface{}) {
|
||
|
e.impl.setActivityTaskQueue(taskqueue, activityFn...)
|
||
|
}
|
||
|
|
||
|
// SetLastCompletionResult sets the result to be returned from workflow.GetLastCompletionResult().
|
||
|
func (e *TestWorkflowEnvironment) SetLastCompletionResult(result interface{}) {
|
||
|
e.impl.setLastCompletionResult(result)
|
||
|
}
|
||
|
|
||
|
// SetLastError sets the result to be returned from workflow.GetLastError().
|
||
|
func (e *TestWorkflowEnvironment) SetLastError(err error) {
|
||
|
e.impl.setLastError(err)
|
||
|
}
|
||
|
|
||
|
// SetMemoOnStart sets the memo when start workflow.
|
||
|
func (e *TestWorkflowEnvironment) SetMemoOnStart(memo map[string]interface{}) error {
|
||
|
memoStruct, err := getWorkflowMemo(memo, e.impl.GetDataConverter())
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
e.impl.workflowInfo.Memo = memoStruct
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// SetSearchAttributesOnStart sets the search attributes when start workflow.
|
||
|
func (e *TestWorkflowEnvironment) SetSearchAttributesOnStart(searchAttributes map[string]interface{}) error {
|
||
|
attr, err := serializeSearchAttributes(searchAttributes)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
e.impl.workflowInfo.SearchAttributes = attr
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// AssertExpectations asserts that everything specified with OnActivity
|
||
|
// in fact called as expected. Calls may have occurred in any order.
|
||
|
func (e *TestWorkflowEnvironment) AssertExpectations(t mock.TestingT) bool {
|
||
|
return e.mock.AssertExpectations(t)
|
||
|
}
|