Skip to content

RUBY-3242 Add spec tests for runCommand #2776

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions spec/runners/unified/assertions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,11 @@ def assert_matches(actual, expected, msg)
end
when Hash
if expected.keys == %w($$unsetOrMatches) && expected.values.first.keys == %w(insertedId)
actual_v = actual.inserted_id
actual_v = get_actual_value(actual, 'inserted_id')
expected_v = expected.values.first.values.first
assert_value_matches(actual_v, expected_v, 'inserted_id')
elsif expected.keys == %w(insertedId)
actual_v = actual.inserted_id
actual_v = get_actual_value(actual, 'inserted_id')
expected_v = expected.values.first
assert_value_matches(actual_v, expected_v, 'inserted_id')
else
Expand All @@ -270,7 +270,7 @@ def assert_matches(actual, expected, msg)
if k.start_with?('$$')
assert_value_matches(actual, expected, k)
else
actual_v = actual[k]
actual_v = get_actual_value(actual, k)
if Hash === expected_v && expected_v.length == 1 && expected_v.keys.first.start_with?('$$')
assert_value_matches(actual_v, expected_v, k)
else
Expand All @@ -290,6 +290,19 @@ def assert_matches(actual, expected, msg)
end
end

# The actual value may be of different types depending on the operation.
# In order to avoid having to write a lot of code to handle the different
# types, we use this method to get the actual value.
def get_actual_value(actual, key)
if Hash === actual
actual[key]
elsif Mongo::Operation::Result === actual && !actual.respond_to?(key.to_sym)
actual.documents.first[key]
else
actual.send(key)
end
end

def assert_type(object, type)
ok = [*type].reduce(false) { |acc, x| acc || type_matches?(object, x) }

Expand Down
319 changes: 319 additions & 0 deletions spec/spec_tests/data/run_command_unified/runCommand.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
description: runCommand

schemaVersion: "1.3"

createEntities:
- client:
id: &client client
useMultipleMongoses: false
observeEvents: [commandStartedEvent]
- database:
id: &db db
client: *client
databaseName: *db
- collection:
id: &collection collection
database: *db
collectionName: *collection
- database:
id: &dbWithRC dbWithRC
client: *client
databaseName: *dbWithRC
databaseOptions:
readConcern: { level: 'local' }
- database:
id: &dbWithWC dbWithWC
client: *client
databaseName: *dbWithWC
databaseOptions:
writeConcern: { w: 0 }
- session:
id: &session session
client: *client
# Stable API test
- client:
id: &clientWithStableApi clientWithStableApi
observeEvents: [commandStartedEvent]
serverApi:
version: "1"
strict: true
- database:
id: &dbWithStableApi dbWithStableApi
client: *clientWithStableApi
databaseName: *dbWithStableApi

initialData:
- collectionName: *collection
databaseName: *db
documents: []

tests:
- description: always attaches $db and implicit lsid to given command and omits default readPreference
operations:
- name: runCommand
object: *db
arguments:
commandName: ping
command: { ping: 1 }
expectResult: { ok: 1 }
expectEvents:
- client: *client
events:
- commandStartedEvent:
command:
ping: 1
$db: *db
lsid: { $$exists: true }
$readPreference: { $$exists: false }
commandName: ping

- description: always gossips the $clusterTime on the sent command
runOnRequirements:
# Only replicasets and sharded clusters have a $clusterTime
- topologies: [ replicaset, sharded ]
operations:
# We have to run one command to obtain a clusterTime to gossip
- name: runCommand
object: *db
arguments:
commandName: ping
command: { ping: 1 }
expectResult: { ok: 1 }
- name: runCommand
object: *db
arguments:
commandName: ping
command: { ping: 1 }
expectResult: { ok: 1 }
expectEvents:
- client: *client
events:
- commandStartedEvent:
commandName: ping
# Only check the shape of the second ping which should have the $clusterTime received from the first operation
- commandStartedEvent:
command:
ping: 1
$clusterTime: { $$exists: true }
commandName: ping

- description: attaches the provided session lsid to given command
operations:
- name: runCommand
object: *db
arguments:
commandName: ping
command: { ping: 1 }
session: *session
expectResult: { ok: 1 }
expectEvents:
- client: *client
events:
- commandStartedEvent:
command:
ping: 1
lsid: { $$sessionLsid: *session }
$db: *db
commandName: ping

- description: attaches the provided $readPreference to given command
runOnRequirements:
# Exclude single topology, which is most likely a standalone server
- topologies: [ replicaset, load-balanced, sharded ]
operations:
- name: runCommand
object: *db
arguments:
commandName: ping
command: { ping: 1 }
readPreference: &readPreference { mode: 'nearest' }
expectResult: { ok: 1 }
expectEvents:
- client: *client
events:
- commandStartedEvent:
command:
ping: 1
$readPreference: *readPreference
$db: *db
commandName: ping

