diff --git a/go.mod b/go.mod index 82db420e6d..d00c233e63 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( github.com/multiformats/go-multibase v0.2.0 github.com/multiformats/go-multicodec v0.9.0 github.com/multiformats/go-multihash v0.2.3 + github.com/onsi/gomega v1.36.2 github.com/pelletier/go-toml v1.9.5 github.com/philippgille/chromem-go v0.7.0 github.com/pkg/errors v0.9.1 diff --git a/tests/integration/encryption/peer_test.go b/tests/integration/encryption/peer_test.go index 480f0a66c9..c452c1155c 100644 --- a/tests/integration/encryption/peer_test.go +++ b/tests/integration/encryption/peer_test.go @@ -140,7 +140,7 @@ func TestDocEncryptionPeer_IfPeerDidNotReceiveKey_ShouldNotFetch(t *testing.T) { } }`, Results: map[string]any{ - "Users": testUtils.AnyOf{ + "Users": testUtils.AnyOf( // The key-sync has not yet completed []map[string]any{}, // The key-sync has completed @@ -149,7 +149,7 @@ func TestDocEncryptionPeer_IfPeerDidNotReceiveKey_ShouldNotFetch(t *testing.T) { "age": int64(21), }, }, - }, + ), }, }, }, diff --git a/tests/integration/mutation/create/embeddings/embedding_test.go b/tests/integration/mutation/create/embeddings/embedding_test.go index 1904a971bd..9a224d40c7 100644 --- a/tests/integration/mutation/create/embeddings/embedding_test.go +++ b/tests/integration/mutation/create/embeddings/embedding_test.go @@ -13,6 +13,7 @@ package constraints import ( "testing" + "github.com/onsi/gomega" "github.com/sourcenetwork/immutable" testUtils "github.com/sourcenetwork/defradb/tests/integration" @@ -23,9 +24,9 @@ func TestMutationCreate_WithMultipleEmbeddingFields_ShouldSucceed(t *testing.T) Description: "Simple create mutation with multiple embedding fields", SupportedClientTypes: immutable.Some([]testUtils.ClientType{ // Embedding test with mutations are currently only compatible with the Go client. - // The docID is updated by collection.Create after vector embedding generation and + // The docID is updated by collection. Create after vector embedding generation and // the HTTP and CLI clients don't receive that updated docID. This causes the waitForUpdateEvents - // to fail sinces it receives an update on a docID that wasn't expected. We will look for a solution + // to fail since it receives an update on a docID that wasn't expected. We will look for a solution // and update the test accordingly. testUtils.GoClientType, }), @@ -63,10 +64,16 @@ func TestMutationCreate_WithMultipleEmbeddingFields_ShouldSucceed(t *testing.T) Results: map[string]any{ "User": []map[string]any{ { - "name_v": testUtils.NewArrayDescription[float32](768), + "name_v": gomega.And( + gomega.BeAssignableToTypeOf([]float32{}), + gomega.HaveLen(768), + ), }, { - "name_v": testUtils.NewArrayDescription[float32](768), + "name_v": gomega.And( + gomega.BeAssignableToTypeOf([]float32{}), + gomega.HaveLen(768), + ), }, }, }, diff --git a/tests/integration/mutation/update/embeddings/embedding_test.go b/tests/integration/mutation/update/embeddings/embedding_test.go index 36054cf25d..e6c792f831 100644 --- a/tests/integration/mutation/update/embeddings/embedding_test.go +++ b/tests/integration/mutation/update/embeddings/embedding_test.go @@ -13,6 +13,7 @@ package constraints import ( "testing" + "github.com/onsi/gomega" "github.com/sourcenetwork/immutable" testUtils "github.com/sourcenetwork/defradb/tests/integration" @@ -77,10 +78,16 @@ func TestMutationUpdate_WithMultipleEmbeddingFields_ShouldSucceed(t *testing.T) Results: map[string]any{ "User": []map[string]any{ { - "name_v": testUtils.NewArrayDescription[float32](768), + "name_v": gomega.And( + gomega.BeAssignableToTypeOf([]float32{}), + gomega.HaveLen(768), + ), }, { - "name_v": testUtils.NewArrayDescription[float32](768), + "name_v": gomega.And( + gomega.BeAssignableToTypeOf([]float32{}), + gomega.HaveLen(768), + ), }, }, }, diff --git a/tests/integration/net/simple/peer/with_update_test.go b/tests/integration/net/simple/peer/with_update_test.go index 54c7aeb3de..d1ef43560e 100644 --- a/tests/integration/net/simple/peer/with_update_test.go +++ b/tests/integration/net/simple/peer/with_update_test.go @@ -175,7 +175,7 @@ func TestP2PWithSingleDocumentUpdatePerNode(t *testing.T) { Results: map[string]any{ "Users": []map[string]any{ { - "Age": testUtils.AnyOf{int64(45), int64(60)}, + "Age": testUtils.AnyOf(int64(45), int64(60)), }, }, }, @@ -431,7 +431,7 @@ func TestP2PWithMultipleDocumentUpdatesPerNode(t *testing.T) { Results: map[string]any{ "Users": []map[string]any{ { - "Age": testUtils.AnyOf{int64(47), int64(62)}, + "Age": testUtils.AnyOf(int64(47), int64(62)), }, }, }, @@ -614,7 +614,7 @@ func TestP2PWithMultipleDocumentUpdatesPerNodeWithP2PCollection(t *testing.T) { Results: map[string]any{ "Users": []map[string]any{ { - "Age": testUtils.AnyOf{int64(47), int64(62)}, + "Age": testUtils.AnyOf(int64(47), int64(62)), }, { "Age": int64(60), diff --git a/tests/integration/results.go b/tests/integration/results.go index 2939cfa10c..2465bb5d03 100644 --- a/tests/integration/results.go +++ b/tests/integration/results.go @@ -13,10 +13,14 @@ package tests import ( "encoding/base64" "encoding/json" + "fmt" "testing" "time" - "github.com/ipfs/go-cid" + "github.com/onsi/gomega" + "github.com/onsi/gomega/types" + + cid "github.com/ipfs/go-cid" "github.com/sourcenetwork/immutable" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -24,38 +28,53 @@ import ( "github.com/sourcenetwork/defradb/client" ) -// Validator instances can be substituted in place of concrete values -// and will be asserted on using their [Validate] function instead of -// asserting direct equality. -// -// They may mutate test state. -// -// Todo: This does not currently support chaining/nesting of Validators, -// although we would like that long term: -// https://github.com/sourcenetwork/defradb/issues/3189 -type Validator interface { - Validate(s *state, actualValue any, msgAndArgs ...any) +type stateMatcher struct { + s *state +} + +func (matcher *stateMatcher) SetState(s *state) { + matcher.s = s } // AnyOf may be used as `Results` field where the value may // be one of several values, yet the value of that field must be the same // across all nodes due to strong eventual consistency. -type AnyOf []any +func AnyOf(values ...any) *anyOf { + return &anyOf{ + Values: values, + } +} -var _ Validator = (AnyOf)(nil) +type anyOf struct { + stateMatcher + Values []any +} -// Validate asserts that actual result is equal to at least one of the expected results. -// -// The comparison is relaxed when using client types other than goClientType. -func (a AnyOf) Validate(s *state, actualValue any, msgAndArgs ...any) { - switch s.clientType { +type StateMatcher interface { + types.GomegaMatcher + SetState(s *state) +} + +var _ StateMatcher = (*anyOf)(nil) + +func (matcher *anyOf) Match(actual any) (success bool, err error) { + switch matcher.s.clientType { case HTTPClientType, CLIClientType: - if !areResultsAnyOf(a, actualValue) { - assert.Contains(s.t, a, actualValue, msgAndArgs...) + if !areResultsAnyOf(matcher.Values, actual) { + return gomega.ContainElement(actual).Match(matcher.Values) } default: - assert.Contains(s.t, a, actualValue, msgAndArgs...) + return gomega.ContainElement(actual).Match(matcher.Values) } + return true, nil +} + +func (matcher *anyOf) FailureMessage(actual any) string { + return fmt.Sprintf("Expected\n\t%v\nto be one of\n\t%v", actual, matcher.Values) +} + +func (matcher *anyOf) NegatedFailureMessage(actual any) string { + return fmt.Sprintf("Expected\n\t%v\nnot to be one of\n\t%v", actual, matcher.Values) } // assertResultsEqual asserts that actual result is equal to the expected result. @@ -75,7 +94,7 @@ func assertResultsEqual(t testing.TB, client ClientType, expected any, actual an // areResultsAnyOf returns true if any of the expected results are of equal value. // // Values of type json.Number and immutable.Option will be reduced to their underlying types. -func areResultsAnyOf(expected AnyOf, actual any) bool { +func areResultsAnyOf(expected []any, actual any) bool { for _, v := range expected { if areResultsEqual(v, actual) { return true @@ -93,11 +112,18 @@ func areResultsAnyOf(expected AnyOf, actual any) bool { // It will also ensure that all Cids described by this [UniqueCid] have the same // valid, Cid value. type UniqueCid struct { - // ID is the arbitrary, but hopefully descriptive, id of this [UniqueCid]. - ID any + stateMatcher + // id is the arbitrary, but hopefully descriptive, id of this [UniqueCid]. + id any + + valuesMismatch bool + duplicatedID any + castFailed bool + cidDecodeErr error } -var _ Validator = (*UniqueCid)(nil) +// var _ Validator = (*UniqueCid)(nil) +var _ StateMatcher = (*UniqueCid)(nil) // NewUniqueCid creates a new [UniqueCid] of the given arbitrary, but hopefully descriptive, // id. @@ -106,34 +132,62 @@ var _ Validator = (*UniqueCid)(nil) // No other [UniqueCid] ids may describe the same Cid value. func NewUniqueCid(id any) *UniqueCid { return &UniqueCid{ - ID: id, + id: id, } } -func (ucid *UniqueCid) Validate(s *state, actualValue any, msgAndArgs ...any) { +func (matcher *UniqueCid) Match(actual any) (success bool, err error) { isNew := true - for id, value := range s.cids { - if id == ucid.ID { - require.Equal(s.t, value, actualValue) + for id, value := range matcher.s.cids { + if id == matcher.id { + if value != actual { + matcher.valuesMismatch = true + return false, nil + } isNew = false } else { - require.NotEqual(s.t, value, actualValue, "UniqueCid must be unique!", msgAndArgs) + if value == actual { + matcher.duplicatedID = id + return false, nil + } } } if isNew { - value, ok := actualValue.(string) + value, ok := actual.(string) if !ok { - require.Fail(s.t, "UniqueCid actualValue string cast failed") + matcher.castFailed = true + return false, nil } cid, err := cid.Decode(value) if err != nil { - require.NoError(s.t, err) + matcher.cidDecodeErr = err + return false, nil } - s.cids[ucid.ID] = cid.String() + matcher.s.cids[matcher.id] = cid.String() } + + return true, nil +} + +func (matcher *UniqueCid) FailureMessage(actual any) string { + if matcher.valuesMismatch { + return fmt.Sprintf("Expected Cids with the same id %v to match", matcher.id) + } else if matcher.duplicatedID != nil { + return fmt.Sprintf("Expected Cid value to be unique, but ids \"%v\" and \"%v\" point to the same cid: %v", + matcher.id, matcher.duplicatedID, actual) + } else if matcher.castFailed { + return fmt.Sprintf("Actual value is expected to be convertible to a string. Actual: %v", actual) + } else if matcher.cidDecodeErr != nil { + return fmt.Sprintf("Expected actual value to be a valid Cid. Error: %v", matcher.cidDecodeErr) + } + return "" +} + +func (matcher *UniqueCid) NegatedFailureMessage(actual any) string { + panic("UniqueCid cannot be negated") } // areResultsEqual returns true if the expected and actual results are of equal value. diff --git a/tests/integration/test_case.go b/tests/integration/test_case.go index d388fbd2e8..2e7eaaa5ea 100644 --- a/tests/integration/test_case.go +++ b/tests/integration/test_case.go @@ -16,7 +16,6 @@ import ( "github.com/lens-vm/lens/host-go/config/model" "github.com/sourcenetwork/immutable" - "github.com/stretchr/testify/require" "github.com/sourcenetwork/defradb/client" "github.com/sourcenetwork/defradb/net" @@ -814,28 +813,3 @@ type Wait struct { // Duration is the duration to wait. Duration time.Duration } - -// ArrayDescription represents an array field. -// -// The test harness will call the Validate method to ensure that the returned array size and type -// match what is described by this struct. -type ArrayDescription[T any] struct { - // Size of the array - Size int -} - -// NewArrayDescription creates a new [ArrayDescription] instance allowing validation of the array -// characteristics instead of the content of the array itself. -func NewArrayDescription[T any](size int) ArrayDescription[T] { - return ArrayDescription[T]{ - Size: size, - } -} - -func (d ArrayDescription[T]) Validate(s *state, actualValue any, msgAndArgs ...any) { - var expT []T - require.IsType(s.t, expT, actualValue, msgAndArgs) - typedActualValue, ok := actualValue.([]T) - require.True(s.t, ok) - require.Equal(s.t, d.Size, len(typedActualValue), msgAndArgs) -} diff --git a/tests/integration/utils.go b/tests/integration/utils.go index 2263836339..b7002dcd2f 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -24,6 +24,7 @@ import ( "time" "github.com/fxamacker/cbor/v2" + "github.com/onsi/gomega" "github.com/sourcenetwork/corelog" "github.com/sourcenetwork/immutable" "github.com/stretchr/testify/assert" @@ -1983,8 +1984,8 @@ func assertRequestResults( stack, ) - case Validator: - exp.Validate(s, actual) + case gomega.OmegaMatcher: + execGomegaMatcher(exp, s, actual, stack) default: assertResultsEqual( @@ -2029,11 +2030,10 @@ func assertRequestResultDocs( for field, actualValue := range actualDoc { stack.pushMap(field) - pathInfo := fmt.Sprintf("node: %v, path: %s", nodeID, stack) switch expectedValue := expectedDoc[field].(type) { - case Validator: - expectedValue.Validate(s, actualValue, pathInfo) + case gomega.OmegaMatcher: + execGomegaMatcher(expectedValue, s, actualValue, stack) case DocIndex: expectedDocID := s.docIDs[expectedValue.CollectionIndex][expectedValue.Index].String() @@ -2042,7 +2042,7 @@ func assertRequestResultDocs( s.clientType, expectedDocID, actualValue, - pathInfo, + fmt.Sprintf("node: %v, path: %s", nodeID, stack), ) case []map[string]any: actualValueMap := ConvertToArrayOfMaps(s.t, actualValue) @@ -2061,7 +2061,7 @@ func assertRequestResultDocs( s.clientType, expectedValue, actualValue, - pathInfo, + fmt.Sprintf("node: %v, path: %s", nodeID, stack), ) } stack.pop() @@ -2072,6 +2072,20 @@ func assertRequestResultDocs( return false } +func execGomegaMatcher(exp gomega.OmegaMatcher, s *state, actual any, stack *assertStack) { + if stateMatcher, ok := exp.(StateMatcher); ok { + stateMatcher.SetState(s) + } + success, err := exp.Match(actual) + if err != nil { + assert.Fail(s.t, "the matcher exited with error", "Error: %s. Path: %s", err, stack) + } + + if !success { + assert.Fail(s.t, exp.FailureMessage(actual), "Path: %s", stack) + } +} + func ConvertToArrayOfMaps(t testing.TB, value any) []map[string]any { valueArrayMap, ok := value.([]map[string]any) if ok {