Files
tasks/chasm/field_test.go
Lina JodoinandGitHub 8f9242aa05 [CHASM] Relax fieldsOf to allow private fields (#8151)
## What changed?
- Relax fieldsOf iterator.

## Why?
- Allow private/locally-cached fields in structs.

## How did you test it?
- [ ] built
- [ ] run locally and tested manually
- [x] covered by existing tests
- [ ] added new unit test(s)
- [ ] added new functional test(s)
2025-08-07 21:14:27 +00:00

353 lines
11 KiB
Go

package chasm
import (
"context"
"reflect"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
persistencespb "go.temporal.io/server/api/persistence/v1"
"go.temporal.io/server/common/clock"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/testing/protorequire"
"go.temporal.io/server/common/testing/testlogger"
"go.temporal.io/server/common/testing/testvars"
"go.uber.org/mock/gomock"
)
type fieldSuite struct {
suite.Suite
*require.Assertions
protorequire.ProtoAssertions
controller *gomock.Controller
nodeBackend *MockNodeBackend
registry *Registry
timeSource *clock.EventTimeSource
nodePathEncoder NodePathEncoder
logger log.Logger
}
func TestFieldSuite(t *testing.T) {
suite.Run(t, new(fieldSuite))
}
func (s *fieldSuite) SetupTest() {
s.initAssertions()
s.controller = gomock.NewController(s.T())
s.nodeBackend = NewMockNodeBackend(s.controller)
s.logger = testlogger.NewTestLogger(s.T(), testlogger.FailOnAnyUnexpectedError)
s.registry = NewRegistry(s.logger)
err := s.registry.Register(newTestLibrary(s.controller))
s.NoError(err)
s.timeSource = clock.NewEventTimeSource()
s.nodePathEncoder = &testNodePathEncoder{}
}
func (s *fieldSuite) SetupSubTest() {
s.initAssertions()
}
func (s *fieldSuite) initAssertions() {
// `s.Assertions` (as well as other test helpers which depends on `s.T()`) must be initialized on
// both test and subtest levels (but not suite level, where `s.T()` is `nil`).
//
// If these helpers are not reinitialized on subtest level, any failed `assert` in
// subtest will fail the entire test (not subtest) immediately without running other subtests.
s.Assertions = require.New(s.T())
s.ProtoAssertions = protorequire.New(s.T())
}
func (s *fieldSuite) TestInternalFieldName() {
f := Field[any]{}
fT := reflect.TypeOf(f)
_, ok := fT.FieldByName(internalFieldName)
s.True(ok, "expected field %s not found", internalFieldName)
}
func (s *fieldSuite) TestFieldGetSimple() {
tests := []struct {
name string
field Field[*TestSubComponent1]
expected *TestSubComponent1
}{
{
name: "Get with non-nil value",
field: Field[*TestSubComponent1]{
Internal: newFieldInternalWithValue(
fieldTypeComponent,
&TestSubComponent1{SubComponent1Data: &protoMessageType{
CreateRequestId: "component-data",
}},
)},
expected: &TestSubComponent1{SubComponent1Data: &protoMessageType{
CreateRequestId: "component-data",
}},
},
{
name: "Get with nil value and nil node",
field: Field[*TestSubComponent1]{
Internal: newFieldInternalWithNode(nil),
},
expected: nil,
},
}
for _, tt := range tests {
s.Run(tt.name, func() {
result, err := tt.field.Get(nil)
s.NoError(err)
s.Equal(tt.expected, result)
})
}
}
func (s *fieldSuite) TestFieldGetComponent() {
serializedNodes := testComponentSerializedNodes()
node, err := s.newTestTree(serializedNodes)
s.NoError(err)
chasmContext := NewMutableContext(context.Background(), node)
c, err := node.Component(chasmContext, ComponentRef{componentPath: rootPath})
s.NoError(err)
s.NotNil(c)
tc := c.(*TestComponent)
sc1, err := tc.SubComponent1.Get(chasmContext)
s.NoError(err)
s.NotNil(sc1)
s.ProtoEqual(&protoMessageType{
CreateRequestId: "sub-component1-data",
}, sc1.SubComponent1Data)
sd1, err := tc.SubData1.Get(chasmContext)
s.NoError(err)
s.NotNil(sd1)
s.ProtoEqual(&protoMessageType{
CreateRequestId: "sub-data1",
}, sd1)
}
func (s *fieldSuite) newTestTree(
serializedNodes map[string]*persistencespb.ChasmNode,
) (*Node, error) {
return NewTree(
serializedNodes,
s.registry,
s.timeSource,
s.nodeBackend,
s.nodePathEncoder,
s.logger,
)
}
// setupBasicTree creates a minimal tree structure with root node and context.
func (s *fieldSuite) setupBasicTree() (*Node, MutableContext, error) {
serializedNodes := map[string]*persistencespb.ChasmNode{
"": {
Metadata: &persistencespb.ChasmNodeMetadata{
InitialVersionedTransition: &persistencespb.VersionedTransition{TransitionCount: 1},
Attributes: &persistencespb.ChasmNodeMetadata_ComponentAttributes{
ComponentAttributes: &persistencespb.ChasmComponentAttributes{},
},
},
},
}
rootNode, err := s.newTestTree(serializedNodes)
if err != nil {
return nil, nil, err
}
ctx := NewMutableContext(context.Background(), rootNode)
return rootNode, ctx, nil
}
// setupComponentWithTree creates a basic component structure and attaches it to the tree.
func (s *fieldSuite) setupComponentWithTree(rootComponent *TestComponent) (*Node, MutableContext, error) {
rootNode, ctx, err := s.setupBasicTree()
if err != nil {
return nil, nil, err
}
rootNode.value = rootComponent
rootNode.valueState = valueStateNeedSerialize
return rootNode, ctx, nil
}
func (s *fieldSuite) TestDeferredPointerResolution() {
tv := testvars.New(s.T())
s.nodeBackend.EXPECT().NextTransitionCount().Return(int64(1)).AnyTimes()
s.nodeBackend.EXPECT().GetCurrentVersion().Return(int64(1)).AnyTimes()
s.nodeBackend.EXPECT().UpdateWorkflowStateStatus(gomock.Any(), gomock.Any()).AnyTimes()
s.nodeBackend.EXPECT().GetWorkflowKey().Return(tv.Any().WorkflowKey()).AnyTimes()
s.nodeBackend.EXPECT().AddTasks(gomock.Any()).AnyTimes()
// Create component structure that will simulate NewEntity scenario.
sc2 := &TestSubComponent2{
SubComponent2Data: &protoMessageType{
CreateRequestId: "sub-component2-data",
},
}
sc1 := &TestSubComponent1{
SubComponent1Data: &protoMessageType{
CreateRequestId: "sub-component1-data",
},
}
rootComponent := &TestComponent{
ComponentData: &protoMessageType{
CreateRequestId: "component-data",
},
SubComponent1: NewComponentField(nil, sc1),
}
rootNode, ctx, err := s.setupComponentWithTree(rootComponent)
s.NoError(err)
// Create deferred pointers.
sc1.SubComponent2Pointer = ComponentPointerTo(ctx, sc2)
rootComponent.SubComponent2 = NewComponentField(nil, sc2)
data := &protoMessageType{CreateRequestId: "sub-data-1"}
sc1.DataPointer = DataPointerTo(ctx, data)
rootComponent.SubData1 = NewDataField(ctx, data)
// Verify it's a deferred pointer storing the component directly.
s.Equal(fieldTypeDeferredPointer, sc1.SubComponent2Pointer.Internal.fieldType())
s.Equal(fieldTypeDeferredPointer, sc1.DataPointer.Internal.fieldType())
s.Equal(sc2, sc1.SubComponent2Pointer.Internal.v)
s.Equal(data, sc1.DataPointer.Internal.v)
// CloseTransaction should resolve the deferred pointer.
mutations, err := rootNode.CloseTransaction()
s.NoError(err)
s.NotEmpty(mutations.UpdatedNodes)
// Verify the pointers were resolved to a regular pointer with path.
s.Equal(fieldTypePointer, sc1.SubComponent2Pointer.Internal.fieldType())
s.Equal(fieldTypePointer, sc1.DataPointer.Internal.fieldType())
cResolvedPath, ok := sc1.SubComponent2Pointer.Internal.v.([]string)
s.True(ok)
s.Equal([]string{"SubComponent2"}, cResolvedPath)
dResolvedPath, ok := sc1.DataPointer.Internal.v.([]string)
s.True(ok)
s.Equal([]string{"SubData1"}, dResolvedPath)
// Verify we can dereference the pointers.
resolvedComponent, err := sc1.SubComponent2Pointer.Get(ctx)
s.NoError(err)
s.Equal(sc2, resolvedComponent)
// TODO - this doesn't resolve, but I've manually verified the tree structure looks correct
// TODO
// TODO
// resolvedData, err := sc1.DataPointer.Get(ctx)
// s.NoError(err)
// s.Equal(sc2.SubComponent2Data, resolvedData)
}
func (s *fieldSuite) TestMixedPointerScenario() {
tv := testvars.New(s.T())
s.nodeBackend.EXPECT().NextTransitionCount().Return(int64(1)).AnyTimes()
s.nodeBackend.EXPECT().GetCurrentVersion().Return(int64(1)).AnyTimes()
s.nodeBackend.EXPECT().UpdateWorkflowStateStatus(gomock.Any(), gomock.Any()).AnyTimes()
s.nodeBackend.EXPECT().GetWorkflowKey().Return(tv.Any().WorkflowKey()).AnyTimes()
s.nodeBackend.EXPECT().AddTasks(gomock.Any()).AnyTimes()
existingComponent := &TestSubComponent11{
SubComponent11Data: &protoMessageType{CreateRequestId: "existing-component"},
}
sc1 := &TestSubComponent1{
SubComponent1Data: &protoMessageType{CreateRequestId: "sub-component1-data"},
SubComponent11: NewComponentField(nil, existingComponent),
}
rootComponent := &TestComponent{
ComponentData: &protoMessageType{CreateRequestId: "component-data"},
SubComponent1: NewComponentField(nil, sc1),
}
rootNode, ctx, err := s.setupComponentWithTree(rootComponent)
s.NoError(err)
rootComponent.SubComponent11Pointer = ComponentPointerTo(ctx, existingComponent)
// Close the transaction to resolve SubComponent11Pointer's field to existingComponent.
_, err = rootNode.CloseTransaction()
s.NoError(err)
s.Equal(fieldTypePointer, rootComponent.SubComponent11Pointer.Internal.fieldType())
// Now, add a new component and deferred pointer for it.
newComponent := &TestSubComponent2{
SubComponent2Data: &protoMessageType{CreateRequestId: "new-component"},
}
ctx2 := NewMutableContext(context.Background(), rootNode)
sc1.SubComponent2Pointer = ComponentPointerTo(ctx2, newComponent)
// Now add the component to the tree so it can be resolved during CloseTransaction.
rootComponent.SubComponent2 = NewComponentField(ctx, newComponent)
s.Equal(fieldTypePointer, rootComponent.SubComponent11Pointer.Internal.fieldType())
s.Equal(fieldTypeDeferredPointer, sc1.SubComponent2Pointer.Internal.fieldType())
_, err = rootNode.CloseTransaction()
s.NoError(err)
// Ensure both pointers have been resolved.
s.Equal(fieldTypePointer, rootComponent.SubComponent11Pointer.Internal.fieldType())
s.Equal(fieldTypePointer, sc1.SubComponent2Pointer.Internal.fieldType())
resolved1, err := rootComponent.SubComponent11Pointer.Get(ctx2)
s.NoError(err)
s.Equal(existingComponent, resolved1)
resolved2, err := sc1.SubComponent2Pointer.Get(ctx2)
s.NoError(err)
s.Equal(newComponent, resolved2)
}
func (s *fieldSuite) TestUnresolvableDeferredPointerError() {
tv := testvars.New(s.T())
s.nodeBackend.EXPECT().NextTransitionCount().Return(int64(1)).AnyTimes()
s.nodeBackend.EXPECT().GetCurrentVersion().Return(int64(1)).AnyTimes()
s.nodeBackend.EXPECT().UpdateWorkflowStateStatus(gomock.Any(), gomock.Any()).AnyTimes()
s.nodeBackend.EXPECT().GetWorkflowKey().Return(tv.Any().WorkflowKey()).AnyTimes()
s.nodeBackend.EXPECT().AddTasks(gomock.Any()).AnyTimes()
orphanComponent := &TestSubComponent11{
SubComponent11Data: &protoMessageType{
CreateRequestId: "orphan-component",
},
}
rootComponent := &TestComponent{
ComponentData: &protoMessageType{
CreateRequestId: "component-data",
},
}
rootNode, ctx, err := s.setupComponentWithTree(rootComponent)
s.NoError(err)
rootComponent.SubComponent11Pointer = ComponentPointerTo(ctx, orphanComponent)
s.Equal(fieldTypeDeferredPointer, rootComponent.SubComponent11Pointer.Internal.fieldType())
_, err = rootNode.CloseTransaction()
s.Error(err)
s.Contains(err.Error(), "failed to resolve deferred pointer during transaction close")
}