diff --git a/data/unified-test-format/valid-pass/entity-cursor-iterateOnce.json b/data/unified-test-format/valid-pass/entity-cursor-iterateOnce.json new file mode 100644 index 0000000000..dd9db29799 --- /dev/null +++ b/data/unified-test-format/valid-pass/entity-cursor-iterateOnce.json @@ -0,0 +1,108 @@ +{ + "description": "entity-cursor-iterateOnce", + "schemaVersion": "1.5", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "databaseName": "database0", + "collectionName": "coll0", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ] + } + ], + "tests": [ + { + "description": "iterateOnce", + "operations": [ + { + "name": "createFindCursor", + "object": "collection0", + "arguments": { + "filter": {}, + "batchSize": 2 + }, + "saveResultAsEntity": "cursor0" + }, + { + "name": "iterateUntilDocumentOrError", + "object": "cursor0", + "expectResult": { + "_id": 1 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "cursor0", + "expectResult": { + "_id": 2 + } + }, + { + "name": "iterateOnce", + "object": "cursor0" + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "coll0", + "filter": {}, + "batchSize": 2 + }, + "commandName": "find", + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": "long" + }, + "collection": "coll0" + }, + "commandName": "getMore" + } + } + ] + } + ] + } + ] +} diff --git a/data/unified-test-format/valid-pass/entity-cursor-iterateOnce.yml b/data/unified-test-format/valid-pass/entity-cursor-iterateOnce.yml new file mode 100644 index 0000000000..bc8b9240a5 --- /dev/null +++ b/data/unified-test-format/valid-pass/entity-cursor-iterateOnce.yml @@ -0,0 +1,58 @@ +description: entity-cursor-iterateOnce + +schemaVersion: '1.5' + +createEntities: + - client: + id: &client0 client0 + observeEvents: [ commandStartedEvent ] + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name database0 + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name coll0 + +initialData: + - databaseName: *database0Name + collectionName: *collection0Name + documents: + - _id: 1 + - _id: 2 + - _id: 3 + +tests: + - description: iterateOnce + operations: + - name: createFindCursor + object: *collection0 + arguments: + filter: {} + batchSize: 2 + saveResultAsEntity: &cursor0 cursor0 + - name: iterateUntilDocumentOrError + object: *cursor0 + expectResult: { _id: 1 } + - name: iterateUntilDocumentOrError + object: *cursor0 + expectResult: { _id: 2 } + # This operation could be iterateUntilDocumentOrError, but we use iterateOne to ensure that drivers support it. + - name: iterateOnce + object: *cursor0 + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + find: *collection0Name + filter: {} + batchSize: 2 + commandName: find + databaseName: *database0Name + - commandStartedEvent: + command: + getMore: { $$type: long } + collection: *collection0Name + commandName: getMore diff --git a/data/unified-test-format/valid-pass/initialCollectionData-collectionOptions.json b/data/unified-test-format/valid-pass/initialCollectionData-collectionOptions.json new file mode 100644 index 0000000000..c68030b066 --- /dev/null +++ b/data/unified-test-format/valid-pass/initialCollectionData-collectionOptions.json @@ -0,0 +1,68 @@ +{ + "description": "initialCollectionData-collectionOptions", + "schemaVersion": "1.5", + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "database0", + "collectionOptions": { + "capped": true, + "size": 512 + }, + "documents": [ + { + "_id": 1, + "x": 11 + } + ] + } + ], + "tests": [ + { + "description": "collection is created with the correct options", + "runOnRequirements": [ + { + "minServerVersion": "3.6" + } + ], + "operations": [ + { + "name": "runCommand", + "object": "database0", + "arguments": { + "commandName": "collStats", + "command": { + "collStats": "coll0", + "scale": 1 + } + }, + "expectResult": { + "capped": true, + "maxSize": 512 + } + } + ] + } + ] +} diff --git a/data/unified-test-format/valid-pass/initialCollectionData-collectionOptions.yml b/data/unified-test-format/valid-pass/initialCollectionData-collectionOptions.yml new file mode 100644 index 0000000000..2f66727d40 --- /dev/null +++ b/data/unified-test-format/valid-pass/initialCollectionData-collectionOptions.yml @@ -0,0 +1,41 @@ +description: initialCollectionData-collectionOptions + +schemaVersion: '1.5' + +createEntities: + - client: + id: &client0 client0 + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name database0 + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name coll0 + +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + collectionOptions: + capped: true + size: &cappedSize 512 + documents: + - { _id: 1, x: 11 } + +tests: + - description: collection is created with the correct options + runOnRequirements: + - minServerVersion: "3.6" + operations: + # Execute a collStats command to ensure the collection was created with the correct options. + - name: runCommand + object: *database0 + arguments: + commandName: collStats + command: + collStats: *collection0Name + scale: 1 + expectResult: + capped: true + maxSize: *cappedSize diff --git a/data/unified-test-format/valid-pass/matches-lte-operator.json b/data/unified-test-format/valid-pass/matches-lte-operator.json new file mode 100644 index 0000000000..04136c17e0 --- /dev/null +++ b/data/unified-test-format/valid-pass/matches-lte-operator.json @@ -0,0 +1,78 @@ +{ + "description": "matches-lte-operator", + "schemaVersion": "1.5", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0Name" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "database0Name", + "documents": [] + } + ], + "tests": [ + { + "description": "special lte matching operator", + "operations": [ + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "y": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "coll0", + "documents": [ + { + "_id": { + "$$lte": 1 + }, + "y": { + "$$lte": 2 + } + } + ] + }, + "commandName": "insert", + "databaseName": "database0Name" + } + } + ] + } + ] + } + ] +} diff --git a/data/unified-test-format/valid-pass/matches-lte-operator.yml b/data/unified-test-format/valid-pass/matches-lte-operator.yml new file mode 100644 index 0000000000..598f1056cc --- /dev/null +++ b/data/unified-test-format/valid-pass/matches-lte-operator.yml @@ -0,0 +1,40 @@ +description: matches-lte-operator + +schemaVersion: '1.5' + +createEntities: + - client: + id: &client0 client0 + observeEvents: [ commandStartedEvent ] + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name database0Name + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name coll0 + +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + documents: [] + +tests: + - description: special lte matching operator + operations: + - name: insertOne + object: *collection0 + arguments: + document: { _id : 1, y: 1 } + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + insert: *collection0Name + documents: + # We can make exact assertions here but we use the $$lte operator to ensure drivers support it. + - { _id: { $$lte: 1 }, y: { $$lte: 2 } } + commandName: insert + databaseName: *database0Name diff --git a/mongo/integration/unified/collection_data.go b/mongo/integration/unified/collection_data.go index 93dcb0e6e9..d9e2cc6a71 100644 --- a/mongo/integration/unified/collection_data.go +++ b/mongo/integration/unified/collection_data.go @@ -20,22 +20,43 @@ import ( ) type collectionData struct { - DatabaseName string `bson:"databaseName"` - CollectionName string `bson:"collectionName"` - Documents []bson.Raw `bson:"documents"` + DatabaseName string `bson:"databaseName"` + CollectionName string `bson:"collectionName"` + Documents []bson.Raw `bson:"documents"` + Options *collectionDataOptions `bson:"collectionOptions"` +} + +type collectionDataOptions struct { + Capped *bool `bson:"capped"` + SizeInBytes *int64 `bson:"size"` } // createCollection configures the collection represented by the receiver using the internal client. This function -// first drops the collection and then creates it and inserts the seed data if needed. +// first drops the collection and then creates it with specified options (if any) and inserts the seed data if needed. func (c *collectionData) createCollection(ctx context.Context) error { - db := mtest.GlobalClient().Database(c.DatabaseName) - coll := db.Collection(c.CollectionName, options.Collection().SetWriteConcern(mtest.MajorityWc)) + db := mtest.GlobalClient().Database(c.DatabaseName, options.Database().SetWriteConcern(mtest.MajorityWc)) + coll := db.Collection(c.CollectionName) if err := coll.Drop(ctx); err != nil { return fmt.Errorf("error dropping collection: %v", err) } - // If no data is given, create the collection with write concern "majority". - if len(c.Documents) == 0 { + // Explicitly create collection if Options are specified. + if c.Options != nil { + createOpts := options.CreateCollection() + if c.Options.Capped != nil { + createOpts = createOpts.SetCapped(*c.Options.Capped) + } + if c.Options.SizeInBytes != nil { + createOpts = createOpts.SetSizeInBytes(*c.Options.SizeInBytes) + } + + if err := db.CreateCollection(ctx, c.CollectionName, createOpts); err != nil { + return fmt.Errorf("error creating collection: %v", err) + } + } + + // If neither documents nor options are provided, still create the collection with write concern "majority". + if len(c.Documents) == 0 && c.Options == nil { // The write concern has to be manually specified in the command document because RunCommand does not honor // the database's write concern. create := bson.D{ diff --git a/mongo/integration/unified/cursor_operation_execution.go b/mongo/integration/unified/cursor_operation_execution.go index 609797cc0c..06777660e2 100644 --- a/mongo/integration/unified/cursor_operation_execution.go +++ b/mongo/integration/unified/cursor_operation_execution.go @@ -24,6 +24,26 @@ func executeClose(ctx context.Context, operation *operation) error { return nil } +func executeIterateOnce(ctx context.Context, operation *operation) (*operationResult, error) { + cursor, err := entities(ctx).cursor(operation.Object) + if err != nil { + return nil, err + } + + // TryNext will attempt to get the next document, potentially issuing a single 'getMore'. + if cursor.TryNext(ctx) { + // We don't expect the server to return malformed documents, so any errors from Decode here are treated + // as fatal. + var res bson.Raw + if err := cursor.Decode(&res); err != nil { + return nil, fmt.Errorf("error decoding cursor result: %v", err) + } + + return newDocumentResult(res, nil), nil + } + return newErrorResult(cursor.Err()), nil +} + func executeIterateUntilDocumentOrError(ctx context.Context, operation *operation) (*operationResult, error) { cursor, err := entities(ctx).cursor(operation.Object) if err != nil { diff --git a/mongo/integration/unified/matches.go b/mongo/integration/unified/matches.go index d54cd74a30..ba1583c2ff 100644 --- a/mongo/integration/unified/matches.go +++ b/mongo/integration/unified/matches.go @@ -232,6 +232,22 @@ func evaluateSpecialComparison(ctx context.Context, assertionDoc bson.Raw, actua if !bytes.Equal(expectedID, actualID) { return fmt.Errorf("expected lsid %v, got %v", expectedID, actualID) } + case "$$lte": + if assertionVal.Type != bsontype.Int32 && assertionVal.Type != bsontype.Int64 { + return fmt.Errorf("expected assertionVal to be an Int32 or Int64 but got a %s", assertionVal.Type) + } + if actual.Type != bsontype.Int32 && actual.Type != bsontype.Int64 { + return fmt.Errorf("expected value to be an Int32 or Int64 but got a %s", actual.Type) + } + + // Numeric values can be compared even if their types are different (e.g. if expected is an int32 and actual + // is an int64). + expectedInt64 := assertionVal.AsInt64() + actualInt64 := actual.AsInt64() + if actualInt64 > expectedInt64 { + return fmt.Errorf("expected numeric value %d to be less than or equal %d", actualInt64, expectedInt64) + } + return nil default: return fmt.Errorf("unrecognized special matching assertion %q", assertion) } diff --git a/mongo/integration/unified/operation.go b/mongo/integration/unified/operation.go index 00bc2d54bc..10384f4bb5 100644 --- a/mongo/integration/unified/operation.go +++ b/mongo/integration/unified/operation.go @@ -151,6 +151,8 @@ func (op *operation) run(ctx context.Context, loopDone <-chan struct{}) (*operat // Cursor operations case "close": return newEmptyResult(), executeClose(ctx, op) + case "iterateOnce": + return executeIterateOnce(ctx, op) case "iterateUntilDocumentOrError": return executeIterateUntilDocumentOrError(ctx, op) default: diff --git a/mongo/integration/unified/testrunner_operation.go b/mongo/integration/unified/testrunner_operation.go index a364e1551c..4016d0f3a7 100644 --- a/mongo/integration/unified/testrunner_operation.go +++ b/mongo/integration/unified/testrunner_operation.go @@ -156,6 +156,23 @@ func executeTestRunnerOperation(ctx context.Context, operation *operation, loopD return fmt.Errorf("expected %d connections to be checked out, got %d", expected, actual) } return nil + case "createEntities": + entitiesRaw, err := args.LookupErr("entities") + if err != nil { + return fmt.Errorf("'entities' argument not found in createEntities operation") + } + + var createEntities map[string]*entityOptions + if err := bson.Unmarshal(entitiesRaw.Value, &createEntities); err != nil { + return fmt.Errorf("error unmarshalling 'entities' argument to entityOptions: %v", err) + } + + for entityType, entityOptions := range createEntities { + if err := entities(ctx).addEntity(ctx, entityType, entityOptions); err != nil { + return fmt.Errorf("error creating entity: %v", err) + } + } + return nil default: return fmt.Errorf("unrecognized testRunner operation %q", operation.Name) }