Files
tasks/chasm/fields_iterator_test.go
Alex ShtinandGitHub 6943eb4b5f CHASM: follow up collection changes (#7795)
## What changed?
Follow up to #7761:
1. Use more concrete types instead of `comparable`.
2. Use `softassert` for "compile" time errors.
3. Rename `chasm.Collection` to `chasm.Map` but left proto
`CollectionAttributes` intact. This will allow to add support for other
collection type in future (slice, array).

## Why?
It is better to narrow key type as much as possible. Other types are not
supported anyway.

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

198 lines
5.5 KiB
Go

package chasm
import (
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"go.uber.org/mock/gomock"
)
type fieldsIteratorSuite struct {
suite.Suite
*require.Assertions
controller *gomock.Controller
}
func TestFieldsIteratorSuite(t *testing.T) {
suite.Run(t, new(fieldsIteratorSuite))
}
func (s *fieldsIteratorSuite) SetupTest() {
s.initAssertions()
s.controller = gomock.NewController(s.T())
}
func (s *fieldsIteratorSuite) SetupSubTest() {
s.initAssertions()
}
func (s *fieldsIteratorSuite) 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())
}
func (s *fieldsIteratorSuite) TestGenericTypePrefix() {
tests := []struct {
name string
input any
expected string
}{
{
name: "Field type",
input: Field[string]{},
expected: chasmFieldTypePrefix,
},
{
name: "Map type",
input: Map[string, int]{},
expected: chasmMapTypePrefix,
},
{
name: "Non-generic type",
input: 0,
expected: "",
},
{
name: "Map type",
input: map[string]int{},
expected: "map[",
},
}
for _, tt := range tests {
s.Run(tt.name, func() {
typ := reflect.TypeOf(tt.input)
result := genericTypePrefix(typ)
s.Equal(tt.expected, result)
})
}
}
func (s *fieldsIteratorSuite) TestChasmFieldTypePrefix() {
f := Field[any]{}
fT := reflect.TypeOf(f)
s.True(strings.HasPrefix(fT.String(), chasmFieldTypePrefix))
}
func (s *fieldsIteratorSuite) TestChasmMapTypePrefix() {
c := Map[string, any]{}
cT := reflect.TypeOf(c)
s.True(strings.HasPrefix(cT.String(), chasmMapTypePrefix))
}
func (s *fieldsIteratorSuite) TestFieldsOf() {
type fieldPointer struct {
DataField *protoMessageType
InvalidField *Field[string]
}
type noDataField struct {
SubField Field[string]
SubMap Map[string, int]
}
type twoDataFields struct {
DataField *protoMessageType
AnotherDataField *protoMessageType
}
type unimplementedComponentOnly struct {
UnimplementedComponent
}
tests := []struct {
name string
input any
expectedKinds []fieldKind
expectedNames []string
expectedTypes []string
expectedErrors []string
}{
{
name: "Valid component with one data field",
input: &struct {
UnimplementedComponent
DataField *protoMessageType
SubField Field[string]
SubMap Map[string, int]
}{},
expectedKinds: []fieldKind{fieldKindData, fieldKindSubField, fieldKindSubMap},
expectedNames: []string{"DataField", "SubField", "SubMap"},
expectedTypes: []string{"*persistence.WorkflowExecutionState", "chasm.Field[string]", "chasm.Map[string,int]"},
expectedErrors: []string{"", "", ""},
},
{
name: "Component with no data field",
input: &noDataField{},
expectedKinds: []fieldKind{fieldKindSubField, fieldKindSubMap, fieldKindUnspecified},
expectedNames: []string{"SubField", "SubMap", ""},
expectedTypes: []string{"chasm.Field[string]", "chasm.Map[string,int]", ""},
expectedErrors: []string{"", "", "*chasm.noDataField: no data field (implements proto.Message) found"},
},
{
name: "Component with *Field",
input: &fieldPointer{},
expectedKinds: []fieldKind{fieldKindData, fieldKindUnspecified},
expectedNames: []string{"DataField", "InvalidField"},
expectedTypes: []string{"*persistence.WorkflowExecutionState", "*chasm.Field[string]"},
expectedErrors: []string{"", "*chasm.fieldPointer.InvalidField: chasm field type *chasm.Field[string] must not be a pointer"},
},
{
name: "Component with multiple data fields",
input: &twoDataFields{},
expectedKinds: []fieldKind{fieldKindData, fieldKindData},
expectedNames: []string{"DataField", "AnotherDataField"},
expectedTypes: []string{"*persistence.WorkflowExecutionState", "*persistence.WorkflowExecutionState"},
expectedErrors: []string{"", "*chasm.twoDataFields.AnotherDataField: only one data field DataField (implements proto.Message) allowed in component"},
},
{
name: "Component with UnimplementedComponent only",
input: &unimplementedComponentOnly{},
expectedKinds: []fieldKind{fieldKindUnspecified},
expectedNames: []string{""},
expectedTypes: []string{""},
expectedErrors: []string{"*chasm.unimplementedComponentOnly: no data field (implements proto.Message) found"},
},
}
for _, tt := range tests {
s.Run(tt.name, func() {
valueV := reflect.ValueOf(tt.input)
var actualKinds []fieldKind
var actualNames []string
var actualTypes []string
var actualErrors []string
for field := range fieldsOf(valueV) {
actualKinds = append(actualKinds, field.kind)
actualNames = append(actualNames, field.name)
if field.typ != nil {
actualTypes = append(actualTypes, field.typ.String())
} else {
actualTypes = append(actualTypes, "")
}
if field.err != nil {
actualErrors = append(actualErrors, field.err.Error())
} else {
actualErrors = append(actualErrors, "")
}
}
s.Equal(tt.expectedKinds, actualKinds)
s.Equal(tt.expectedNames, actualNames)
s.Equal(tt.expectedTypes, actualTypes)
s.Equal(tt.expectedErrors, actualErrors)
})
}
}