From aab8d2ef2cc5a743f84b0ff834231faa988ef653 Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Fri, 25 Aug 2023 15:29:43 +0200 Subject: [PATCH 1/3] RUBY-3242 Add spec tests for runCommand --- spec/runners/unified/assertions.rb | 16 +- .../data/run_command_unified/runCommand.yml | 319 ++++++++++++++++++ spec/spec_tests/run_command_unified_spec.rb | 13 + 3 files changed, 345 insertions(+), 3 deletions(-) create mode 100644 spec/spec_tests/data/run_command_unified/runCommand.yml create mode 100644 spec/spec_tests/run_command_unified_spec.rb diff --git a/spec/runners/unified/assertions.rb b/spec/runners/unified/assertions.rb index 722b11ad83..a802597861 100644 --- a/spec/runners/unified/assertions.rb +++ b/spec/runners/unified/assertions.rb @@ -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 @@ -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 @@ -290,6 +290,16 @@ def assert_matches(actual, expected, msg) end end + def get_actual_value(actual, key) + if Hash === actual + actual[key] + elsif Mongo::Operation::Result === actual + 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) } diff --git a/spec/spec_tests/data/run_command_unified/runCommand.yml b/spec/spec_tests/data/run_command_unified/runCommand.yml new file mode 100644 index 0000000000..9b0bf1ad63 --- /dev/null +++ b/spec/spec_tests/data/run_command_unified/runCommand.yml @@ -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 diff --git a/spec/spec_tests/run_command_unified_spec.rb b/spec/spec_tests/run_command_unified_spec.rb new file mode 100644 index 0000000000..50878ca4fe --- /dev/null +++ b/spec/spec_tests/run_command_unified_spec.rb @@ -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 From 224024b8e243e3bf9484929bf60b2aba44b82ecd Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Fri, 25 Aug 2023 16:17:13 +0200 Subject: [PATCH 2/3] Fix runner --- spec/runners/unified/assertions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/runners/unified/assertions.rb b/spec/runners/unified/assertions.rb index a802597861..7077f051cd 100644 --- a/spec/runners/unified/assertions.rb +++ b/spec/runners/unified/assertions.rb @@ -293,7 +293,7 @@ def assert_matches(actual, expected, msg) def get_actual_value(actual, key) if Hash === actual actual[key] - elsif Mongo::Operation::Result === actual + elsif Mongo::Operation::Result === actual && !actual.respond_to?(key.to_sym) actual.documents.first[key] else actual.send(key) From 4bca699cef6684bb62a9f585a55519ec5c908b60 Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Fri, 25 Aug 2023 17:26:46 +0200 Subject: [PATCH 3/3] Add a bit of a comment --- spec/runners/unified/assertions.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/runners/unified/assertions.rb b/spec/runners/unified/assertions.rb index 7077f051cd..908fe80fbb 100644 --- a/spec/runners/unified/assertions.rb +++ b/spec/runners/unified/assertions.rb @@ -290,6 +290,9 @@ 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]