- description: does not attach $readPreference to given command on standalone
runOnRequirements:
# This test assumes that the single topology contains a standalone server;
# however, it is possible for a single topology to contain a direct
# connection to another server type.
# See: https://github.com/mongodb/specifications/blob/master/source/server-selection/server-selection.rst#topology-type-single
- topologies: [ single ]
operations:
- name: runCommand
object: *db
arguments:
commandName: ping
command: { ping: 1 }
readPreference: { mode: 'nearest' }
expectResult: { ok: 1 }
expectEvents:
- client: *client
events:
- commandStartedEvent:
command:
ping: 1
$readPreference: { $$exists: false }
$db: *db
commandName: ping

- description: does not attach primary $readPreference to given command
operations:
- name: runCommand
object: *db
arguments:
commandName: ping
command: { ping: 1 }
readPreference: { mode: 'primary' }
expectResult: { ok: 1 }
expectEvents:
- client: *client
events:
- commandStartedEvent:
command:
ping: 1
$readPreference: { $$exists: false }
$db: *db
commandName: ping

- description: does not inherit readConcern specified at the db level
operations:
- name: runCommand
object: *dbWithRC
# Test with a command that supports a readConcern option.
# expectResult is intentionally omitted because some drivers
# may automatically convert command responses into cursors.
arguments:
commandName: aggregate
command: { aggregate: *collection, pipeline: [], cursor: {} }
expectEvents:
- client: *client
events:
- commandStartedEvent:
command:
aggregate: *collection
readConcern: { $$exists: false }
$db: *dbWithRC
commandName: aggregate

- description: does not inherit writeConcern specified at the db level
operations:
- name: runCommand
object: *dbWithWC
arguments:
commandName: insert
command:
insert: *collection
documents: [ { foo: 'bar' } ]
ordered: true
expectResult: { ok: 1 }
expectEvents:
- client: *client
events:
- commandStartedEvent:
command:
insert: *collection
writeConcern: { $$exists: false }
$db: *dbWithWC
commandName: insert

- description: does not retry retryable errors on given command
runOnRequirements:
- minServerVersion: "4.2"
operations:
- name: failPoint
object: testRunner
arguments:
client: *client
failPoint:
configureFailPoint: failCommand
mode: { times: 1 }
data:
failCommands: [ping]
closeConnection: true
- name: runCommand
object: *db
arguments:
commandName: ping
command: { ping: 1 }
expectError:
isClientError: true

- description: attaches transaction fields to given command
runOnRequirements:
- minServerVersion: "4.0"
topologies: [ replicaset ]
- minServerVersion: "4.2"
topologies: [ sharded, load-balanced ]
operations:
- name: withTransaction
object: *session
arguments:
callback:
- name: runCommand
object: *db
arguments:
session: *session
commandName: insert
command:
insert: *collection
documents: [ { foo: 'transaction' } ]
ordered: true
expectResult: { $$unsetOrMatches: { insertedId: { $$unsetOrMatches: 1 } } }
expectEvents:
- client: *client
events:
- commandStartedEvent:
command:
insert: *collection
documents: [ { foo: 'transaction' } ]
ordered: true
lsid: { $$sessionLsid: *session }
txnNumber: 1
startTransaction: true
autocommit: false
# omitted fields
readConcern: { $$exists: false }
writeConcern: { $$exists: false }
commandName: insert
databaseName: *db
- commandStartedEvent:
command:
commitTransaction: 1
lsid: { $$sessionLsid: *session }
txnNumber: 1
autocommit: false
# omitted fields
writeConcern: { $$exists: false }
readConcern: { $$exists: false }
commandName: commitTransaction
databaseName: admin

- description: attaches apiVersion fields to given command when stableApi is configured on the client
runOnRequirements:
- minServerVersion: "5.0"
operations:
- name: runCommand
object: *dbWithStableApi
arguments:
commandName: ping
command:
ping: 1
expectResult: { ok: 1 }
expectEvents:
- client: *clientWithStableApi
events:
- commandStartedEvent:
command:
ping: 1
$db: *dbWithStableApi
apiVersion: "1"
apiStrict: true
apiDeprecationErrors: { $$unsetOrMatches: false }
commandName: ping
13 changes: 13 additions & 0 deletions spec/spec_tests/run_command_unified_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true
# rubocop:todo all

require 'spec_helper'

require 'runners/unified'

base = "#{CURRENT_PATH}/spec_tests/data/run_command_unified"
RUN_COMMAND_UNIFIED_TESTS = Dir.glob("#{base}/**/*.yml").sort

describe 'runCommand unified spec tests' do
define_unified_spec_tests(base, RUN_COMMAND_UNIFIED_TESTS)
end