Skip to content

Commit

Permalink
Add unit test runner for workflows (#751)
Browse files Browse the repository at this point in the history
  • Loading branch information
nolag authored Sep 9, 2024
1 parent 1b0938c commit 3c6df3a
Show file tree
Hide file tree
Showing 6 changed files with 445 additions and 101 deletions.
13 changes: 7 additions & 6 deletions pkg/capabilities/cli/cmd/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli/cmd/testdata/fixtures/capabilities/nestedaction"
"github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli/cmd/testdata/fixtures/capabilities/referenceaction"
"github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli/cmd/testdata/fixtures/capabilities/referenceaction/referenceactiontest"
"github.com/smartcontractkit/chainlink-common/pkg/utils/tests"
"github.com/smartcontractkit/chainlink-common/pkg/workflows"
"github.com/smartcontractkit/chainlink-common/pkg/workflows/testutils"
)
Expand Down Expand Up @@ -243,7 +244,7 @@ func TestTypeGeneration(t *testing.T) {

func TestMockGeneration(t *testing.T) {
t.Run("Basic trigger", func(t *testing.T) {
runner := testutils.NewRunner()
runner := testutils.NewRunner(tests.Context(t))
capMock := basictriggertest.Trigger(runner, func() (basictrigger.TriggerOutputs, error) {
return basictrigger.TriggerOutputs{}, nil
})
Expand All @@ -255,7 +256,7 @@ func TestMockGeneration(t *testing.T) {
})

t.Run("Basic action", func(t *testing.T) {
runner := testutils.NewRunner()
runner := testutils.NewRunner(tests.Context(t))

// nolint value is never used but it's assigned to mock to verify the type
capMock := basicactiontest.Action(runner, func(_ basicaction.ActionInputs) (basicaction.ActionOutputs, error) {
Expand All @@ -275,7 +276,7 @@ func TestMockGeneration(t *testing.T) {
})

t.Run("Basic target", func(t *testing.T) {
runner := testutils.NewRunner()
runner := testutils.NewRunner(tests.Context(t))
capMock := basictargettest.Target(runner, func(_ basictarget.TargetInputs) error {
return nil
})
Expand All @@ -287,7 +288,7 @@ func TestMockGeneration(t *testing.T) {
})

t.Run("References", func(t *testing.T) {
runner := testutils.NewRunner()
runner := testutils.NewRunner(tests.Context(t))

// nolint value is never used but it's assigned to mock to verify the type
capMock := referenceactiontest.Action(runner, func(_ referenceaction.SomeInputs) (referenceaction.SomeOutputs, error) {
Expand All @@ -307,7 +308,7 @@ func TestMockGeneration(t *testing.T) {
})

t.Run("External references", func(t *testing.T) {
runner := testutils.NewRunner()
runner := testutils.NewRunner(tests.Context(t))

// nolint value is never used but it's assigned to mock to verify the type
capMock := externalreferenceactiontest.Action(runner, func(_ referenceaction.SomeInputs) (referenceaction.SomeOutputs, error) {
Expand All @@ -330,7 +331,7 @@ func TestMockGeneration(t *testing.T) {
// no need to test nesting, we don't generate anything different for the mock's

t.Run("Array action", func(t *testing.T) {
runner := testutils.NewRunner()
runner := testutils.NewRunner(tests.Context(t))
// nolint value is never used but it's assigned to mock to verify the type
capMock := arrayactiontest.Action(runner, func(_ arrayaction.ActionInputs) ([]arrayaction.ActionOutputsElem, error) {
return []arrayaction.ActionOutputsElem{}, nil
Expand Down
6 changes: 5 additions & 1 deletion pkg/workflows/dependency_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ func ParseDependencyGraph(yamlWorkflow string) (*DependencyGraph, error) {
return nil, err
}

return BuildDependencyGraph(spec)
}

func BuildDependencyGraph(spec WorkflowSpec) (*DependencyGraph, error) {
// Construct and validate the graph. We instantiate an
// empty graph with just one starting entry: `trigger`.
// This provides the starting point for our graph and
Expand All @@ -110,7 +114,7 @@ func ParseDependencyGraph(yamlWorkflow string) (*DependencyGraph, error) {
graph.PreventCycles(),
graph.Directed(),
)
err = g.AddVertex(&Vertex{
err := g.AddVertex(&Vertex{
StepDefinition: StepDefinition{Ref: KeywordTrigger},
})
if err != nil {
Expand Down
35 changes: 35 additions & 0 deletions pkg/workflows/testutils/compute_capability.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package testutils

import (
"context"

"github.com/smartcontractkit/chainlink-common/pkg/capabilities"
"github.com/smartcontractkit/chainlink-common/pkg/workflows"
)

type computeCapability struct {
sdk workflows.SDK
callback func(sdk workflows.SDK, request capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error)
}

func (c *computeCapability) Info(ctx context.Context) (capabilities.CapabilityInfo, error) {
info := capabilities.MustNewCapabilityInfo(
"[email protected]", capabilities.CapabilityTypeAction, "Custom compute capability",
)
info.IsLocal = true
return info, nil
}

func (c *computeCapability) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error {
return nil
}

func (c *computeCapability) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error {
return nil
}

func (c *computeCapability) Execute(ctx context.Context, request capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) {
return c.callback(c.sdk, request)
}

var _ capabilities.ActionCapability = &computeCapability{}
113 changes: 77 additions & 36 deletions pkg/workflows/testutils/mocks.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
package testutils

import (
"context"
"encoding/json"

"github.com/smartcontractkit/chainlink-common/pkg/capabilities"
"github.com/smartcontractkit/chainlink-common/pkg/values"
)

// CapabilityMock allows for mocking of capabilities in a workflow
// they can be registered for a particular reference or entirely
// Note that registrations for a step are taken over registrations for a capability when there are both.
type CapabilityMock interface {
Run(request capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error)
ID() string
func MockCapability[I, O any](id string, fn func(I) (O, error)) *Mock[I, O] {
return &Mock[I, O]{mockBase: mockCapabilityBase[I, O](id, fn)}
}

func MockCapability[I, O any](id string, fn func(I) (O, error)) *Mock[I, O] {
return &Mock[I, O]{
func mockCapabilityBase[I, O any](id string, fn func(I) (O, error)) *mockBase[I, O] {
return &mockBase[I, O]{
id: id,
inputs: map[string]I{},
outputs: map[string]O{},
Expand All @@ -23,25 +22,51 @@ func MockCapability[I, O any](id string, fn func(I) (O, error)) *Mock[I, O] {
}
}

type Mock[I, O any] struct {
type mockBase[I, O any] struct {
id string
inputs map[string]I
outputs map[string]O
errors map[string]error
fn func(I) (O, error)
}

var _ CapabilityMock = &Mock[any, any]{}
var _ capabilities.ExecutableCapability = &Mock[any, any]{}

func (m *Mock[I, O]) Run(request capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) {
func (m *mockBase[I, O]) Info(ctx context.Context) (capabilities.CapabilityInfo, error) {
return capabilities.CapabilityInfo{ID: m.id, IsLocal: true}, nil
}

func (m *mockBase[I, O]) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error {
return nil
}

func (m *mockBase[I, O]) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error {
return nil
}

func (m *mockBase[I, O]) Execute(ctx context.Context, request capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) {
var i I

if err := request.Inputs.UnwrapTo(&i); err != nil {
m.errors[request.Metadata.ReferenceID] = err
return capabilities.CapabilityResponse{}, err
}

m.inputs[request.Metadata.ReferenceID] = i

// validate against schema
var tmp I
b, err := json.Marshal(i)
if err != nil {
m.errors[request.Metadata.ReferenceID] = err
return capabilities.CapabilityResponse{}, err
}

if err = json.Unmarshal(b, &tmp); err != nil {
m.errors[request.Metadata.ReferenceID] = err
return capabilities.CapabilityResponse{}, err
}

result, err := m.fn(i)
if err != nil {
m.errors[request.Metadata.ReferenceID] = err
Expand All @@ -59,17 +84,21 @@ func (m *Mock[I, O]) Run(request capabilities.CapabilityRequest) (capabilities.C
return capabilities.CapabilityResponse{Value: wrapped}, nil
}

func (m *Mock[I, O]) ID() string {
func (m *mockBase[I, O]) ID() string {
return m.id
}

func (m *Mock[I, O]) GetStep(ref string) StepResults[I, O] {
func (m *mockBase[I, O]) GetStep(ref string) StepResults[I, O] {
input, ran := m.inputs[ref]
output := m.outputs[ref]
err := m.errors[ref]
return StepResults[I, O]{WasRun: ran, Input: input, Output: output, Error: err}
}

type Mock[I, O any] struct {
*mockBase[I, O]
}

type StepResults[I, O any] struct {
WasRun bool
Input I
Expand All @@ -90,60 +119,72 @@ type TriggerResults[O any] struct {

func MockTrigger[O any](id string, fn func() (O, error)) *TriggerMock[O] {
return &TriggerMock[O]{
mock: MockCapability[struct{}, O](id, func(struct{}) (O, error) {
mockBase: mockCapabilityBase[struct{}, O](id, func(struct{}) (O, error) {
return fn()
}),
}
}

type TriggerMock[O any] struct {
mock *Mock[struct{}, O]
*mockBase[struct{}, O]
}

func (t *TriggerMock[O]) Run(request capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) {
return t.mock.Run(request)
var _ capabilities.TriggerCapability = &TriggerMock[any]{}

func (t *TriggerMock[O]) RegisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) {
result, err := t.mockBase.fn(struct{}{})

wrapped, wErr := values.CreateMapFromStruct(result)
if wErr != nil {
return nil, wErr
}

response := capabilities.TriggerResponse{
Event: capabilities.TriggerEvent{
TriggerType: "Mock " + t.ID(),
ID: t.ID(),
Outputs: wrapped,
},
Err: err,
}
ch := make(chan capabilities.TriggerResponse, 1)
ch <- response
close(ch)
return ch, nil
}

func (t *TriggerMock[O]) ID() string {
return t.mock.ID()
func (t *TriggerMock[O]) UnregisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) error {
return nil
}

func (t *TriggerMock[O]) GetStep() TriggerResults[O] {
step := t.mock.GetStep("trigger")
step := t.mockBase.GetStep("trigger")
return TriggerResults[O]{Output: step.Output, Error: step.Error}
}

var _ CapabilityMock = &TriggerMock[any]{}

type TargetMock[I any] struct {
mock *Mock[I, struct{}]
*mockBase[I, struct{}]
}

func MockTarget[I any](id string, fn func(I) error) *TargetMock[I] {
return &TargetMock[I]{
mock: MockCapability[I, struct{}](id, func(i I) (struct{}, error) {
mockBase: mockCapabilityBase[I, struct{}](id, func(i I) (struct{}, error) {
return struct{}{}, fn(i)
}),
}
}

func (t *TargetMock[I]) Run(request capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) {
return t.mock.Run(request)
}

func (t *TargetMock[I]) ID() string {
return t.mock.ID()
}
var _ capabilities.TargetCapability = &TargetMock[any]{}

func (t *TargetMock[I]) GetAllWrites() TargetResults[I] {
targetResults := TargetResults[I]{}
for ref := range t.mock.inputs {
for ref := range t.mockBase.inputs {
targetResults.NumRuns++
step := t.mock.GetStep(ref)
step := t.mockBase.GetStep(ref)
targetResults.Inputs = append(targetResults.Inputs, step.Input)
targetResults.Errors = append(targetResults.Errors, step.Error)
if step.Error != nil {
targetResults.Errors = append(targetResults.Errors, step.Error)
}
}
return targetResults
}

var _ CapabilityMock = &TargetMock[any]{}
Loading

0 comments on commit 3c6df3a

Please sign in to comment.