diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index 2499bc884df..a57f6473b6f 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -144,8 +144,8 @@ functions: "create and upload SSDLC release assets": - command: shell.exec - shell: "bash" params: + shell: "bash" working_dir: "src" env: PRODUCT_NAME: ${product_name} @@ -287,7 +287,7 @@ functions: AUTH="${AUTH}" SSL="${SSL}" MONGODB_URI="${MONGODB_URI}" SAFE_FOR_MULTI_MONGOS="${SAFE_FOR_MULTI_MONGOS}" TOPOLOGY="${TOPOLOGY}" \ COMPRESSOR="${COMPRESSOR}" JAVA_VERSION="${JAVA_VERSION}" REQUIRE_API_VERSION=${REQUIRE_API_VERSION} \ - .evergreen/run-tests.sh + TESTS="${TESTS}" .evergreen/run-tests.sh "run load-balancer tests": - command: shell.exec @@ -335,7 +335,7 @@ functions: AUTH="${AUTH}" SSL="${SSL}" MONGODB_URI="${MONGODB_URI}" TOPOLOGY="${TOPOLOGY}" COMPRESSOR="${COMPRESSOR}" \ JAVA_VERSION="${JAVA_VERSION}" \ AZUREKMS_KEY_VAULT_ENDPOINT=${testazurekms_keyvaultendpoint} AZUREKMS_KEY_NAME=${testazurekms_keyname} \ - SLOW_TESTS_ONLY=true .evergreen/run-tests.sh + TESTS="testSlowOnly" .evergreen/run-tests.sh "run scala tests": - command: shell.exec @@ -861,11 +861,42 @@ tasks: vars: file: ".evergreen/static-checks.sh" - - name: "test" + - name: "test-bson-and-crypt" + commands: + - func: "run tests" + vars: + TESTS: 'bson:test bson-record-codec:test mongodb-crypt:test' + + - name: "test-core" + commands: + - func: "bootstrap mongo-orchestration" + - func: "run tests" + vars: + TESTS: 'driver-core:test' + + - name: "test-legacy" commands: - func: "start-kms-kmip-server" - func: "bootstrap mongo-orchestration" - func: "run tests" + vars: + TESTS: 'driver-legacy:test' + + - name: "test-sync" + commands: + - func: "start-kms-kmip-server" + - func: "bootstrap mongo-orchestration" + - func: "run tests" + vars: + TESTS: 'driver-sync:test' + + - name: "test-reactive" + commands: + - func: "start-kms-kmip-server" + - func: "bootstrap mongo-orchestration" + - func: "run tests" + vars: + TESTS: 'driver-reactive-streams:test' - name: load-balancer-test commands: @@ -1826,11 +1857,11 @@ axes: - id: "2.12" display_name: "Scala 2.12" variables: - SCALA: "2.12.15" + SCALA: "2.12.20" - id: "2.13" display_name: "Scala 2.13" variables: - SCALA: "2.13.6" + SCALA: "2.13.15" # Choice of MongoDB storage engine - id: storage-engine @@ -2105,14 +2136,20 @@ buildvariants: display_name: "${version} ${compressor} ${topology} ${auth} ${ssl} ${jdk} ${os} " tags: ["tests-variant"] tasks: - - name: "test" + - name: "test-sync" + - name: "test-reactive" + - name: "test-core" + - name: "test-legacy" - matrix_name: "tests-snappy-compression" matrix_spec: { compressor : "snappy", auth: "noauth", ssl: "nossl", jdk: "jdk8", version: "*", topology: "standalone", os: "linux" } display_name: "${version} ${compressor} ${topology} ${auth} ${ssl} ${jdk} ${os} " tags: ["tests-variant"] tasks: - - name: "test" + - name: "test-sync" + - name: "test-reactive" + - name: "test-core" + - name: "test-legacy" - matrix_name: "tests-zstd-compression" matrix_spec: { compressor : "zstd", auth: "noauth", ssl: "nossl", jdk: "jdk8", @@ -2121,7 +2158,17 @@ buildvariants: display_name: "${version} ${compressor} ${topology} ${auth} ${ssl} ${jdk} ${os} " tags: ["tests-variant"] tasks: - - name: "test" + - name: "test-sync" + - name: "test-reactive" + - name: "test-core" + - name: "test-legacy" + +- matrix_name: "tests-unit" + matrix_spec: { jdk: [ "jdk8", "jdk11", "jdk17", "jdk21"], os: "linux" } + display_name: "${jdk} ${os} Unit" + tags: ["tests-variant"] + tasks: + - name: "test-bson-and-crypt" - matrix_name: "tests-jdk8-unsecure" matrix_spec: { auth: "noauth", ssl: "nossl", jdk: "jdk8", version: ["4.0", "4.2", "4.4", "5.0", "6.0", "7.0", "8.0", "latest"], @@ -2129,7 +2176,10 @@ buildvariants: display_name: "${version} ${topology} ${auth} ${ssl} ${jdk} ${os} " tags: ["tests-variant"] tasks: - - name: "test" + - name: "test-sync" + - name: "test-reactive" + - name: "test-core" + - name: "test-legacy" - matrix_name: "tests-jdk-secure" matrix_spec: { auth: "auth", ssl: "ssl", jdk: [ "jdk8", "jdk17", "jdk21"], @@ -2138,14 +2188,20 @@ buildvariants: display_name: "${version} ${topology} ${auth} ${ssl} ${jdk} ${os} " tags: ["tests-variant"] tasks: - - name: "test" + - name: "test-sync" + - name: "test-reactive" + - name: "test-core" + - name: "test-legacy" - matrix_name: "tests-jdk-secure-jdk11" matrix_spec: { auth: "auth", ssl: "ssl", jdk: ["jdk11"], version: ["7.0"], topology: ["replicaset"], os: "linux" } display_name: "${version} ${topology} ${auth} ${ssl} ${jdk} ${os} " tags: ["tests-variant"] tasks: - - name: "test" + - name: "test-sync" + - name: "test-reactive" + - name: "test-core" + - name: "test-legacy" - matrix_name: "tests-require-api-version" matrix_spec: { api-version: "required", auth: "auth", ssl: "nossl", jdk: ["jdk21"], version: ["5.0", "6.0", "7.0", "8.0", "latest"], @@ -2153,7 +2209,10 @@ buildvariants: display_name: "${version} ${topology} ${api-version} " tags: ["tests-variant"] tasks: - - name: "test" + - name: "test-sync" + - name: "test-reactive" + - name: "test-core" + - name: "test-legacy" - matrix_name: "tests-load-balancer-secure" matrix_spec: { auth: "auth", ssl: "ssl", jdk: ["jdk21"], version: ["5.0", "6.0", "7.0", "8.0", "latest"], topology: "sharded-cluster", diff --git a/.evergreen/run-atlas-search-tests.sh b/.evergreen/run-atlas-search-tests.sh index 7669c87ae5d..36cc981b3f4 100755 --- a/.evergreen/run-atlas-search-tests.sh +++ b/.evergreen/run-atlas-search-tests.sh @@ -16,4 +16,4 @@ echo "Running Atlas Search tests" ./gradlew --stacktrace --info \ -Dorg.mongodb.test.atlas.search=true \ -Dorg.mongodb.test.uri=${MONGODB_URI} \ - driver-core:test --tests AggregatesSearchIntegrationTest + driver-core:test --tests AggregatesSearchIntegrationTest --tests AggregatesVectorSearchIntegrationTest diff --git a/.evergreen/run-graalvm-native-image-app.sh b/.evergreen/run-graalvm-native-image-app.sh index 130b0ef7b4e..ba24ef764a8 100755 --- a/.evergreen/run-graalvm-native-image-app.sh +++ b/.evergreen/run-graalvm-native-image-app.sh @@ -22,4 +22,4 @@ echo "The Gradle version is" ./gradlew --version echo "Building and running the GraalVM native image app" -./gradlew -PjavaVersion=${JAVA_VERSION} -Dorg.mongodb.test.uri=${MONGODB_URI} :graalvm-native-image-app:nativeRun +./gradlew -PincludeGraalvm -PjavaVersion=${JAVA_VERSION} -Dorg.mongodb.test.uri=${MONGODB_URI} :graalvm-native-image-app:nativeRun diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 49390e88d26..f8e90977e36 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -35,6 +35,7 @@ MONGODB_URI=${MONGODB_URI:-} TOPOLOGY=${TOPOLOGY:-server} COMPRESSOR=${COMPRESSOR:-} STREAM_TYPE=${STREAM_TYPE:-nio2} +TESTS=${TESTS:-test} SLOW_TESTS_ONLY=${SLOW_TESTS_ONLY:-false} export ASYNC_TYPE="-Dorg.mongodb.test.async.type=${STREAM_TYPE}" @@ -136,15 +137,8 @@ echo "Running $AUTH tests over $SSL for $TOPOLOGY and connecting to $MONGODB_URI echo "Running tests with Java ${JAVA_VERSION}" ./gradlew -version -if [ "$SLOW_TESTS_ONLY" == "true" ]; then - ./gradlew -PjavaVersion=${JAVA_VERSION} -Dorg.mongodb.test.uri=${MONGODB_URI} \ - ${MULTI_MONGOS_URI_SYSTEM_PROPERTY} ${GRADLE_EXTRA_VARS} ${ASYNC_TYPE} \ - ${JAVA_SYSPROP_NETTY_SSL_PROVIDER} \ - --stacktrace --info testSlowOnly -else - ./gradlew -PjavaVersion=${JAVA_VERSION} -Dorg.mongodb.test.uri=${MONGODB_URI} \ - ${MULTI_MONGOS_URI_SYSTEM_PROPERTY} ${API_VERSION} ${GRADLE_EXTRA_VARS} ${ASYNC_TYPE} \ - ${JAVA_SYSPROP_NETTY_SSL_PROVIDER} \ - -Dorg.mongodb.test.fle.on.demand.credential.test.failure.enabled=true \ - --stacktrace --info --continue test -fi +./gradlew -PjavaVersion=${JAVA_VERSION} -Dorg.mongodb.test.uri=${MONGODB_URI} \ + ${MULTI_MONGOS_URI_SYSTEM_PROPERTY} ${API_VERSION} ${GRADLE_EXTRA_VARS} ${ASYNC_TYPE} \ + ${JAVA_SYSPROP_NETTY_SSL_PROVIDER} \ + -Dorg.mongodb.test.fle.on.demand.credential.test.failure.enabled=true \ + --stacktrace --info --continue ${TESTS} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4724227e0ff..9ab0c31e5ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -78,12 +78,12 @@ jobs: exit 1 # For non-patch releases (A.B.C where C == 0), we expect the release to - # be triggered from master or the A.B.x maintenance branch. This includes + # be triggered from main or the A.B.x maintenance branch. This includes # pre-releases for any non-patch releases, e.g. 5.2.0-beta1 - name: "Fail if non-patch release is created from wrong release branch" - if: ${{ endsWith(env.RELEASE_VERSION_WITHOUT_SUFFIX, '.0') && env.RELEASE_BRANCH != github.ref_name && github.ref_name != 'master' }} + if: ${{ endsWith(env.RELEASE_VERSION_WITHOUT_SUFFIX, '.0') && env.RELEASE_BRANCH != github.ref_name && github.ref_name != 'main' }} run: | - echo '❌ Release failed due to branch mismatch: expected ${{ inputs.version }} to be released from ${{ env.RELEASE_BRANCH }} or master, got ${{ github.ref_name }}' >> $GITHUB_STEP_SUMMARY + echo '❌ Release failed due to branch mismatch: expected ${{ inputs.version }} to be released from ${{ env.RELEASE_BRANCH }} or main, got ${{ github.ref_name }}' >> $GITHUB_STEP_SUMMARY exit 1 # Set commit author information to the user that triggered the release workflow diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/DateTimeSerializers.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/DateTimeSerializers.kt index e3e228ecbfb..7b597135d4f 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/DateTimeSerializers.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/DateTimeSerializers.kt @@ -54,11 +54,16 @@ import org.bson.codecs.kotlinx.utils.SerializationModuleUtils.isClassAvailable public val dateTimeSerializersModule: SerializersModule by lazy { var module = SerializersModule {} if (isClassAvailable("kotlinx.datetime.Instant")) { - module += - InstantAsBsonDateTime.serializersModule + - LocalDateAsBsonDateTime.serializersModule + - LocalDateTimeAsBsonDateTime.serializersModule + - LocalTimeAsBsonDateTime.serializersModule + module += InstantAsBsonDateTime.serializersModule + } + if (isClassAvailable("kotlinx.datetime.LocalDate")) { + module += LocalDateAsBsonDateTime.serializersModule + } + if (isClassAvailable("kotlinx.datetime.LocalDateTime")) { + module += LocalDateTimeAsBsonDateTime.serializersModule + } + if (isClassAvailable("kotlinx.datetime.LocalTime")) { + module += LocalTimeAsBsonDateTime.serializersModule } module } diff --git a/bson-scala/build.gradle b/bson-scala/build.gradle index 6606dec5a89..5d21ed521b5 100644 --- a/bson-scala/build.gradle +++ b/bson-scala/build.gradle @@ -18,7 +18,7 @@ description = "A Scala wrapper / extension to the bson library" archivesBaseName = 'mongo-scala-bson' dependencies { - implementation project(path: ':bson', configuration: 'default') + api project(path: ':bson', configuration: 'default') } sourceSets { diff --git a/bson-scala/src/main/scala/org/mongodb/scala/bson/codecs/macrocodecs/MacroCodec.scala b/bson-scala/src/main/scala/org/mongodb/scala/bson/codecs/macrocodecs/MacroCodec.scala index 090d066223c..e284647af87 100644 --- a/bson-scala/src/main/scala/org/mongodb/scala/bson/codecs/macrocodecs/MacroCodec.scala +++ b/bson-scala/src/main/scala/org/mongodb/scala/bson/codecs/macrocodecs/MacroCodec.scala @@ -22,6 +22,7 @@ import scala.collection.mutable import org.bson._ import org.bson.codecs.configuration.{ CodecRegistries, CodecRegistry } import org.bson.codecs.{ Codec, DecoderContext, Encoder, EncoderContext } +import scala.collection.immutable.Vector import org.mongodb.scala.bson.BsonNull diff --git a/bson-scala/src/test/scala/org/mongodb/scala/bson/codecs/MacrosSpec.scala b/bson-scala/src/test/scala/org/mongodb/scala/bson/codecs/MacrosSpec.scala index 95d7533cc87..c16215a16e8 100644 --- a/bson-scala/src/test/scala/org/mongodb/scala/bson/codecs/MacrosSpec.scala +++ b/bson-scala/src/test/scala/org/mongodb/scala/bson/codecs/MacrosSpec.scala @@ -30,6 +30,7 @@ import org.mongodb.scala.bson.annotations.{ BsonIgnore, BsonProperty } import org.mongodb.scala.bson.codecs.Macros.{ createCodecProvider, createCodecProviderIgnoreNone } import org.mongodb.scala.bson.codecs.Registry.DEFAULT_CODEC_REGISTRY import org.mongodb.scala.bson.collection.immutable.Document +import scala.collection.immutable.Vector import scala.collection.JavaConverters._ import scala.reflect.ClassTag diff --git a/bson/src/main/org/bson/BsonBinary.java b/bson/src/main/org/bson/BsonBinary.java index d5d07273cea..8590c2920be 100644 --- a/bson/src/main/org/bson/BsonBinary.java +++ b/bson/src/main/org/bson/BsonBinary.java @@ -18,10 +18,13 @@ import org.bson.assertions.Assertions; import org.bson.internal.UuidHelper; +import org.bson.internal.vector.VectorHelper; import java.util.Arrays; import java.util.UUID; +import static org.bson.internal.vector.VectorHelper.encodeVectorToBinary; + /** * A representation of the BSON Binary type. Note that for performance reasons instances of this class are not immutable, * so care should be taken to only modify the underlying byte array if you know what you're doing, or else make a defensive copy. @@ -89,6 +92,20 @@ public BsonBinary(final UUID uuid) { this(uuid, UuidRepresentation.STANDARD); } + /** + * Constructs a {@linkplain BsonBinarySubType#VECTOR subtype 9} {@link BsonBinary} from the given {@link Vector}. + * + * @param vector the {@link Vector} + * @since 5.3 + */ + public BsonBinary(final Vector vector) { + if (vector == null) { + throw new IllegalArgumentException("Vector must not be null"); + } + this.data = encodeVectorToBinary(vector); + type = BsonBinarySubType.VECTOR.getValue(); + } + /** * Construct a new instance from the given UUID and UuidRepresentation * @@ -127,6 +144,21 @@ public UUID asUuid() { return UuidHelper.decodeBinaryToUuid(this.data.clone(), this.type, UuidRepresentation.STANDARD); } + /** + * Returns the binary as a {@link Vector}. The {@linkplain #getType() subtype} must be {@linkplain BsonBinarySubType#VECTOR 9}. + * + * @return the vector + * @throws BsonInvalidOperationException if the binary subtype is not {@link BsonBinarySubType#VECTOR}. + * @since 5.3 + */ + public Vector asVector() { + if (type != BsonBinarySubType.VECTOR.getValue()) { + throw new BsonInvalidOperationException("type must be a Vector subtype."); + } + + return VectorHelper.decodeBinaryToVector(this.data); + } + /** * Returns the binary as a UUID. * diff --git a/bson/src/main/org/bson/BsonBinarySubType.java b/bson/src/main/org/bson/BsonBinarySubType.java index fb1b8d0dfbe..7b5948b4efc 100644 --- a/bson/src/main/org/bson/BsonBinarySubType.java +++ b/bson/src/main/org/bson/BsonBinarySubType.java @@ -17,7 +17,7 @@ package org.bson; /** - * The Binary subtype + * The Binary subtype. * * @since 3.0 */ @@ -60,12 +60,28 @@ public enum BsonBinarySubType { ENCRYPTED((byte) 0x06), /** - * Columnar data + * Columnar data. * * @since 4.4 */ COLUMN((byte) 0x07), + /** + * Sensitive data (e.g., HMAC keys) that should be excluded from server-side logging. + * + * @since 5.3 + */ + SENSITIVE((byte) 0x08), + + /** + * Vector data. + * + * @mongodb.server.release 6.0 + * @since 5.3 + * @see Vector + */ + VECTOR((byte) 0x09), + /** * User defined binary data. */ @@ -74,10 +90,10 @@ public enum BsonBinarySubType { private final byte value; /** - * Returns true if the given value is a UUID subtype + * Returns true if the given value is a UUID subtype. * - * @param value the subtype value as a byte - * @return true if value is a UUID subtype + * @param value the subtype value as a byte. + * @return true if value is a UUID subtype. * @since 3.4 */ public static boolean isUuid(final byte value) { diff --git a/bson/src/main/org/bson/Float32Vector.java b/bson/src/main/org/bson/Float32Vector.java new file mode 100644 index 00000000000..9678003b72f --- /dev/null +++ b/bson/src/main/org/bson/Float32Vector.java @@ -0,0 +1,79 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson; + +import java.util.Arrays; + +import static org.bson.assertions.Assertions.assertNotNull; + +/** + * Represents a vector of 32-bit floating-point numbers, where each element in the vector is a float. + *

+ * The {@link Float32Vector} is used to store and retrieve data efficiently using the BSON Binary Subtype 9 format. + * + * @mongodb.server.release 6.0 + * @see Vector#floatVector(float[]) + * @see BsonBinary#BsonBinary(Vector) + * @see BsonBinary#asVector() + * @since 5.3 + */ +public final class Float32Vector extends Vector { + + private final float[] data; + + Float32Vector(final float[] vectorData) { + super(DataType.FLOAT32); + this.data = assertNotNull(vectorData); + } + + /** + * Retrieve the underlying float array representing this {@link Float32Vector}, where each float + * represents an element of a vector. + *

+ * NOTE: The underlying float array is not copied; changes to the returned array will be reflected in this instance. + * + * @return the underlying float array representing this {@link Float32Vector} vector. + */ + public float[] getData() { + return assertNotNull(data); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Float32Vector that = (Float32Vector) o; + return Arrays.equals(data, that.data); + } + + @Override + public int hashCode() { + return Arrays.hashCode(data); + } + + @Override + public String toString() { + return "Float32Vector{" + + "data=" + Arrays.toString(data) + + ", dataType=" + getDataType() + + '}'; + } +} diff --git a/bson/src/main/org/bson/Int8Vector.java b/bson/src/main/org/bson/Int8Vector.java new file mode 100644 index 00000000000..b61e6bfee55 --- /dev/null +++ b/bson/src/main/org/bson/Int8Vector.java @@ -0,0 +1,80 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson; + +import java.util.Arrays; +import java.util.Objects; + +import static org.bson.assertions.Assertions.assertNotNull; + +/** + * Represents a vector of 8-bit signed integers, where each element in the vector is a byte. + *

+ * The {@link Int8Vector} is used to store and retrieve data efficiently using the BSON Binary Subtype 9 format. + * + * @mongodb.server.release 6.0 + * @see Vector#int8Vector(byte[]) + * @see BsonBinary#BsonBinary(Vector) + * @see BsonBinary#asVector() + * @since 5.3 + */ +public final class Int8Vector extends Vector { + + private byte[] data; + + Int8Vector(final byte[] data) { + super(DataType.INT8); + this.data = assertNotNull(data); + } + + /** + * Retrieve the underlying byte array representing this {@link Int8Vector} vector, where each byte represents + * an element of a vector. + *

+ * NOTE: The underlying byte array is not copied; changes to the returned array will be reflected in this instance. + * + * @return the underlying byte array representing this {@link Int8Vector} vector. + */ + public byte[] getData() { + return assertNotNull(data); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Int8Vector that = (Int8Vector) o; + return Objects.deepEquals(data, that.data); + } + + @Override + public int hashCode() { + return Arrays.hashCode(data); + } + + @Override + public String toString() { + return "Int8Vector{" + + "data=" + Arrays.toString(data) + + ", dataType=" + getDataType() + + '}'; + } +} diff --git a/bson/src/main/org/bson/PackedBitVector.java b/bson/src/main/org/bson/PackedBitVector.java new file mode 100644 index 00000000000..a5dd8f4dcdf --- /dev/null +++ b/bson/src/main/org/bson/PackedBitVector.java @@ -0,0 +1,101 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson; + +import java.util.Arrays; +import java.util.Objects; + +import static org.bson.assertions.Assertions.assertNotNull; + +/** + * Represents a packed bit vector, where each element of the vector is represented by a single bit (0 or 1). + *

+ * The {@link PackedBitVector} is used to store data efficiently using the BSON Binary Subtype 9 format. + * + * @mongodb.server.release 6.0 + * @see Vector#packedBitVector(byte[], byte) + * @see BsonBinary#BsonBinary(Vector) + * @see BsonBinary#asVector() + * @since 5.3 + */ +public final class PackedBitVector extends Vector { + + private final byte padding; + private final byte[] data; + + PackedBitVector(final byte[] data, final byte padding) { + super(DataType.PACKED_BIT); + this.data = assertNotNull(data); + this.padding = padding; + } + + /** + * Retrieve the underlying byte array representing this {@link PackedBitVector} vector, where + * each bit represents an element of the vector (either 0 or 1). + *

+ * Note that the {@linkplain #getPadding() padding value} should be considered when interpreting the final byte of the array, + * as it indicates how many least-significant bits are to be ignored. + * + * @return the underlying byte array representing this {@link PackedBitVector} vector. + * @see #getPadding() + */ + public byte[] getData() { + return assertNotNull(data); + } + + /** + * Returns the padding value for this vector. + * + *

Padding refers to the number of least-significant bits in the final byte that are ignored when retrieving + * {@linkplain #getData() the vector array}. For instance, if the padding value is 3, this means that the last byte contains + * 3 least-significant unused bits, which should be disregarded during operations.

+ *

+ * + * NOTE: The underlying byte array is not copied; changes to the returned array will be reflected in this instance. + * + * @return the padding value (between 0 and 7). + */ + public byte getPadding() { + return this.padding; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PackedBitVector that = (PackedBitVector) o; + return padding == that.padding && Arrays.equals(data, that.data); + } + + @Override + public int hashCode() { + return Objects.hash(padding, Arrays.hashCode(data)); + } + + @Override + public String toString() { + return "PackedBitVector{" + + "padding=" + padding + + ", data=" + Arrays.toString(data) + + ", dataType=" + getDataType() + + '}'; + } +} diff --git a/bson/src/main/org/bson/Vector.java b/bson/src/main/org/bson/Vector.java new file mode 100644 index 00000000000..d267387d727 --- /dev/null +++ b/bson/src/main/org/bson/Vector.java @@ -0,0 +1,201 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson; + +import static org.bson.assertions.Assertions.isTrueArgument; +import static org.bson.assertions.Assertions.notNull; + +/** + * Represents a vector that is stored and retrieved using the BSON Binary Subtype 9 format. + * This class supports multiple vector {@link DataType}'s and provides static methods to create + * vectors. + *

+ * Vectors are densely packed arrays of numbers, all the same type, which are stored efficiently + * in BSON using a binary format. + *

+ * NOTE: This class should be treated as sealed: it must not be extended or implemented by consumers of the library. + * + * @mongodb.server.release 6.0 + * @see BsonBinary + * @since 5.3 + */ +public abstract class Vector { + private final DataType dataType; + + Vector(final DataType dataType) { + this.dataType = dataType; + } + + /** + * Creates a vector with the {@link DataType#PACKED_BIT} data type. + *

+ * A {@link DataType#PACKED_BIT} vector is a binary quantized vector where each element of a vector is represented by a single bit (0 or 1). Each byte + * can hold up to 8 bits (vector elements). The padding parameter is used to specify how many least-significant bits in the final byte + * should be ignored.

+ * + *

For example, a vector with two bytes and a padding of 4 would have the following structure:

+ *
+     * Byte 1: 238 (binary: 11101110)
+     * Byte 2: 224 (binary: 11100000)
+     * Padding: 4 (ignore the last 4 bits in Byte 2)
+     * Resulting vector: 12 bits: 111011101110
+     * 
+ *

+ * NOTE: The byte array `data` is not copied; changes to the provided array will be reflected + * in the created {@link PackedBitVector} instance. + * + * @param data The byte array representing the packed bit vector data. Each byte can store 8 bits. + * @param padding The number of least-significant bits (0 to 7) to ignore in the final byte of the vector data. + * @return A {@link PackedBitVector} instance with the {@link DataType#PACKED_BIT} data type. + * @throws IllegalArgumentException If the padding value is greater than 7. + */ + public static PackedBitVector packedBitVector(final byte[] data, final byte padding) { + notNull("data", data); + isTrueArgument("Padding must be between 0 and 7 bits. Provided padding: " + padding, padding >= 0 && padding <= 7); + isTrueArgument("Padding must be 0 if vector is empty. Provided padding: " + padding, padding == 0 || data.length > 0); + return new PackedBitVector(data, padding); + } + + /** + * Creates a vector with the {@link DataType#INT8} data type. + * + *

A {@link DataType#INT8} vector is a vector of 8-bit signed integers where each byte in the vector represents an element of a vector, + * with values in the range [-128, 127].

+ *

+ * NOTE: The byte array `data` is not copied; changes to the provided array will be reflected + * in the created {@link Int8Vector} instance. + * + * @param data The byte array representing the {@link DataType#INT8} vector data. + * @return A {@link Int8Vector} instance with the {@link DataType#INT8} data type. + */ + public static Int8Vector int8Vector(final byte[] data) { + notNull("data", data); + return new Int8Vector(data); + } + + /** + * Creates a vector with the {@link DataType#FLOAT32} data type. + *

+ * A {@link DataType#FLOAT32} vector is a vector of floating-point numbers, where each element in the vector is a float.

+ *

+ * NOTE: The float array `data` is not copied; changes to the provided array will be reflected + * in the created {@link Float32Vector} instance. + * + * @param data The float array representing the {@link DataType#FLOAT32} vector data. + * @return A {@link Float32Vector} instance with the {@link DataType#FLOAT32} data type. + */ + public static Float32Vector floatVector(final float[] data) { + notNull("data", data); + return new Float32Vector(data); + } + + /** + * Returns the {@link PackedBitVector}. + * + * @return {@link PackedBitVector}. + * @throws IllegalStateException if this vector is not of type {@link DataType#PACKED_BIT}. Use {@link #getDataType()} to check the vector + * type before calling this method. + */ + public PackedBitVector asPackedBitVector() { + ensureType(DataType.PACKED_BIT); + return (PackedBitVector) this; + } + + /** + * Returns the {@link Int8Vector}. + * + * @return {@link Int8Vector}. + * @throws IllegalStateException if this vector is not of type {@link DataType#INT8}. Use {@link #getDataType()} to check the vector + * type before calling this method. + */ + public Int8Vector asInt8Vector() { + ensureType(DataType.INT8); + return (Int8Vector) this; + } + + /** + * Returns the {@link Float32Vector}. + * + * @return {@link Float32Vector}. + * @throws IllegalStateException if this vector is not of type {@link DataType#FLOAT32}. Use {@link #getDataType()} to check the vector + * type before calling this method. + */ + public Float32Vector asFloat32Vector() { + ensureType(DataType.FLOAT32); + return (Float32Vector) this; + } + + /** + * Returns {@link DataType} of the vector. + * + * @return the data type of the vector. + */ + public DataType getDataType() { + return this.dataType; + } + + + private void ensureType(final DataType expected) { + if (this.dataType != expected) { + throw new IllegalStateException("Expected vector data type " + expected + ", but found " + this.dataType); + } + } + + /** + * Represents the data type (dtype) of a vector. + *

+ * Each dtype determines how the data in the vector is stored, including how many bits are used to represent each element + * in the vector. + * + * @mongodb.server.release 6.0 + * @since 5.3 + */ + public enum DataType { + /** + * An INT8 vector is a vector of 8-bit signed integers. The vector is stored as an array of bytes, where each byte + * represents a signed integer in the range [-128, 127]. + */ + INT8((byte) 0x03), + /** + * A FLOAT32 vector is a vector of 32-bit floating-point numbers, where each element in the vector is a float. + */ + FLOAT32((byte) 0x27), + /** + * A PACKED_BIT vector is a binary quantized vector where each element of a vector is represented by a single bit (0 or 1). + * Each byte can hold up to 8 bits (vector elements). + */ + PACKED_BIT((byte) 0x10); + + private final byte value; + + DataType(final byte value) { + this.value = value; + } + + /** + * Returns the byte value associated with this {@link DataType}. + * + *

This value is used in the BSON binary format to indicate the data type of the vector.

+ * + * @return the byte value representing the {@link DataType}. + */ + public byte getValue() { + return value; + } + } +} + diff --git a/bson/src/main/org/bson/codecs/ContainerCodecHelper.java b/bson/src/main/org/bson/codecs/ContainerCodecHelper.java index 5969763546b..b454206d5e8 100644 --- a/bson/src/main/org/bson/codecs/ContainerCodecHelper.java +++ b/bson/src/main/org/bson/codecs/ContainerCodecHelper.java @@ -16,10 +16,12 @@ package org.bson.codecs; +import org.bson.BsonBinarySubType; import org.bson.BsonReader; import org.bson.BsonType; import org.bson.Transformer; import org.bson.UuidRepresentation; +import org.bson.Vector; import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.codecs.configuration.CodecRegistry; @@ -28,6 +30,8 @@ import java.util.Arrays; import java.util.UUID; +import static org.bson.internal.UuidHelper.isLegacyUUID; + /** * Helper methods for Codec implementations for containers, e.g. {@code Map} and {@code Iterable}. */ @@ -42,28 +46,50 @@ static Object readValue(final BsonReader reader, final DecoderContext decoderCon reader.readNull(); return null; } else { - Codec codec = bsonTypeCodecMap.get(bsonType); + Codec currentCodec = bsonTypeCodecMap.get(bsonType); + + if (bsonType == BsonType.BINARY) { + byte binarySubType = reader.peekBinarySubType(); + currentCodec = getBinarySubTypeCodec( + reader, + uuidRepresentation, + registry, binarySubType, + currentCodec); + } + + return valueTransformer.transform(currentCodec.decode(reader, decoderContext)); + } + } + + private static Codec getBinarySubTypeCodec(final BsonReader reader, + final UuidRepresentation uuidRepresentation, + final CodecRegistry registry, + final byte binarySubType, + final Codec binaryTypeCodec) { - if (bsonType == BsonType.BINARY && reader.peekBinarySize() == 16) { - switch (reader.peekBinarySubType()) { - case 3: - if (uuidRepresentation == UuidRepresentation.JAVA_LEGACY - || uuidRepresentation == UuidRepresentation.C_SHARP_LEGACY - || uuidRepresentation == UuidRepresentation.PYTHON_LEGACY) { - codec = registry.get(UUID.class); - } - break; - case 4: - if (uuidRepresentation == UuidRepresentation.STANDARD) { - codec = registry.get(UUID.class); - } - break; - default: - break; - } + if (binarySubType == BsonBinarySubType.VECTOR.getValue()) { + Codec vectorCodec = registry.get(Vector.class, registry); + if (vectorCodec != null) { + return vectorCodec; + } + } else if (reader.peekBinarySize() == 16) { + switch (binarySubType) { + case 3: + if (isLegacyUUID(uuidRepresentation)) { + return registry.get(UUID.class); + } + break; + case 4: + if (uuidRepresentation == UuidRepresentation.STANDARD) { + return registry.get(UUID.class); + } + break; + default: + break; } - return valueTransformer.transform(codec.decode(reader, decoderContext)); } + + return binaryTypeCodec; } static Codec getCodec(final CodecRegistry codecRegistry, final Type type) { @@ -77,7 +103,6 @@ static Codec getCodec(final CodecRegistry codecRegistry, final Type type) { } } - private ContainerCodecHelper() { } } diff --git a/bson/src/main/org/bson/codecs/Float32VectorCodec.java b/bson/src/main/org/bson/codecs/Float32VectorCodec.java new file mode 100644 index 00000000000..a6df27e3f87 --- /dev/null +++ b/bson/src/main/org/bson/codecs/Float32VectorCodec.java @@ -0,0 +1,56 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson.codecs; + +import org.bson.BsonBinary; +import org.bson.BsonBinarySubType; +import org.bson.BsonInvalidOperationException; +import org.bson.BsonReader; +import org.bson.BsonWriter; +import org.bson.Float32Vector; + +/** + * Encodes and decodes {@link Float32Vector} objects. + * + */ +final class Float32VectorCodec implements Codec { + + @Override + public void encode(final BsonWriter writer, final Float32Vector vectorToEncode, final EncoderContext encoderContext) { + writer.writeBinaryData(new BsonBinary(vectorToEncode)); + } + + @Override + public Float32Vector decode(final BsonReader reader, final DecoderContext decoderContext) { + byte subType = reader.peekBinarySubType(); + + if (subType != BsonBinarySubType.VECTOR.getValue()) { + throw new BsonInvalidOperationException("Expected vector binary subtype " + BsonBinarySubType.VECTOR.getValue() + " but found: " + subType); + } + + return reader.readBinaryData() + .asBinary() + .asVector() + .asFloat32Vector(); + } + + @Override + public Class getEncoderClass() { + return Float32Vector.class; + } +} + diff --git a/bson/src/main/org/bson/codecs/Int8VectorCodec.java b/bson/src/main/org/bson/codecs/Int8VectorCodec.java new file mode 100644 index 00000000000..a9a70f53746 --- /dev/null +++ b/bson/src/main/org/bson/codecs/Int8VectorCodec.java @@ -0,0 +1,58 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson.codecs; + +import org.bson.BsonBinary; +import org.bson.BsonBinarySubType; +import org.bson.BsonInvalidOperationException; +import org.bson.BsonReader; +import org.bson.BsonWriter; +import org.bson.Int8Vector; + +/** + * Encodes and decodes {@link Int8Vector} objects. + * + * @since 5.3 + */ +final class Int8VectorCodec implements Codec { + + @Override + public void encode(final BsonWriter writer, final Int8Vector vectorToEncode, final EncoderContext encoderContext) { + writer.writeBinaryData(new BsonBinary(vectorToEncode)); + } + + @Override + public Int8Vector decode(final BsonReader reader, final DecoderContext decoderContext) { + byte subType = reader.peekBinarySubType(); + + if (subType != BsonBinarySubType.VECTOR.getValue()) { + throw new BsonInvalidOperationException("Expected vector binary subtype " + BsonBinarySubType.VECTOR.getValue() + " but found: " + subType); + } + + return reader.readBinaryData() + .asBinary() + .asVector() + .asInt8Vector(); + } + + + @Override + public Class getEncoderClass() { + return Int8Vector.class; + } +} + diff --git a/bson/src/main/org/bson/codecs/PackedBitVectorCodec.java b/bson/src/main/org/bson/codecs/PackedBitVectorCodec.java new file mode 100644 index 00000000000..6fcb9552955 --- /dev/null +++ b/bson/src/main/org/bson/codecs/PackedBitVectorCodec.java @@ -0,0 +1,59 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson.codecs; + +import org.bson.BsonBinary; +import org.bson.BsonBinarySubType; +import org.bson.BsonInvalidOperationException; +import org.bson.BsonReader; +import org.bson.BsonWriter; +import org.bson.PackedBitVector; + +/** + * Encodes and decodes {@link PackedBitVector} objects. + * + */ +final class PackedBitVectorCodec implements Codec { + + @Override + public void encode(final BsonWriter writer, final PackedBitVector vectorToEncode, final EncoderContext encoderContext) { + writer.writeBinaryData(new BsonBinary(vectorToEncode)); + } + + @Override + public PackedBitVector decode(final BsonReader reader, final DecoderContext decoderContext) { + byte subType = reader.peekBinarySubType(); + + if (subType != BsonBinarySubType.VECTOR.getValue()) { + throw new BsonInvalidOperationException( + "Expected vector binary subtype " + BsonBinarySubType.VECTOR.getValue() + " but found: " + subType); + } + + return reader.readBinaryData() + .asBinary() + .asVector() + .asPackedBitVector(); + } + + + @Override + public Class getEncoderClass() { + return PackedBitVector.class; + } +} + + diff --git a/bson/src/main/org/bson/codecs/ValueCodecProvider.java b/bson/src/main/org/bson/codecs/ValueCodecProvider.java index 80ec5e6f18d..3a921c1b08a 100644 --- a/bson/src/main/org/bson/codecs/ValueCodecProvider.java +++ b/bson/src/main/org/bson/codecs/ValueCodecProvider.java @@ -42,6 +42,10 @@ *
  • {@link org.bson.codecs.StringCodec}
  • *
  • {@link org.bson.codecs.SymbolCodec}
  • *
  • {@link org.bson.codecs.UuidCodec}
  • + *
  • {@link VectorCodec}
  • + *
  • {@link Float32VectorCodec}
  • + *
  • {@link Int8VectorCodec}
  • + *
  • {@link PackedBitVectorCodec}
  • *
  • {@link org.bson.codecs.ByteCodec}
  • *
  • {@link org.bson.codecs.ShortCodec}
  • *
  • {@link org.bson.codecs.ByteArrayCodec}
  • @@ -86,6 +90,10 @@ private void addCodecs() { addCodec(new StringCodec()); addCodec(new SymbolCodec()); addCodec(new OverridableUuidRepresentationUuidCodec()); + addCodec(new VectorCodec()); + addCodec(new Float32VectorCodec()); + addCodec(new Int8VectorCodec()); + addCodec(new PackedBitVectorCodec()); addCodec(new ByteCodec()); addCodec(new PatternCodec()); diff --git a/bson/src/main/org/bson/codecs/VectorCodec.java b/bson/src/main/org/bson/codecs/VectorCodec.java new file mode 100644 index 00000000000..87d847664dc --- /dev/null +++ b/bson/src/main/org/bson/codecs/VectorCodec.java @@ -0,0 +1,56 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson.codecs; + +import org.bson.BsonBinary; +import org.bson.BsonBinarySubType; +import org.bson.BsonInvalidOperationException; +import org.bson.BsonReader; +import org.bson.BsonWriter; +import org.bson.Vector; + +/** + * Encodes and decodes {@link Vector} objects. + * + */ + final class VectorCodec implements Codec { + + @Override + public void encode(final BsonWriter writer, final Vector vectorToEncode, final EncoderContext encoderContext) { + writer.writeBinaryData(new BsonBinary(vectorToEncode)); + } + + @Override + public Vector decode(final BsonReader reader, final DecoderContext decoderContext) { + byte subType = reader.peekBinarySubType(); + + if (subType != BsonBinarySubType.VECTOR.getValue()) { + throw new BsonInvalidOperationException("Expected vector binary subtype " + BsonBinarySubType.VECTOR.getValue() + " but found " + subType); + } + + return reader.readBinaryData() + .asBinary() + .asVector(); + } + + @Override + public Class getEncoderClass() { + return Vector.class; + } +} + + diff --git a/bson/src/main/org/bson/internal/UuidHelper.java b/bson/src/main/org/bson/internal/UuidHelper.java index efe3d5b5812..9c46614b56e 100644 --- a/bson/src/main/org/bson/internal/UuidHelper.java +++ b/bson/src/main/org/bson/internal/UuidHelper.java @@ -124,6 +124,12 @@ public static UUID decodeBinaryToUuid(final byte[] data, final byte type, final return new UUID(readLongFromArrayBigEndian(localData, 0), readLongFromArrayBigEndian(localData, 8)); } + public static boolean isLegacyUUID(final UuidRepresentation uuidRepresentation) { + return uuidRepresentation == UuidRepresentation.JAVA_LEGACY + || uuidRepresentation == UuidRepresentation.C_SHARP_LEGACY + || uuidRepresentation == UuidRepresentation.PYTHON_LEGACY; + } + private UuidHelper() { } } diff --git a/bson/src/main/org/bson/internal/vector/VectorHelper.java b/bson/src/main/org/bson/internal/vector/VectorHelper.java new file mode 100644 index 00000000000..9dbf583d2b0 --- /dev/null +++ b/bson/src/main/org/bson/internal/vector/VectorHelper.java @@ -0,0 +1,177 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson.internal.vector; + +import org.bson.BsonBinary; +import org.bson.BsonInvalidOperationException; +import org.bson.Float32Vector; +import org.bson.Int8Vector; +import org.bson.PackedBitVector; +import org.bson.Vector; +import org.bson.assertions.Assertions; +import org.bson.types.Binary; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +/** + * Helper class for encoding and decoding vectors to and from {@link BsonBinary}/{@link Binary}. + * + *

    + * This class is not part of the public API and may be removed or changed at any time. + * + * @see Vector + * @see BsonBinary#asVector() + * @see BsonBinary#BsonBinary(Vector) + */ +public final class VectorHelper { + + private static final ByteOrder STORED_BYTE_ORDER = ByteOrder.LITTLE_ENDIAN; + private static final String ERROR_MESSAGE_UNKNOWN_VECTOR_DATA_TYPE = "Unknown vector data type: "; + private static final byte ZERO_PADDING = 0; + + private VectorHelper() { + //NOP + } + + private static final int METADATA_SIZE = 2; + + public static byte[] encodeVectorToBinary(final Vector vector) { + Vector.DataType dataType = vector.getDataType(); + switch (dataType) { + case INT8: + return encodeVector(dataType.getValue(), ZERO_PADDING, vector.asInt8Vector().getData()); + case PACKED_BIT: + PackedBitVector packedBitVector = vector.asPackedBitVector(); + return encodeVector(dataType.getValue(), packedBitVector.getPadding(), packedBitVector.getData()); + case FLOAT32: + return encodeVector(dataType.getValue(), vector.asFloat32Vector().getData()); + default: + throw Assertions.fail(ERROR_MESSAGE_UNKNOWN_VECTOR_DATA_TYPE + dataType); + } + } + + /** + * Decodes a vector from a binary representation. + *

    + * encodedVector is not mutated nor stored in the returned {@link Vector}. + */ + public static Vector decodeBinaryToVector(final byte[] encodedVector) { + isTrue("Vector encoded array length must be at least 2, but found: " + encodedVector.length, encodedVector.length >= METADATA_SIZE); + Vector.DataType dataType = determineVectorDType(encodedVector[0]); + byte padding = encodedVector[1]; + switch (dataType) { + case INT8: + return decodeInt8Vector(encodedVector, padding); + case PACKED_BIT: + return decodePackedBitVector(encodedVector, padding); + case FLOAT32: + return decodeFloat32Vector(encodedVector, padding); + default: + throw Assertions.fail(ERROR_MESSAGE_UNKNOWN_VECTOR_DATA_TYPE + dataType); + } + } + + private static Float32Vector decodeFloat32Vector(final byte[] encodedVector, final byte padding) { + isTrue("Padding must be 0 for FLOAT32 data type, but found: " + padding, padding == 0); + return Vector.floatVector(decodeLittleEndianFloats(encodedVector)); + } + + private static PackedBitVector decodePackedBitVector(final byte[] encodedVector, final byte padding) { + byte[] packedBitVector = extractVectorData(encodedVector); + isTrue("Padding must be 0 if vector is empty, but found: " + padding, padding == 0 || packedBitVector.length > 0); + isTrue("Padding must be between 0 and 7 bits, but found: " + padding, padding >= 0 && padding <= 7); + return Vector.packedBitVector(packedBitVector, padding); + } + + private static Int8Vector decodeInt8Vector(final byte[] encodedVector, final byte padding) { + isTrue("Padding must be 0 for INT8 data type, but found: " + padding, padding == 0); + byte[] int8Vector = extractVectorData(encodedVector); + return Vector.int8Vector(int8Vector); + } + + private static byte[] extractVectorData(final byte[] encodedVector) { + int vectorDataLength = encodedVector.length - METADATA_SIZE; + byte[] vectorData = new byte[vectorDataLength]; + System.arraycopy(encodedVector, METADATA_SIZE, vectorData, 0, vectorDataLength); + return vectorData; + } + + private static byte[] encodeVector(final byte dType, final byte padding, final byte[] vectorData) { + final byte[] bytes = new byte[vectorData.length + METADATA_SIZE]; + bytes[0] = dType; + bytes[1] = padding; + System.arraycopy(vectorData, 0, bytes, METADATA_SIZE, vectorData.length); + return bytes; + } + + private static byte[] encodeVector(final byte dType, final float[] vectorData) { + final byte[] bytes = new byte[vectorData.length * Float.BYTES + METADATA_SIZE]; + + bytes[0] = dType; + bytes[1] = ZERO_PADDING; + + ByteBuffer buffer = ByteBuffer.wrap(bytes); + buffer.order(STORED_BYTE_ORDER); + buffer.position(METADATA_SIZE); + + FloatBuffer floatBuffer = buffer.asFloatBuffer(); + + // The JVM may optimize this operation internally, potentially using intrinsics + // or platform-specific optimizations (such as SIMD). If the byte order matches the underlying system's + // native order, the operation may involve a direct memory copy. + floatBuffer.put(vectorData); + + return bytes; + } + + private static float[] decodeLittleEndianFloats(final byte[] encodedVector) { + isTrue("Byte array length must be a multiple of 4 for FLOAT32 data type, but found: " + encodedVector.length, + (encodedVector.length - METADATA_SIZE) % Float.BYTES == 0); + + int vectorSize = encodedVector.length - METADATA_SIZE; + + int numFloats = vectorSize / Float.BYTES; + float[] floatArray = new float[numFloats]; + + ByteBuffer buffer = ByteBuffer.wrap(encodedVector, METADATA_SIZE, vectorSize); + buffer.order(STORED_BYTE_ORDER); + + // The JVM may optimize this operation internally, potentially using intrinsics + // or platform-specific optimizations (such as SIMD). If the byte order matches the underlying system's + // native order, the operation may involve a direct memory copy. + buffer.asFloatBuffer().get(floatArray); + return floatArray; + } + + public static Vector.DataType determineVectorDType(final byte dType) { + Vector.DataType[] values = Vector.DataType.values(); + for (Vector.DataType value : values) { + if (value.getValue() == dType) { + return value; + } + } + throw new BsonInvalidOperationException(ERROR_MESSAGE_UNKNOWN_VECTOR_DATA_TYPE + dType); + } + + private static void isTrue(final String message, final boolean condition) { + if (!condition) { + throw new BsonInvalidOperationException(message); + } + } +} diff --git a/bson/src/test/resources/bson-binary-vector/float32.json b/bson/src/test/resources/bson-binary-vector/float32.json new file mode 100644 index 00000000000..e1d142c184b --- /dev/null +++ b/bson/src/test/resources/bson-binary-vector/float32.json @@ -0,0 +1,50 @@ +{ + "description": "Tests of Binary subtype 9, Vectors, with dtype FLOAT32", + "test_key": "vector", + "tests": [ + { + "description": "Simple Vector FLOAT32", + "valid": true, + "vector": [127.0, 7.0], + "dtype_hex": "0x27", + "dtype_alias": "FLOAT32", + "padding": 0, + "canonical_bson": "1C00000005766563746F72000A0000000927000000FE420000E04000" + }, + { + "description": "Vector with decimals and negative value FLOAT32", + "valid": true, + "vector": [127.7, -7.7], + "dtype_hex": "0x27", + "dtype_alias": "FLOAT32", + "padding": 0, + "canonical_bson": "1C00000005766563746F72000A0000000927006666FF426666F6C000" + }, + { + "description": "Empty Vector FLOAT32", + "valid": true, + "vector": [], + "dtype_hex": "0x27", + "dtype_alias": "FLOAT32", + "padding": 0, + "canonical_bson": "1400000005766563746F72000200000009270000" + }, + { + "description": "Infinity Vector FLOAT32", + "valid": true, + "vector": ["-inf", 0.0, "inf"], + "dtype_hex": "0x27", + "dtype_alias": "FLOAT32", + "padding": 0, + "canonical_bson": "2000000005766563746F72000E000000092700000080FF000000000000807F00" + }, + { + "description": "FLOAT32 with padding", + "valid": false, + "vector": [127.0, 7.0], + "dtype_hex": "0x27", + "dtype_alias": "FLOAT32", + "padding": 3 + } + ] +} \ No newline at end of file diff --git a/bson/src/test/resources/bson-binary-vector/int8.json b/bson/src/test/resources/bson-binary-vector/int8.json new file mode 100644 index 00000000000..c10c1b7d4e2 --- /dev/null +++ b/bson/src/test/resources/bson-binary-vector/int8.json @@ -0,0 +1,56 @@ +{ + "description": "Tests of Binary subtype 9, Vectors, with dtype INT8", + "test_key": "vector", + "tests": [ + { + "description": "Simple Vector INT8", + "valid": true, + "vector": [127, 7], + "dtype_hex": "0x03", + "dtype_alias": "INT8", + "padding": 0, + "canonical_bson": "1600000005766563746F7200040000000903007F0700" + }, + { + "description": "Empty Vector INT8", + "valid": true, + "vector": [], + "dtype_hex": "0x03", + "dtype_alias": "INT8", + "padding": 0, + "canonical_bson": "1400000005766563746F72000200000009030000" + }, + { + "description": "Overflow Vector INT8", + "valid": false, + "vector": [128], + "dtype_hex": "0x03", + "dtype_alias": "INT8", + "padding": 0 + }, + { + "description": "Underflow Vector INT8", + "valid": false, + "vector": [-129], + "dtype_hex": "0x03", + "dtype_alias": "INT8", + "padding": 0 + }, + { + "description": "INT8 with padding", + "valid": false, + "vector": [127, 7], + "dtype_hex": "0x03", + "dtype_alias": "INT8", + "padding": 3 + }, + { + "description": "INT8 with float inputs", + "valid": false, + "vector": [127.77, 7.77], + "dtype_hex": "0x03", + "dtype_alias": "INT8", + "padding": 0 + } + ] +} \ No newline at end of file diff --git a/bson/src/test/resources/bson-binary-vector/packed_bit.json b/bson/src/test/resources/bson-binary-vector/packed_bit.json new file mode 100644 index 00000000000..69fb3948335 --- /dev/null +++ b/bson/src/test/resources/bson-binary-vector/packed_bit.json @@ -0,0 +1,97 @@ +{ + "description": "Tests of Binary subtype 9, Vectors, with dtype PACKED_BIT", + "test_key": "vector", + "tests": [ + { + "description": "Padding specified with no vector data PACKED_BIT", + "valid": false, + "vector": [], + "dtype_hex": "0x10", + "dtype_alias": "PACKED_BIT", + "padding": 1 + }, + { + "description": "Simple Vector PACKED_BIT", + "valid": true, + "vector": [127, 7], + "dtype_hex": "0x10", + "dtype_alias": "PACKED_BIT", + "padding": 0, + "canonical_bson": "1600000005766563746F7200040000000910007F0700" + }, + { + "description": "Empty Vector PACKED_BIT", + "valid": true, + "vector": [], + "dtype_hex": "0x10", + "dtype_alias": "PACKED_BIT", + "padding": 0, + "canonical_bson": "1400000005766563746F72000200000009100000" + }, + { + "description": "PACKED_BIT with padding", + "valid": true, + "vector": [127, 7], + "dtype_hex": "0x10", + "dtype_alias": "PACKED_BIT", + "padding": 3, + "canonical_bson": "1600000005766563746F7200040000000910037F0700" + }, + { + "description": "Overflow Vector PACKED_BIT", + "valid": false, + "vector": [256], + "dtype_hex": "0x10", + "dtype_alias": "PACKED_BIT", + "padding": 0 + }, + { + "description": "Underflow Vector PACKED_BIT", + "valid": false, + "vector": [-1], + "dtype_hex": "0x10", + "dtype_alias": "PACKED_BIT", + "padding": 0 + }, + { + "description": "Vector with float values PACKED_BIT", + "valid": false, + "vector": [127.5], + "dtype_hex": "0x10", + "dtype_alias": "PACKED_BIT", + "padding": 0 + }, + { + "description": "Padding specified with no vector data PACKED_BIT", + "valid": false, + "vector": [], + "dtype_hex": "0x10", + "dtype_alias": "PACKED_BIT", + "padding": 1 + }, + { + "description": "Exceeding maximum padding PACKED_BIT", + "valid": false, + "vector": [1], + "dtype_hex": "0x10", + "dtype_alias": "PACKED_BIT", + "padding": 8 + }, + { + "description": "Negative padding PACKED_BIT", + "valid": false, + "vector": [1], + "dtype_hex": "0x10", + "dtype_alias": "PACKED_BIT", + "padding": -1 + }, + { + "description": "Vector with float values PACKED_BIT", + "valid": false, + "vector": [127.5], + "dtype_hex": "0x10", + "dtype_alias": "PACKED_BIT", + "padding": 0 + } + ] +} \ No newline at end of file diff --git a/bson/src/test/resources/bson/binary.json b/bson/src/test/resources/bson/binary.json index d3c57ec1081..29d88471afe 100644 --- a/bson/src/test/resources/bson/binary.json +++ b/bson/src/test/resources/bson/binary.json @@ -55,6 +55,11 @@ "canonical_bson": "1D000000057800100000000773FFD26444B34C6990E8E7D1DFC035D400", "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"c//SZESzTGmQ6OfR38A11A==\", \"subType\" : \"07\"}}}" }, + { + "description": "subtype 0x08", + "canonical_bson": "1D000000057800100000000873FFD26444B34C6990E8E7D1DFC035D400", + "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"c//SZESzTGmQ6OfR38A11A==\", \"subType\" : \"08\"}}}" + }, { "description": "subtype 0x80", "canonical_bson": "0F0000000578000200000080FFFF00", @@ -69,6 +74,36 @@ "description": "$type query operator (conflicts with legacy $binary form with $type field)", "canonical_bson": "180000000378001000000010247479706500020000000000", "canonical_extjson": "{\"x\" : { \"$type\" : {\"$numberInt\": \"2\"}}}" + }, + { + "description": "subtype 0x09 Vector FLOAT32", + "canonical_bson": "170000000578000A0000000927000000FE420000E04000", + "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"JwAAAP5CAADgQA==\", \"subType\": \"09\"}}}" + }, + { + "description": "subtype 0x09 Vector INT8", + "canonical_bson": "11000000057800040000000903007F0700", + "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"AwB/Bw==\", \"subType\": \"09\"}}}" + }, + { + "description": "subtype 0x09 Vector PACKED_BIT", + "canonical_bson": "11000000057800040000000910007F0700", + "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"EAB/Bw==\", \"subType\": \"09\"}}}" + }, + { + "description": "subtype 0x09 Vector (Zero-length) FLOAT32", + "canonical_bson": "0F0000000578000200000009270000", + "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"JwA=\", \"subType\": \"09\"}}}" + }, + { + "description": "subtype 0x09 Vector (Zero-length) INT8", + "canonical_bson": "0F0000000578000200000009030000", + "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"AwA=\", \"subType\": \"09\"}}}" + }, + { + "description": "subtype 0x09 Vector (Zero-length) PACKED_BIT", + "canonical_bson": "0F0000000578000200000009100000", + "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"EAA=\", \"subType\": \"09\"}}}" } ], "decodeErrors": [ @@ -115,4 +150,4 @@ "string": "{\"x\" : { \"$uuid\" : \"----d264-44b3-4--9-90e8-e7d1dfc0----\"}}" } ] -} +} \ No newline at end of file diff --git a/bson/src/test/unit/org/bson/BsonBinarySpecification.groovy b/bson/src/test/unit/org/bson/BsonBinarySpecification.groovy index e51094e964f..503440daa04 100644 --- a/bson/src/test/unit/org/bson/BsonBinarySpecification.groovy +++ b/bson/src/test/unit/org/bson/BsonBinarySpecification.groovy @@ -48,9 +48,14 @@ class BsonBinarySpecification extends Specification { data == bsonBinary.getData() where: - subType << [BsonBinarySubType.BINARY, BsonBinarySubType.FUNCTION, BsonBinarySubType.MD5, - BsonBinarySubType.OLD_BINARY, BsonBinarySubType.USER_DEFINED, BsonBinarySubType.UUID_LEGACY, - BsonBinarySubType.UUID_STANDARD] + subType << [BsonBinarySubType.BINARY, + BsonBinarySubType.FUNCTION, + BsonBinarySubType.MD5, + BsonBinarySubType.OLD_BINARY, + BsonBinarySubType.USER_DEFINED, + BsonBinarySubType.UUID_LEGACY, + BsonBinarySubType.UUID_STANDARD, + BsonBinarySubType.VECTOR] } @Unroll diff --git a/bson/src/test/unit/org/bson/BsonBinarySubTypeSpecification.groovy b/bson/src/test/unit/org/bson/BsonBinarySubTypeSpecification.groovy index f26d1ad00d9..448d63f23fd 100644 --- a/bson/src/test/unit/org/bson/BsonBinarySubTypeSpecification.groovy +++ b/bson/src/test/unit/org/bson/BsonBinarySubTypeSpecification.groovy @@ -33,5 +33,7 @@ class BsonBinarySubTypeSpecification extends Specification { 5 | false 6 | false 7 | false + 8 | false + 9 | false } } diff --git a/bson/src/test/unit/org/bson/BsonBinaryTest.java b/bson/src/test/unit/org/bson/BsonBinaryTest.java new file mode 100644 index 00000000000..029c611c594 --- /dev/null +++ b/bson/src/test/unit/org/bson/BsonBinaryTest.java @@ -0,0 +1,266 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class BsonBinaryTest { + + private static final byte FLOAT32_DTYPE = Vector.DataType.FLOAT32.getValue(); + private static final byte INT8_DTYPE = Vector.DataType.INT8.getValue(); + private static final byte PACKED_BIT_DTYPE = Vector.DataType.PACKED_BIT.getValue(); + public static final int ZERO_PADDING = 0; + + @Test + void shouldThrowExceptionWhenCreatingBsonBinaryWithNullVector() { + // given + Vector vector = null; + + // when & then + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> new BsonBinary(vector)); + assertEquals("Vector must not be null", exception.getMessage()); + } + + @ParameterizedTest + @EnumSource(value = BsonBinarySubType.class, mode = EnumSource.Mode.EXCLUDE, names = {"VECTOR"}) + void shouldThrowExceptionWhenBsonBinarySubTypeIsNotVector(final BsonBinarySubType bsonBinarySubType) { + // given + byte[] data = new byte[]{1, 2, 3, 4}; + BsonBinary bsonBinary = new BsonBinary(bsonBinarySubType.getValue(), data); + + // when & then + BsonInvalidOperationException exception = assertThrows(BsonInvalidOperationException.class, bsonBinary::asVector); + assertEquals("type must be a Vector subtype.", exception.getMessage()); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("provideFloatVectors") + void shouldEncodeFloatVector(final Vector actualFloat32Vector, final byte[] expectedBsonEncodedVector) { + // when + BsonBinary actualBsonBinary = new BsonBinary(actualFloat32Vector); + byte[] actualBsonEncodedVector = actualBsonBinary.getData(); + + // then + assertEquals(BsonBinarySubType.VECTOR.getValue(), actualBsonBinary.getType(), "The subtype must be VECTOR"); + assertArrayEquals(expectedBsonEncodedVector, actualBsonEncodedVector); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("provideFloatVectors") + void shouldDecodeFloatVector(final Float32Vector expectedFloatVector, final byte[] bsonEncodedVector) { + // when + Float32Vector decodedVector = (Float32Vector) new BsonBinary(BsonBinarySubType.VECTOR, bsonEncodedVector).asVector(); + + // then + assertEquals(expectedFloatVector, decodedVector); + } + + private static Stream provideFloatVectors() { + return Stream.of( + arguments( + Vector.floatVector(new float[]{1.1f, 2.2f, 3.3f, -1.0f, Float.MAX_VALUE, Float.MIN_VALUE, Float.POSITIVE_INFINITY, + Float.NEGATIVE_INFINITY}), + new byte[]{FLOAT32_DTYPE, ZERO_PADDING, + (byte) 205, (byte) 204, (byte) 140, (byte) 63, // 1.1f in little-endian + (byte) 205, (byte) 204, (byte) 12, (byte) 64, // 2.2f in little-endian + (byte) 51, (byte) 51, (byte) 83, (byte) 64, // 3.3f in little-endian + (byte) 0, (byte) 0, (byte) 128, (byte) 191, // -1.0f in little-endian + (byte) 255, (byte) 255, (byte) 127, (byte) 127, // Float.MAX_VALUE in little-endian + (byte) 1, (byte) 0, (byte) 0, (byte) 0, // Float.MIN_VALUE in little-endian + (byte) 0, (byte) 0, (byte) 128, (byte) 127, // Float.POSITIVE_INFINITY in little-endian + (byte) 0, (byte) 0, (byte) 128, (byte) 255 // Float.NEGATIVE_INFINITY in little-endian + } + ), + arguments( + Vector.floatVector(new float[]{0.0f}), + new byte[]{FLOAT32_DTYPE, ZERO_PADDING, + (byte) 0, (byte) 0, (byte) 0, (byte) 0 // 0.0f in little-endian + } + ), + arguments( + Vector.floatVector(new float[]{}), + new byte[]{FLOAT32_DTYPE, ZERO_PADDING} + ) + ); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("provideInt8Vectors") + void shouldEncodeInt8Vector(final Vector actualInt8Vector, final byte[] expectedBsonEncodedVector) { + // when + BsonBinary actualBsonBinary = new BsonBinary(actualInt8Vector); + byte[] actualBsonEncodedVector = actualBsonBinary.getData(); + + // then + assertEquals(BsonBinarySubType.VECTOR.getValue(), actualBsonBinary.getType(), "The subtype must be VECTOR"); + assertArrayEquals(expectedBsonEncodedVector, actualBsonEncodedVector); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("provideInt8Vectors") + void shouldDecodeInt8Vector(final Int8Vector expectedInt8Vector, final byte[] bsonEncodedVector) { + // when + Int8Vector decodedVector = (Int8Vector) new BsonBinary(BsonBinarySubType.VECTOR, bsonEncodedVector).asVector(); + + // then + assertEquals(expectedInt8Vector, decodedVector); + } + + private static Stream provideInt8Vectors() { + return Stream.of( + arguments( + Vector.int8Vector(new byte[]{Byte.MAX_VALUE, 1, 2, 3, 4, Byte.MIN_VALUE}), + new byte[]{INT8_DTYPE, ZERO_PADDING, Byte.MAX_VALUE, 1, 2, 3, 4, Byte.MIN_VALUE + }), + arguments(Vector.int8Vector(new byte[]{}), + new byte[]{INT8_DTYPE, ZERO_PADDING} + ) + ); + } + + @ParameterizedTest + @MethodSource("providePackedBitVectors") + void shouldEncodePackedBitVector(final Vector actualPackedBitVector, final byte[] expectedBsonEncodedVector) { + // when + BsonBinary actualBsonBinary = new BsonBinary(actualPackedBitVector); + byte[] actualBsonEncodedVector = actualBsonBinary.getData(); + + // then + assertEquals(BsonBinarySubType.VECTOR.getValue(), actualBsonBinary.getType(), "The subtype must be VECTOR"); + assertArrayEquals(expectedBsonEncodedVector, actualBsonEncodedVector); + } + + @ParameterizedTest + @MethodSource("providePackedBitVectors") + void shouldDecodePackedBitVector(final PackedBitVector expectedPackedBitVector, final byte[] bsonEncodedVector) { + // when + PackedBitVector decodedVector = (PackedBitVector) new BsonBinary(BsonBinarySubType.VECTOR, bsonEncodedVector).asVector(); + + // then + assertEquals(expectedPackedBitVector, decodedVector); + } + + private static Stream providePackedBitVectors() { + return Stream.of( + arguments( + Vector.packedBitVector(new byte[]{(byte) 0, (byte) 255, (byte) 10}, (byte) 2), + new byte[]{PACKED_BIT_DTYPE, 2, (byte) 0, (byte) 255, (byte) 10} + ), + arguments( + Vector.packedBitVector(new byte[0], (byte) 0), + new byte[]{PACKED_BIT_DTYPE, 0} + )); + } + + @Test + void shouldThrowExceptionForInvalidFloatArrayLengthWhenDecode() { + // given + byte[] invalidData = {FLOAT32_DTYPE, 0, 10, 20, 30}; + + // when & Then + BsonInvalidOperationException thrown = assertThrows(BsonInvalidOperationException.class, () -> { + new BsonBinary(BsonBinarySubType.VECTOR, invalidData).asVector(); + }); + assertEquals("Byte array length must be a multiple of 4 for FLOAT32 data type, but found: " + invalidData.length, + thrown.getMessage()); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1}) + void shouldThrowExceptionWhenEncodedVectorLengthIsLessThenMetadataLength(final int encodedVectorLength) { + // given + byte[] invalidData = new byte[encodedVectorLength]; + + // when & Then + BsonInvalidOperationException thrown = assertThrows(BsonInvalidOperationException.class, () -> { + new BsonBinary(BsonBinarySubType.VECTOR, invalidData).asVector(); + }); + assertEquals("Vector encoded array length must be at least 2, but found: " + encodedVectorLength, + thrown.getMessage()); + } + + @ParameterizedTest + @ValueSource(bytes = {-1, 1}) + void shouldThrowExceptionForInvalidFloatArrayPaddingWhenDecode(final byte invalidPadding) { + // given + byte[] invalidData = {FLOAT32_DTYPE, invalidPadding, 10, 20, 30, 20}; + + // when & Then + BsonInvalidOperationException thrown = assertThrows(BsonInvalidOperationException.class, () -> { + new BsonBinary(BsonBinarySubType.VECTOR, invalidData).asVector(); + }); + assertEquals("Padding must be 0 for FLOAT32 data type, but found: " + invalidPadding, thrown.getMessage()); + } + + @ParameterizedTest + @ValueSource(bytes = {-1, 1}) + void shouldThrowExceptionForInvalidInt8ArrayPaddingWhenDecode(final byte invalidPadding) { + // given + byte[] invalidData = {INT8_DTYPE, invalidPadding, 10, 20, 30, 20}; + + // when & Then + BsonInvalidOperationException thrown = assertThrows(BsonInvalidOperationException.class, () -> { + new BsonBinary(BsonBinarySubType.VECTOR, invalidData).asVector(); + }); + assertEquals("Padding must be 0 for INT8 data type, but found: " + invalidPadding, thrown.getMessage()); + } + + @ParameterizedTest + @ValueSource(bytes = {-1, 8}) + void shouldThrowExceptionForInvalidPackedBitArrayPaddingWhenDecode(final byte invalidPadding) { + // given + byte[] invalidData = {PACKED_BIT_DTYPE, invalidPadding, 10, 20, 30, 20}; + + // when & then + BsonInvalidOperationException thrown = assertThrows(BsonInvalidOperationException.class, () -> { + new BsonBinary(BsonBinarySubType.VECTOR, invalidData).asVector(); + }); + assertEquals("Padding must be between 0 and 7 bits, but found: " + invalidPadding, thrown.getMessage()); + } + + @ParameterizedTest + @ValueSource(bytes = {-1, 1, 2, 3, 4, 5, 6, 7, 8}) + void shouldThrowExceptionForInvalidPackedBitArrayPaddingWhenDecodeEmptyVector(final byte invalidPadding) { + // given + byte[] invalidData = {PACKED_BIT_DTYPE, invalidPadding}; + + // when & Then + BsonInvalidOperationException thrown = assertThrows(BsonInvalidOperationException.class, () -> { + new BsonBinary(BsonBinarySubType.VECTOR, invalidData).asVector(); + }); + assertEquals("Padding must be 0 if vector is empty, but found: " + invalidPadding, thrown.getMessage()); + } + + @Test + void shouldThrowWhenUnknownVectorDType() { + // when + BsonBinary bsonBinary = new BsonBinary(BsonBinarySubType.VECTOR, new byte[]{(byte) 0}); + assertThrows(BsonInvalidOperationException.class, bsonBinary::asVector); + } +} diff --git a/bson/src/test/unit/org/bson/BsonBinaryWriterTest.java b/bson/src/test/unit/org/bson/BsonBinaryWriterTest.java index 15e27065ba2..c9e22fcce7a 100644 --- a/bson/src/test/unit/org/bson/BsonBinaryWriterTest.java +++ b/bson/src/test/unit/org/bson/BsonBinaryWriterTest.java @@ -40,6 +40,9 @@ public class BsonBinaryWriterTest { + private static final byte FLOAT32_DTYPE = Vector.DataType.FLOAT32.getValue(); + private static final int ZERO_PADDING = 0; + private BsonBinaryWriter writer; private BasicOutputBuffer buffer; @@ -299,12 +302,38 @@ public void testWriteBinary() { writer.writeBinaryData("b1", new BsonBinary(new byte[]{0, 0, 0, 0, 0, 0, 0, 0})); writer.writeBinaryData("b2", new BsonBinary(BsonBinarySubType.OLD_BINARY, new byte[]{1, 1, 1, 1, 1})); writer.writeBinaryData("b3", new BsonBinary(BsonBinarySubType.FUNCTION, new byte[]{})); + writer.writeBinaryData("b4", new BsonBinary(BsonBinarySubType.VECTOR, new byte[]{FLOAT32_DTYPE, ZERO_PADDING, + (byte) 205, (byte) 204, (byte) 140, (byte) 63})); writer.writeEndDocument(); + byte[] expectedValues = new byte[]{ + 64, // total document length + 0, 0, 0, + + //Binary + (byte) BsonType.BINARY.getValue(), + 98, 49, 0, // name "b1" + 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + + // Old binary + (byte) BsonType.BINARY.getValue(), + 98, 50, 0, // name "b2" + 9, 0, 0, 0, 2, 5, 0, 0, 0, 1, 1, 1, 1, 1, + + // Function binary + (byte) BsonType.BINARY.getValue(), + 98, 51, 0, // name "b3" + 0, 0, 0, 0, 1, + + //Vector binary + (byte) BsonType.BINARY.getValue(), + 98, 52, 0, // name "b4" + 6, 0, 0, 0, // total length, int32 (little endian) + BsonBinarySubType.VECTOR.getValue(), FLOAT32_DTYPE, ZERO_PADDING, (byte) 205, (byte) 204, (byte) 140, 63, + + 0 //end of document + }; - byte[] expectedValues = {49, 0, 0, 0, 5, 98, 49, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 98, 50, 0, - 9, 0, - 0, 0, 2, 5, 0, 0, 0, 1, 1, 1, 1, 1, 5, 98, 51, 0, 0, 0, 0, 0, 1, 0}; assertArrayEquals(expectedValues, buffer.toByteArray()); } diff --git a/bson/src/test/unit/org/bson/BsonHelper.java b/bson/src/test/unit/org/bson/BsonHelper.java index 985e398b1ca..59fdba474a2 100644 --- a/bson/src/test/unit/org/bson/BsonHelper.java +++ b/bson/src/test/unit/org/bson/BsonHelper.java @@ -17,10 +17,12 @@ package org.bson; import org.bson.codecs.BsonDocumentCodec; +import org.bson.codecs.DecoderContext; import org.bson.codecs.EncoderContext; import org.bson.io.BasicOutputBuffer; import org.bson.types.Decimal128; import org.bson.types.ObjectId; +import util.Hex; import java.nio.ByteBuffer; import java.util.Date; @@ -109,4 +111,23 @@ public static ByteBuffer toBson(final BsonDocument document) { private BsonHelper() { } + + public static BsonDocument decodeToDocument(final String subjectHex, final String description) { + ByteBuffer byteBuffer = ByteBuffer.wrap(Hex.decode(subjectHex)); + BsonDocument actualDecodedDocument = new BsonDocumentCodec().decode(new BsonBinaryReader(byteBuffer), + DecoderContext.builder().build()); + + if (byteBuffer.hasRemaining()) { + throw new BsonSerializationException(format("Should have consumed all bytes, but " + byteBuffer.remaining() + + " still remain in the buffer for document with description ", + description)); + } + return actualDecodedDocument; + } + + public static String encodeToHex(final BsonDocument decodedDocument) { + BasicOutputBuffer outputBuffer = new BasicOutputBuffer(); + new BsonDocumentCodec().encode(new BsonBinaryWriter(outputBuffer), decodedDocument, EncoderContext.builder().build()); + return Hex.encode(outputBuffer.toByteArray()); + } } diff --git a/bson/src/test/unit/org/bson/GenericBsonTest.java b/bson/src/test/unit/org/bson/GenericBsonTest.java index 2f50bcd7f61..6ba2c6ae382 100644 --- a/bson/src/test/unit/org/bson/GenericBsonTest.java +++ b/bson/src/test/unit/org/bson/GenericBsonTest.java @@ -16,10 +16,6 @@ package org.bson; -import org.bson.codecs.BsonDocumentCodec; -import org.bson.codecs.DecoderContext; -import org.bson.codecs.EncoderContext; -import org.bson.io.BasicOutputBuffer; import org.bson.json.JsonMode; import org.bson.json.JsonParseException; import org.bson.json.JsonWriterSettings; @@ -27,7 +23,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import util.Hex; import util.JsonPoweredTestHelper; import java.io.File; @@ -35,7 +30,6 @@ import java.io.StringReader; import java.io.StringWriter; import java.net.URISyntaxException; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -44,6 +38,8 @@ import static java.lang.String.format; import static org.bson.BsonDocument.parse; +import static org.bson.BsonHelper.decodeToDocument; +import static org.bson.BsonHelper.encodeToHex; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -207,25 +203,6 @@ private boolean shouldEscapeCharacter(final char escapedChar) { } } - private BsonDocument decodeToDocument(final String subjectHex, final String description) { - ByteBuffer byteBuffer = ByteBuffer.wrap(Hex.decode(subjectHex)); - BsonDocument actualDecodedDocument = new BsonDocumentCodec().decode(new BsonBinaryReader(byteBuffer), - DecoderContext.builder().build()); - - if (byteBuffer.hasRemaining()) { - throw new BsonSerializationException(format("Should have consumed all bytes, but " + byteBuffer.remaining() - + " still remain in the buffer for document with description ", - description)); - } - return actualDecodedDocument; - } - - private String encodeToHex(final BsonDocument decodedDocument) { - BasicOutputBuffer outputBuffer = new BasicOutputBuffer(); - new BsonDocumentCodec().encode(new BsonBinaryWriter(outputBuffer), decodedDocument, EncoderContext.builder().build()); - return Hex.encode(outputBuffer.toByteArray()); - } - private void runDecodeError(final BsonDocument testCase) { try { String description = testCase.getString("description").getValue(); diff --git a/bson/src/test/unit/org/bson/VectorTest.java b/bson/src/test/unit/org/bson/VectorTest.java new file mode 100644 index 00000000000..36cc7156db6 --- /dev/null +++ b/bson/src/test/unit/org/bson/VectorTest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class VectorTest { + + @Test + void shouldCreateInt8Vector() { + // given + byte[] data = {1, 2, 3, 4, 5}; + + // when + Int8Vector vector = Vector.int8Vector(data); + + // then + assertNotNull(vector); + assertEquals(Vector.DataType.INT8, vector.getDataType()); + assertArrayEquals(data, vector.getData()); + } + + @Test + void shouldThrowExceptionWhenCreatingInt8VectorWithNullData() { + // given + byte[] data = null; + + // when & Then + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> Vector.int8Vector(data)); + assertEquals("data can not be null", exception.getMessage()); + } + + @Test + void shouldCreateFloat32Vector() { + // given + float[] data = {1.0f, 2.0f, 3.0f}; + + // when + Float32Vector vector = Vector.floatVector(data); + + // then + assertNotNull(vector); + assertEquals(Vector.DataType.FLOAT32, vector.getDataType()); + assertArrayEquals(data, vector.getData()); + } + + @Test + void shouldThrowExceptionWhenCreatingFloat32VectorWithNullData() { + // given + float[] data = null; + + // when & Then + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> Vector.floatVector(data)); + assertEquals("data can not be null", exception.getMessage()); + } + + + @ParameterizedTest(name = "{index}: validPadding={0}") + @ValueSource(bytes = {0, 1, 2, 3, 4, 5, 6, 7}) + void shouldCreatePackedBitVector(final byte validPadding) { + // given + byte[] data = {(byte) 0b10101010, (byte) 0b01010101}; + + // when + PackedBitVector vector = Vector.packedBitVector(data, validPadding); + + // then + assertNotNull(vector); + assertEquals(Vector.DataType.PACKED_BIT, vector.getDataType()); + assertArrayEquals(data, vector.getData()); + assertEquals(validPadding, vector.getPadding()); + } + + @ParameterizedTest(name = "{index}: invalidPadding={0}") + @ValueSource(bytes = {-1, 8}) + void shouldThrowExceptionWhenPackedBitVectorHasInvalidPadding(final byte invalidPadding) { + // given + byte[] data = {(byte) 0b10101010}; + + // when & Then + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> + Vector.packedBitVector(data, invalidPadding)); + assertEquals("state should be: Padding must be between 0 and 7 bits. Provided padding: " + invalidPadding, exception.getMessage()); + } + + @Test + void shouldThrowExceptionWhenPackedBitVectorIsCreatedWithNullData() { + // given + byte[] data = null; + byte padding = 0; + + // when & Then + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> + Vector.packedBitVector(data, padding)); + assertEquals("data can not be null", exception.getMessage()); + } + + @Test + void shouldCreatePackedBitVectorWithZeroPaddingAndEmptyData() { + // given + byte[] data = new byte[0]; + byte padding = 0; + + // when + PackedBitVector vector = Vector.packedBitVector(data, padding); + + // then + assertNotNull(vector); + assertEquals(Vector.DataType.PACKED_BIT, vector.getDataType()); + assertArrayEquals(data, vector.getData()); + assertEquals(padding, vector.getPadding()); + } + + @Test + void shouldThrowExceptionWhenPackedBitVectorWithNonZeroPaddingAndEmptyData() { + // given + byte[] data = new byte[0]; + byte padding = 1; + + // when & Then + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> + Vector.packedBitVector(data, padding)); + assertEquals("state should be: Padding must be 0 if vector is empty. Provided padding: " + padding, exception.getMessage()); + } + + @Test + void shouldThrowExceptionWhenRetrievingInt8DataFromNonInt8Vector() { + // given + float[] data = {1.0f, 2.0f}; + Vector vector = Vector.floatVector(data); + + // when & Then + IllegalStateException exception = assertThrows(IllegalStateException.class, vector::asInt8Vector); + assertEquals("Expected vector data type INT8, but found FLOAT32", exception.getMessage()); + } + + @Test + void shouldThrowExceptionWhenRetrievingFloat32DataFromNonFloat32Vector() { + // given + byte[] data = {1, 2, 3}; + Vector vector = Vector.int8Vector(data); + + // when & Then + IllegalStateException exception = assertThrows(IllegalStateException.class, vector::asFloat32Vector); + assertEquals("Expected vector data type FLOAT32, but found INT8", exception.getMessage()); + } + + @Test + void shouldThrowExceptionWhenRetrievingPackedBitDataFromNonPackedBitVector() { + // given + float[] data = {1.0f, 2.0f}; + Vector vector = Vector.floatVector(data); + + // when & Then + IllegalStateException exception = assertThrows(IllegalStateException.class, vector::asPackedBitVector); + assertEquals("Expected vector data type PACKED_BIT, but found FLOAT32", exception.getMessage()); + } +} diff --git a/bson/src/test/unit/org/bson/codecs/DocumentCodecTest.java b/bson/src/test/unit/org/bson/codecs/DocumentCodecTest.java index 79c65573556..67c6b561aa5 100644 --- a/bson/src/test/unit/org/bson/codecs/DocumentCodecTest.java +++ b/bson/src/test/unit/org/bson/codecs/DocumentCodecTest.java @@ -23,6 +23,7 @@ import org.bson.BsonObjectId; import org.bson.ByteBufNIO; import org.bson.Document; +import org.bson.Vector; import org.bson.io.BasicOutputBuffer; import org.bson.io.BsonInput; import org.bson.io.ByteBufferBsonInput; @@ -80,6 +81,9 @@ public void testPrimitiveBSONTypeCodecs() throws IOException { doc.put("code", new Code("var i = 0")); doc.put("minkey", new MinKey()); doc.put("maxkey", new MaxKey()); + doc.put("vectorFloat", Vector.floatVector(new float[]{1.1f, 2.2f, 3.3f})); + doc.put("vectorInt8", Vector.int8Vector(new byte[]{10, 20, 30, 40})); + doc.put("vectorPackedBit", Vector.packedBitVector(new byte[]{(byte) 0b10101010, (byte) 0b01010101}, (byte) 3)); // doc.put("pattern", Pattern.compile("^hello")); // TODO: Pattern doesn't override equals method! doc.put("null", null); diff --git a/bson/src/test/unit/org/bson/codecs/ValueCodecProviderSpecification.groovy b/bson/src/test/unit/org/bson/codecs/ValueCodecProviderSpecification.groovy index c20299715e0..23c46fb7b0b 100644 --- a/bson/src/test/unit/org/bson/codecs/ValueCodecProviderSpecification.groovy +++ b/bson/src/test/unit/org/bson/codecs/ValueCodecProviderSpecification.groovy @@ -17,6 +17,10 @@ package org.bson.codecs import org.bson.Document +import org.bson.Float32Vector +import org.bson.Int8Vector +import org.bson.PackedBitVector +import org.bson.Vector import org.bson.codecs.configuration.CodecRegistries import org.bson.types.Binary import org.bson.types.Code @@ -32,6 +36,8 @@ import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong import java.util.regex.Pattern +//Codenarc +@SuppressWarnings("VectorIsObsolete") class ValueCodecProviderSpecification extends Specification { private final provider = new ValueCodecProvider() private final registry = CodecRegistries.fromProviders(provider) @@ -56,6 +62,10 @@ class ValueCodecProviderSpecification extends Specification { provider.get(Short, registry) instanceof ShortCodec provider.get(byte[], registry) instanceof ByteArrayCodec provider.get(Float, registry) instanceof FloatCodec + provider.get(Vector, registry) instanceof VectorCodec + provider.get(Float32Vector, registry) instanceof Float32VectorCodec + provider.get(Int8Vector, registry) instanceof Int8VectorCodec + provider.get(PackedBitVector, registry) instanceof PackedBitVectorCodec provider.get(Binary, registry) instanceof BinaryCodec provider.get(MinKey, registry) instanceof MinKeyCodec diff --git a/bson/src/test/unit/org/bson/codecs/VectorCodecTest.java b/bson/src/test/unit/org/bson/codecs/VectorCodecTest.java new file mode 100644 index 00000000000..bf33af90cae --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/VectorCodecTest.java @@ -0,0 +1,152 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson.codecs; + +import org.bson.BsonBinary; +import org.bson.BsonBinaryReader; +import org.bson.BsonBinarySubType; +import org.bson.BsonBinaryWriter; +import org.bson.BsonDocument; +import org.bson.BsonInvalidOperationException; +import org.bson.BsonType; +import org.bson.BsonWriter; +import org.bson.ByteBufNIO; +import org.bson.Float32Vector; +import org.bson.Int8Vector; +import org.bson.PackedBitVector; +import org.bson.Vector; +import org.bson.io.BasicOutputBuffer; +import org.bson.io.ByteBufferBsonInput; +import org.bson.io.OutputBuffer; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.stream.Stream; + +import static org.bson.BsonHelper.toBson; +import static org.bson.assertions.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class VectorCodecTest extends CodecTestCase { + + private static Stream provideVectorsAndCodecs() { + return Stream.of( + arguments(Vector.floatVector(new float[]{1.1f, 2.2f, 3.3f}), new Float32VectorCodec(), Float32Vector.class), + arguments(Vector.int8Vector(new byte[]{10, 20, 30, 40}), new Int8VectorCodec(), Int8Vector.class), + arguments(Vector.packedBitVector(new byte[]{(byte) 0b10101010, (byte) 0b01010101}, (byte) 3), new PackedBitVectorCodec(), PackedBitVector.class), + arguments(Vector.packedBitVector(new byte[]{(byte) 0b10101010, (byte) 0b01010101}, (byte) 3), new VectorCodec(), Vector.class), + arguments(Vector.int8Vector(new byte[]{10, 20, 30, 40}), new VectorCodec(), Vector.class), + arguments(Vector.packedBitVector(new byte[]{(byte) 0b10101010, (byte) 0b01010101}, (byte) 3), new VectorCodec(), Vector.class) + ); + } + + @ParameterizedTest + @MethodSource("provideVectorsAndCodecs") + void shouldEncodeVector(final Vector vectorToEncode, final Codec vectorCodec) throws IOException { + // given + BsonBinary bsonBinary = new BsonBinary(vectorToEncode); + byte[] encodedVector = bsonBinary.getData(); + ByteArrayOutputStream expectedStream = new ByteArrayOutputStream(); + // Total length of a Document (int 32). It is 0, because we do not expect + // codec to write the end of the document (that is when we back-patch the length of the document). + expectedStream.write(new byte[]{0, 0, 0, 0}); + // Bson type + expectedStream.write((byte) BsonType.BINARY.getValue()); + // Field name "b4" + expectedStream.write(new byte[]{98, 52, 0}); + // Total length of binary data (little-endian format) + expectedStream.write(new byte[]{(byte) encodedVector.length, 0, 0, 0}); + // Vector binary subtype + expectedStream.write(BsonBinarySubType.VECTOR.getValue()); + // Actual BSON binary data + expectedStream.write(encodedVector); + + OutputBuffer buffer = new BasicOutputBuffer(); + BsonWriter writer = new BsonBinaryWriter(buffer); + writer.writeStartDocument(); + writer.writeName("b4"); + + // when + vectorCodec.encode(writer, vectorToEncode, EncoderContext.builder().build()); + + // then + assertArrayEquals(expectedStream.toByteArray(), buffer.toByteArray()); + } + + @ParameterizedTest + @MethodSource("provideVectorsAndCodecs") + void shouldDecodeVector(final Vector vectorToDecode, final Codec vectorCodec) { + // given + OutputBuffer buffer = new BasicOutputBuffer(); + BsonWriter writer = new BsonBinaryWriter(buffer); + writer.writeStartDocument(); + writer.writeName("vector"); + writer.writeBinaryData(new BsonBinary(vectorToDecode)); + writer.writeEndDocument(); + + BsonBinaryReader reader = new BsonBinaryReader(new ByteBufferBsonInput(new ByteBufNIO(ByteBuffer.wrap(buffer.toByteArray())))); + reader.readStartDocument(); + + // when + Vector decodedVector = vectorCodec.decode(reader, DecoderContext.builder().build()); + + // then + assertDoesNotThrow(reader::readEndDocument); + assertNotNull(decodedVector); + assertEquals(vectorToDecode, decodedVector); + } + + + @ParameterizedTest + @EnumSource(value = BsonBinarySubType.class, mode = EnumSource.Mode.EXCLUDE, names = {"VECTOR"}) + void shouldThrowExceptionForInvalidSubType(final BsonBinarySubType subType) { + // given + BsonDocument document = new BsonDocument("name", new BsonBinary(subType.getValue(), new byte[]{})); + BsonBinaryReader reader = new BsonBinaryReader(toBson(document)); + reader.readStartDocument(); + + // when & then + Stream.of(new Float32VectorCodec(), new Int8VectorCodec(), new PackedBitVectorCodec()) + .forEach(codec -> { + BsonInvalidOperationException exception = assertThrows(BsonInvalidOperationException.class, () -> + codec.decode(reader, DecoderContext.builder().build())); + assertEquals("Expected vector binary subtype 9 but found: " + subType.getValue(), exception.getMessage()); + }); + } + + + @ParameterizedTest + @MethodSource("provideVectorsAndCodecs") + void shouldReturnCorrectEncoderClass(final Vector vector, + final Codec codec, + final Class expectedEncoderClass) { + // when + Class encoderClass = codec.getEncoderClass(); + + // then + assertEquals(expectedEncoderClass, encoderClass); + } +} diff --git a/bson/src/test/unit/org/bson/vector/VectorGenericBsonTest.java b/bson/src/test/unit/org/bson/vector/VectorGenericBsonTest.java new file mode 100644 index 00000000000..64e84f6afc8 --- /dev/null +++ b/bson/src/test/unit/org/bson/vector/VectorGenericBsonTest.java @@ -0,0 +1,276 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson.vector; + +import org.bson.BsonArray; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonString; +import org.bson.BsonValue; +import org.bson.Float32Vector; +import org.bson.PackedBitVector; +import org.bson.Vector; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import util.JsonPoweredTestHelper; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static java.lang.String.format; +import static org.bson.BsonHelper.decodeToDocument; +import static org.bson.BsonHelper.encodeToHex; +import static org.bson.internal.vector.VectorHelper.determineVectorDType; +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +/** + * See + * JSON-based tests that included in test resources. + */ +class VectorGenericBsonTest { + + private static final List TEST_NAMES_TO_IGNORE = Arrays.asList( + //NO API to set padding for floats available. + "FLOAT32 with padding", + //NO API to set padding for floats available. + "INT8 with padding", + //It is impossible to provide float inputs for INT8 in the API. + "INT8 with float inputs", + //It is impossible to provide float inputs for INT8. + "Underflow Vector PACKED_BIT", + //It is impossible to provide float inputs for PACKED_BIT in the API. + "Vector with float values PACKED_BIT", + //It is impossible to provide float inputs for INT8. + "Overflow Vector PACKED_BIT", + //It is impossible to overflow byte with values higher than 127 in the API. + "Overflow Vector INT8", + //It is impossible to underflow byte with values lower than -128 in the API. + "Underflow Vector INT8"); + + + @ParameterizedTest(name = "{0}") + @MethodSource("provideTestCases") + void shouldPassAllOutcomes(@SuppressWarnings("unused") final String description, + final BsonDocument testDefinition, final BsonDocument testCase) { + assumeFalse(TEST_NAMES_TO_IGNORE.contains(testCase.get("description").asString().getValue())); + + String testKey = testDefinition.getString("test_key").getValue(); + boolean isValidVector = testCase.getBoolean("valid").getValue(); + if (isValidVector) { + runValidTestCase(testKey, testCase); + } else { + runInvalidTestCase(testCase); + } + } + + private static void runInvalidTestCase(final BsonDocument testCase) { + BsonArray arrayVector = testCase.getArray("vector"); + byte expectedPadding = (byte) testCase.getInt32("padding").getValue(); + byte dtypeByte = Byte.decode(testCase.getString("dtype_hex").getValue()); + Vector.DataType expectedDType = determineVectorDType(dtypeByte); + + switch (expectedDType) { + case INT8: + byte[] expectedVectorData = toByteArray(arrayVector); + assertValidationException(assertThrows(RuntimeException.class, + () -> Vector.int8Vector(expectedVectorData))); + break; + case PACKED_BIT: + byte[] expectedVectorPackedBitData = toByteArray(arrayVector); + assertValidationException(assertThrows(RuntimeException.class, + () -> Vector.packedBitVector(expectedVectorPackedBitData, expectedPadding))); + break; + case FLOAT32: + float[] expectedFloatVector = toFloatArray(arrayVector); + assertValidationException(assertThrows(RuntimeException.class, () -> Vector.floatVector(expectedFloatVector))); + break; + default: + throw new IllegalArgumentException("Unsupported vector data type: " + expectedDType); + } + } + + private static void runValidTestCase(final String testKey, final BsonDocument testCase) { + String description = testCase.getString("description").getValue(); + byte dtypeByte = Byte.decode(testCase.getString("dtype_hex").getValue()); + + byte expectedPadding = (byte) testCase.getInt32("padding").getValue(); + Vector.DataType expectedDType = determineVectorDType(dtypeByte); + String expectedCanonicalBsonHex = testCase.getString("canonical_bson").getValue().toUpperCase(); + + BsonArray arrayVector = testCase.getArray("vector"); + BsonDocument actualDecodedDocument = decodeToDocument(expectedCanonicalBsonHex, description); + Vector actualVector = actualDecodedDocument.getBinary("vector").asVector(); + + switch (expectedDType) { + case INT8: + byte[] expectedVectorData = toByteArray(arrayVector); + byte[] actualVectorData = actualVector.asInt8Vector().getData(); + assertVectorDecoding( + expectedVectorData, + expectedDType, + actualVectorData, + actualVector); + + assertThatVectorCreationResultsInCorrectBinary(Vector.int8Vector(expectedVectorData), + testKey, + actualDecodedDocument, + expectedCanonicalBsonHex, + description); + break; + case PACKED_BIT: + PackedBitVector actualPackedBitVector = actualVector.asPackedBitVector(); + byte[] expectedVectorPackedBitData = toByteArray(arrayVector); + assertVectorDecoding( + expectedVectorPackedBitData, + expectedDType, expectedPadding, + actualPackedBitVector); + + assertThatVectorCreationResultsInCorrectBinary( + Vector.packedBitVector(expectedVectorPackedBitData, expectedPadding), + testKey, + actualDecodedDocument, + expectedCanonicalBsonHex, + description); + break; + case FLOAT32: + Float32Vector actualFloat32Vector = actualVector.asFloat32Vector(); + float[] expectedFloatVector = toFloatArray(arrayVector); + assertVectorDecoding( + expectedFloatVector, + expectedDType, + actualFloat32Vector); + assertThatVectorCreationResultsInCorrectBinary( + Vector.floatVector(expectedFloatVector), + testKey, + actualDecodedDocument, + expectedCanonicalBsonHex, + description); + break; + default: + throw new IllegalArgumentException("Unsupported vector data type: " + expectedDType); + } + } + + private static void assertValidationException(final RuntimeException runtimeException) { + assertTrue(runtimeException instanceof IllegalArgumentException || runtimeException instanceof IllegalStateException); + } + + private static void assertThatVectorCreationResultsInCorrectBinary(final Vector expectedVectorData, + final String testKey, + final BsonDocument actualDecodedDocument, + final String expectedCanonicalBsonHex, + final String description) { + BsonDocument documentToEncode = new BsonDocument(testKey, new BsonBinary(expectedVectorData)); + assertEquals(documentToEncode, actualDecodedDocument); + assertEquals(expectedCanonicalBsonHex, encodeToHex(documentToEncode), + format("Failed to create expected BSON for document with description '%s'", description)); + } + + private static void assertVectorDecoding(final byte[] expectedVectorData, + final Vector.DataType expectedDType, + final byte[] actualVectorData, + final Vector actualVector) { + Assertions.assertArrayEquals(actualVectorData, expectedVectorData, + () -> "Actual: " + Arrays.toString(actualVectorData) + " != Expected:" + Arrays.toString(expectedVectorData)); + assertEquals(expectedDType, actualVector.getDataType()); + } + + private static void assertVectorDecoding(final byte[] expectedVectorData, + final Vector.DataType expectedDType, + final byte expectedPadding, + final PackedBitVector actualVector) { + byte[] actualVectorData = actualVector.getData(); + assertVectorDecoding( + expectedVectorData, + expectedDType, + actualVectorData, + actualVector); + assertEquals(expectedPadding, actualVector.getPadding()); + } + + private static void assertVectorDecoding(final float[] expectedVectorData, + final Vector.DataType expectedDType, + final Float32Vector actualVector) { + float[] actualVectorArray = actualVector.getData(); + Assertions.assertArrayEquals(actualVectorArray, expectedVectorData, + () -> "Actual: " + Arrays.toString(actualVectorArray) + " != Expected:" + Arrays.toString(expectedVectorData)); + assertEquals(expectedDType, actualVector.getDataType()); + } + + private static byte[] toByteArray(final BsonArray arrayVector) { + byte[] bytes = new byte[arrayVector.size()]; + for (int i = 0; i < arrayVector.size(); i++) { + bytes[i] = (byte) arrayVector.get(i).asInt32().getValue(); + } + return bytes; + } + + private static float[] toFloatArray(final BsonArray arrayVector) { + float[] floats = new float[arrayVector.size()]; + for (int i = 0; i < arrayVector.size(); i++) { + BsonValue bsonValue = arrayVector.get(i); + if (bsonValue.isString()) { + floats[i] = parseFloat(bsonValue.asString()); + } else { + floats[i] = (float) arrayVector.get(i).asDouble().getValue(); + } + } + return floats; + } + + private static float parseFloat(final BsonString bsonValue) { + String floatValue = bsonValue.getValue(); + switch (floatValue) { + case "-inf": + return Float.NEGATIVE_INFINITY; + case "inf": + return Float.POSITIVE_INFINITY; + default: + return Float.parseFloat(floatValue); + } + } + + private static Stream provideTestCases() throws URISyntaxException, IOException { + List data = new ArrayList<>(); + for (File file : JsonPoweredTestHelper.getTestFiles("/bson-binary-vector")) { + BsonDocument testDocument = JsonPoweredTestHelper.getTestDocument(file); + for (BsonValue curValue : testDocument.getArray("tests", new BsonArray())) { + BsonDocument testCaseDocument = curValue.asDocument(); + data.add(Arguments.of(createTestCaseDescription(testDocument, testCaseDocument), testDocument, testCaseDocument)); + } + } + return data.stream(); + } + + private static String createTestCaseDescription(final BsonDocument testDocument, + final BsonDocument testCaseDocument) { + boolean isValidTestCase = testCaseDocument.getBoolean("valid").getValue(); + String fileDescription = testDocument.getString("description").getValue(); + String testDescription = testCaseDocument.getString("description").getValue(); + return "[Valid input: " + isValidTestCase + "] " + fileDescription + ": " + testDescription; + } +} diff --git a/build.gradle b/build.gradle index d9ebd912fb8..54679fa5d88 100644 --- a/build.gradle +++ b/build.gradle @@ -75,7 +75,7 @@ configure(coreProjects) { apply plugin: 'idea' group = 'org.mongodb' - version = '5.2.0-SNAPSHOT' + version = '5.3.0-beta0' repositories { mavenLocal() @@ -109,6 +109,7 @@ configure(javaProjects) { configure(scalaProjects) { apply plugin: 'scala' + apply plugin: 'java-library' apply plugin: 'idea' apply plugin: "com.adtran.scala-multiversion-plugin" apply plugin: "com.diffplug.spotless" @@ -116,8 +117,8 @@ configure(scalaProjects) { group = 'org.mongodb.scala' dependencies { - implementation ('org.scala-lang:scala-library:%scala-version%') - implementation ('org.scala-lang:scala-reflect:%scala-version%') + api ('org.scala-lang:scala-library:%scala-version%') + api ('org.scala-lang:scala-reflect:%scala-version%') testImplementation(platform("org.junit:junit-bom:$junitBomVersion")) testImplementation("org.junit.vintage:junit-vintage-engine") @@ -157,7 +158,7 @@ configure(scalaProjects) { "-feature", "-unchecked", "-language:reflectiveCalls", - "-Wconf:cat=deprecation:ws,any:e", + "-Wconf:cat=deprecation:ws", "-Wconf:msg=While parsing annotations in:silent", "-Xlint:strict-unsealed-patmat" ] diff --git a/driver-core/build.gradle b/driver-core/build.gradle index c23a24a9fb8..70061ca2b1e 100644 --- a/driver-core/build.gradle +++ b/driver-core/build.gradle @@ -82,6 +82,7 @@ afterEvaluate { '!sun.misc.*', // Used by DirectBufferDeallocator only for java 8 '!sun.nio.ch.*', // Used by DirectBufferDeallocator only for java 8 '!javax.annotation.*', // Brought in by com.google.code.findbugs:annotations + '!com.oracle.svm.core.annotate.*', // this dependency is provided by the GraalVM runtime 'io.netty.*;resolution:=optional', 'com.amazonaws.*;resolution:=optional', 'software.amazon.awssdk.*;resolution:=optional', diff --git a/driver-core/src/main/com/mongodb/client/model/Aggregates.java b/driver-core/src/main/com/mongodb/client/model/Aggregates.java index 4bb3a03771c..7d6306cdd23 100644 --- a/driver-core/src/main/com/mongodb/client/model/Aggregates.java +++ b/driver-core/src/main/com/mongodb/client/model/Aggregates.java @@ -37,6 +37,7 @@ import org.bson.BsonType; import org.bson.BsonValue; import org.bson.Document; +import org.bson.Vector; import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; @@ -963,28 +964,37 @@ public static Bson vectorSearch( notNull("queryVector", queryVector); notNull("index", index); notNull("options", options); - return new Bson() { - @Override - public BsonDocument toBsonDocument(final Class documentClass, final CodecRegistry codecRegistry) { - Document specificationDoc = new Document("path", path.toValue()) - .append("queryVector", queryVector) - .append("index", index) - .append("limit", limit); - specificationDoc.putAll(options.toBsonDocument(documentClass, codecRegistry)); - return new Document("$vectorSearch", specificationDoc).toBsonDocument(documentClass, codecRegistry); - } + return new VectorSearchBson(path, queryVector, index, limit, options); + } - @Override - public String toString() { - return "Stage{name=$vectorSearch" - + ", path=" + path - + ", queryVector=" + queryVector - + ", index=" + index - + ", limit=" + limit - + ", options=" + options - + '}'; - } - }; + /** + * Creates a {@code $vectorSearch} pipeline stage supported by MongoDB Atlas. + * You may use the {@code $meta: "vectorSearchScore"} expression, e.g., via {@link Projections#metaVectorSearchScore(String)}, + * to extract the relevance score assigned to each found document. + * + * @param queryVector The {@linkplain Vector query vector}. The number of dimensions must match that of the {@code index}. + * @param path The field to be searched. + * @param index The name of the index to use. + * @param limit The limit on the number of documents produced by the pipeline stage. + * @param options Optional {@code $vectorSearch} pipeline stage fields. + * @return The {@code $vectorSearch} pipeline stage. + * @mongodb.atlas.manual atlas-vector-search/vector-search-stage/ $vectorSearch + * @mongodb.atlas.manual atlas-search/scoring/ Scoring + * @mongodb.server.release 6.0 + * @see Vector + * @since 5.3 + */ + public static Bson vectorSearch( + final FieldSearchPath path, + final Vector queryVector, + final String index, + final long limit, + final VectorSearchOptions options) { + notNull("path", path); + notNull("queryVector", queryVector); + notNull("index", index); + notNull("options", options); + return new VectorSearchBson(path, queryVector, index, limit, options); } /** @@ -2145,6 +2155,45 @@ public String toString() { } } + private static class VectorSearchBson implements Bson { + private final FieldSearchPath path; + private final Object queryVector; + private final String index; + private final long limit; + private final VectorSearchOptions options; + + VectorSearchBson(final FieldSearchPath path, final Object queryVector, + final String index, final long limit, + final VectorSearchOptions options) { + this.path = path; + this.queryVector = queryVector; + this.index = index; + this.limit = limit; + this.options = options; + } + + @Override + public BsonDocument toBsonDocument(final Class documentClass, final CodecRegistry codecRegistry) { + Document specificationDoc = new Document("path", path.toValue()) + .append("queryVector", queryVector) + .append("index", index) + .append("limit", limit); + specificationDoc.putAll(options.toBsonDocument(documentClass, codecRegistry)); + return new Document("$vectorSearch", specificationDoc).toBsonDocument(documentClass, codecRegistry); + } + + @Override + public String toString() { + return "Stage{name=$vectorSearch" + + ", path=" + path + + ", queryVector=" + queryVector + + ", index=" + index + + ", limit=" + limit + + ", options=" + options + + '}'; + } + } + private Aggregates() { } } diff --git a/driver-core/src/main/com/mongodb/connection/ClusterSettings.java b/driver-core/src/main/com/mongodb/connection/ClusterSettings.java index 0af168725cd..01e5c140441 100644 --- a/driver-core/src/main/com/mongodb/connection/ClusterSettings.java +++ b/driver-core/src/main/com/mongodb/connection/ClusterSettings.java @@ -36,7 +36,6 @@ import static com.mongodb.assertions.Assertions.isTrueArgument; import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.internal.connection.ServerAddressHelper.createServerAddress; -import static java.lang.String.format; import static java.util.Collections.singletonList; import static java.util.Collections.unmodifiableList; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -607,11 +606,6 @@ private ClusterSettings(final Builder builder) { if (builder.srvHost.contains(":")) { throw new IllegalArgumentException("The srvHost can not contain a host name that specifies a port"); } - - if (builder.srvHost.split("\\.").length < 3) { - throw new IllegalArgumentException(format("An SRV host name '%s' was provided that does not contain at least three parts. " - + "It must contain a hostname, domain name and a top level domain.", builder.srvHost)); - } } if (builder.hosts.size() > 1 && builder.requiredClusterType == ClusterType.STANDALONE) { diff --git a/driver-core/src/main/com/mongodb/internal/dns/DefaultDnsResolver.java b/driver-core/src/main/com/mongodb/internal/dns/DefaultDnsResolver.java index d483e220253..f7b433b85bd 100644 --- a/driver-core/src/main/com/mongodb/internal/dns/DefaultDnsResolver.java +++ b/driver-core/src/main/com/mongodb/internal/dns/DefaultDnsResolver.java @@ -64,13 +64,19 @@ public DefaultDnsResolver(@Nullable final DnsClient dnsClient) { The priority and weight are ignored, and we just concatenate the host (after removing the ending '.') and port with a ':' in between, as expected by ServerAddress. - - It's required that the srvHost has at least three parts (e.g. foo.bar.baz) and that all of the resolved hosts have a parent - domain equal to the domain of the srvHost. */ @Override public List resolveHostFromSrvRecords(final String srvHost, final String srvServiceName) { - String srvHostDomain = srvHost.substring(srvHost.indexOf('.') + 1); + List srvHostParts = asList(srvHost.split("\\.")); + + String srvHostDomain; + boolean srvHasLessThanThreeParts = srvHostParts.size() < 3; + if (srvHasLessThanThreeParts) { + srvHostDomain = srvHost; + } else { + srvHostDomain = srvHost.substring(srvHost.indexOf('.') + 1); + } + List srvHostDomainParts = asList(srvHostDomain.split("\\.")); List hosts = new ArrayList<>(); String resourceName = "_" + srvServiceName + "._tcp." + srvHost; @@ -84,9 +90,15 @@ public List resolveHostFromSrvRecords(final String srvHost, final String String[] split = srvRecord.split(" "); String resolvedHost = split[3].endsWith(".") ? split[3].substring(0, split[3].length() - 1) : split[3]; String resolvedHostDomain = resolvedHost.substring(resolvedHost.indexOf('.') + 1); - if (!sameParentDomain(srvHostDomainParts, resolvedHostDomain)) { + List resolvedHostDomainParts = asList(resolvedHostDomain.split("\\.")); + if (!sameDomain(srvHostDomainParts, resolvedHostDomainParts)) { + throw new MongoConfigurationException( + format("The SRV host name '%s' resolved to a host '%s' that does not share domain name", + srvHost, resolvedHost)); + } + if (srvHasLessThanThreeParts && resolvedHostDomainParts.size() <= srvHostDomainParts.size()) { throw new MongoConfigurationException( - format("The SRV host name '%s' resolved to a host '%s 'that is not in a sub-domain of the SRV host.", + format("The SRV host name '%s' resolved to a host '%s' that does not have at least one more domain level", srvHost, resolvedHost)); } hosts.add(resolvedHost + ":" + split[2]); @@ -98,8 +110,7 @@ public List resolveHostFromSrvRecords(final String srvHost, final String return hosts; } - private static boolean sameParentDomain(final List srvHostDomainParts, final String resolvedHostDomain) { - List resolvedHostDomainParts = asList(resolvedHostDomain.split("\\.")); + private static boolean sameDomain(final List srvHostDomainParts, final List resolvedHostDomainParts) { if (srvHostDomainParts.size() > resolvedHostDomainParts.size()) { return false; } diff --git a/driver-core/src/main/com/mongodb/internal/operation/CreateSearchIndexesOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CreateSearchIndexesOperation.java index 2e52e3fa0ae..a57087e9217 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/CreateSearchIndexesOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/CreateSearchIndexesOperation.java @@ -32,11 +32,11 @@ * *

    This class is not part of the public API and may be removed or changed at any time

    */ -final class CreateSearchIndexesOperation extends AbstractWriteSearchIndexOperation { +public final class CreateSearchIndexesOperation extends AbstractWriteSearchIndexOperation { private static final String COMMAND_NAME = "createSearchIndexes"; private final List indexRequests; - CreateSearchIndexesOperation(final MongoNamespace namespace, final List indexRequests) { + public CreateSearchIndexesOperation(final MongoNamespace namespace, final List indexRequests) { super(namespace); this.indexRequests = assertNotNull(indexRequests); } diff --git a/driver-core/src/main/com/mongodb/internal/operation/ListSearchIndexesOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ListSearchIndexesOperation.java index 0f9a81dbf19..3dfde30511d 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/ListSearchIndexesOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/ListSearchIndexesOperation.java @@ -42,7 +42,7 @@ * *

    This class is not part of the public API and may be removed or changed at any time

    */ -final class ListSearchIndexesOperation +public final class ListSearchIndexesOperation implements AsyncExplainableReadOperation>, ExplainableReadOperation> { private static final String STAGE_LIST_SEARCH_INDEXES = "$listSearchIndexes"; private final MongoNamespace namespace; @@ -59,9 +59,10 @@ final class ListSearchIndexesOperation private final String indexName; private final boolean retryReads; - ListSearchIndexesOperation(final MongoNamespace namespace, final Decoder decoder, @Nullable final String indexName, - @Nullable final Integer batchSize, @Nullable final Collation collation, @Nullable final BsonValue comment, - @Nullable final Boolean allowDiskUse, final boolean retryReads) { + public ListSearchIndexesOperation(final MongoNamespace namespace, final Decoder decoder, @Nullable final String indexName, + @Nullable final Integer batchSize, @Nullable final Collation collation, + @Nullable final BsonValue comment, + @Nullable final Boolean allowDiskUse, final boolean retryReads) { this.namespace = namespace; this.decoder = decoder; this.allowDiskUse = allowDiskUse; diff --git a/driver-core/src/main/com/mongodb/internal/operation/SearchIndexRequest.java b/driver-core/src/main/com/mongodb/internal/operation/SearchIndexRequest.java index 0d37d2c2178..29b9b1ef34d 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/SearchIndexRequest.java +++ b/driver-core/src/main/com/mongodb/internal/operation/SearchIndexRequest.java @@ -31,14 +31,15 @@ * *

    This class is not part of the public API and may be removed or changed at any time

    */ -final class SearchIndexRequest { +public final class SearchIndexRequest { private final BsonDocument definition; @Nullable private final String indexName; @Nullable private final SearchIndexType searchIndexType; - SearchIndexRequest(final BsonDocument definition, @Nullable final String indexName, @Nullable final SearchIndexType searchIndexType) { + public SearchIndexRequest(final BsonDocument definition, @Nullable final String indexName, + @Nullable final SearchIndexType searchIndexType) { assertNotNull(definition); this.definition = definition; this.indexName = indexName; diff --git a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesVectorSearchIntegrationTest.java b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesVectorSearchIntegrationTest.java new file mode 100644 index 00000000000..15def0f5d71 --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesVectorSearchIntegrationTest.java @@ -0,0 +1,353 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client.model.search; + +import com.mongodb.MongoInterruptedException; +import com.mongodb.MongoNamespace; +import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.SearchIndexType; +import com.mongodb.client.test.CollectionHelper; +import com.mongodb.internal.operation.SearchIndexRequest; +import org.bson.BsonDocument; +import org.bson.Document; +import org.bson.Vector; +import org.bson.codecs.DocumentCodec; +import org.bson.conversions.Bson; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import static com.mongodb.ClusterFixture.isAtlasSearchTest; +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.client.model.Filters.and; +import static com.mongodb.client.model.Filters.eq; +import static com.mongodb.client.model.Filters.gt; +import static com.mongodb.client.model.Filters.gte; +import static com.mongodb.client.model.Filters.in; +import static com.mongodb.client.model.Filters.lt; +import static com.mongodb.client.model.Filters.lte; +import static com.mongodb.client.model.Filters.ne; +import static com.mongodb.client.model.Filters.nin; +import static com.mongodb.client.model.Filters.or; +import static com.mongodb.client.model.Projections.fields; +import static com.mongodb.client.model.Projections.metaVectorSearchScore; +import static com.mongodb.client.model.search.SearchPath.fieldPath; +import static com.mongodb.client.model.search.VectorSearchOptions.approximateVectorSearchOptions; +import static com.mongodb.client.model.search.VectorSearchOptions.exactVectorSearchOptions; +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class AggregatesVectorSearchIntegrationTest { + private static final String EXCEED_WAIT_ATTEMPTS_ERROR_MESSAGE = + "Exceeded maximum attempts waiting for Search Index creation in Atlas cluster. Index document: %s"; + + private static final String VECTOR_INDEX = "vector_search_index"; + private static final String VECTOR_FIELD_INT_8 = "int8Vector"; + private static final String VECTOR_FIELD_FLOAT_32 = "float32Vector"; + private static final String VECTOR_FIELD_LEGACY_DOUBLE_LIST = "legacyDoubleVector"; + private static final int LIMIT = 5; + private static final String FIELD_YEAR = "year"; + private static CollectionHelper collectionHelper; + private static final BsonDocument VECTOR_SEARCH_INDEX_DEFINITION = BsonDocument.parse( + "{" + + " fields: [" + + " {" + + " path: '" + VECTOR_FIELD_INT_8 + "'," + + " numDimensions: 5," + + " similarity: 'cosine'," + + " type: 'vector'," + + " }," + + " {" + + " path: '" + VECTOR_FIELD_FLOAT_32 + "'," + + " numDimensions: 5," + + " similarity: 'cosine'," + + " type: 'vector'," + + " }," + + " {" + + " path: '" + VECTOR_FIELD_LEGACY_DOUBLE_LIST + "'," + + " numDimensions: 5," + + " similarity: 'cosine'," + + " type: 'vector'," + + " }," + + " {" + + " path: '" + FIELD_YEAR + "'," + + " type: 'filter'," + + " }," + + " ]" + + "}"); + + @BeforeAll + static void beforeAll() { + assumeTrue(isAtlasSearchTest()); + assumeTrue(serverVersionAtLeast(6, 0)); + + collectionHelper = + new CollectionHelper<>(new DocumentCodec(), new MongoNamespace("javaVectorSearchTest", AggregatesVectorSearchIntegrationTest.class.getSimpleName())); + collectionHelper.drop(); + collectionHelper.insertDocuments( + new Document() + .append("_id", 0) + .append(VECTOR_FIELD_INT_8, Vector.int8Vector(new byte[]{0, 1, 2, 3, 4})) + .append(VECTOR_FIELD_FLOAT_32, Vector.floatVector(new float[]{0.0001f, 1.12345f, 2.23456f, 3.34567f, 4.45678f})) + .append(VECTOR_FIELD_LEGACY_DOUBLE_LIST, new double[]{0.0001, 1.12345, 2.23456, 3.34567, 4.45678}) + .append(FIELD_YEAR, 2016), + new Document() + .append("_id", 1) + .append(VECTOR_FIELD_INT_8, Vector.int8Vector(new byte[]{1, 2, 3, 4, 5})) + .append(VECTOR_FIELD_FLOAT_32, Vector.floatVector(new float[]{1.0001f, 2.12345f, 3.23456f, 4.34567f, 5.45678f})) + .append(VECTOR_FIELD_LEGACY_DOUBLE_LIST, new double[]{1.0001, 2.12345, 3.23456, 4.34567, 5.45678}) + .append(FIELD_YEAR, 2017), + new Document() + .append("_id", 2) + .append(VECTOR_FIELD_INT_8, Vector.int8Vector(new byte[]{2, 3, 4, 5, 6})) + .append(VECTOR_FIELD_FLOAT_32, Vector.floatVector(new float[]{2.0002f, 3.12345f, 4.23456f, 5.34567f, 6.45678f})) + .append(VECTOR_FIELD_LEGACY_DOUBLE_LIST, new double[]{2.0002, 3.12345, 4.23456, 5.34567, 6.45678}) + .append(FIELD_YEAR, 2018), + new Document() + .append("_id", 3) + .append(VECTOR_FIELD_INT_8, Vector.int8Vector(new byte[]{3, 4, 5, 6, 7})) + .append(VECTOR_FIELD_FLOAT_32, Vector.floatVector(new float[]{3.0003f, 4.12345f, 5.23456f, 6.34567f, 7.45678f})) + .append(VECTOR_FIELD_LEGACY_DOUBLE_LIST, new double[]{3.0003, 4.12345, 5.23456, 6.34567, 7.45678}) + .append(FIELD_YEAR, 2019), + new Document() + .append("_id", 4) + .append(VECTOR_FIELD_INT_8, Vector.int8Vector(new byte[]{4, 5, 6, 7, 8})) + .append(VECTOR_FIELD_FLOAT_32, Vector.floatVector(new float[]{4.0004f, 5.12345f, 6.23456f, 7.34567f, 8.45678f})) + .append(VECTOR_FIELD_LEGACY_DOUBLE_LIST, new double[]{4.0004, 5.12345, 6.23456, 7.34567, 8.45678}) + .append(FIELD_YEAR, 2020), + new Document() + .append("_id", 5) + .append(VECTOR_FIELD_INT_8, Vector.int8Vector(new byte[]{5, 6, 7, 8, 9})) + .append(VECTOR_FIELD_FLOAT_32, Vector.floatVector(new float[]{5.0005f, 6.12345f, 7.23456f, 8.34567f, 9.45678f})) + .append(VECTOR_FIELD_LEGACY_DOUBLE_LIST, new double[]{5.0005, 6.12345, 7.23456, 8.34567, 9.45678}) + .append(FIELD_YEAR, 2021), + new Document() + .append("_id", 6) + .append(VECTOR_FIELD_INT_8, Vector.int8Vector(new byte[]{6, 7, 8, 9, 10})) + .append(VECTOR_FIELD_FLOAT_32, Vector.floatVector(new float[]{6.0006f, 7.12345f, 8.23456f, 9.34567f, 10.45678f})) + .append(VECTOR_FIELD_LEGACY_DOUBLE_LIST, new double[]{6.0006, 7.12345, 8.23456, 9.34567, 10.45678}) + .append(FIELD_YEAR, 2022), + new Document() + .append("_id", 7) + .append(VECTOR_FIELD_INT_8, Vector.int8Vector(new byte[]{7, 8, 9, 10, 11})) + .append(VECTOR_FIELD_FLOAT_32, Vector.floatVector(new float[]{7.0007f, 8.12345f, 9.23456f, 10.34567f, 11.45678f})) + .append(VECTOR_FIELD_LEGACY_DOUBLE_LIST, new double[]{7.0007, 8.12345, 9.23456, 10.34567, 11.45678}) + .append(FIELD_YEAR, 2023), + new Document() + .append("_id", 8) + .append(VECTOR_FIELD_INT_8, Vector.int8Vector(new byte[]{8, 9, 10, 11, 12})) + .append(VECTOR_FIELD_FLOAT_32, Vector.floatVector(new float[]{8.0008f, 9.12345f, 10.23456f, 11.34567f, 12.45678f})) + .append(VECTOR_FIELD_LEGACY_DOUBLE_LIST, new double[]{8.0008, 9.12345, 10.23456, 11.34567, 12.45678}) + .append(FIELD_YEAR, 2024), + new Document() + .append("_id", 9) + .append(VECTOR_FIELD_INT_8, Vector.int8Vector(new byte[]{9, 10, 11, 12, 13})) + .append(VECTOR_FIELD_FLOAT_32, Vector.floatVector(new float[]{9.0009f, 10.12345f, 11.23456f, 12.34567f, 13.45678f})) + .append(VECTOR_FIELD_LEGACY_DOUBLE_LIST, new double[]{9.0009, 10.12345, 11.23456, 12.34567, 13.45678}) + .append(FIELD_YEAR, 2025) + ); + + collectionHelper.createSearchIndex( + new SearchIndexRequest(VECTOR_SEARCH_INDEX_DEFINITION, VECTOR_INDEX, + SearchIndexType.vectorSearch())); + awaitIndexCreation(); + } + + @AfterAll + static void afterAll() { + if (collectionHelper != null) { + collectionHelper.drop(); + } + } + + private static Stream provideSupportedVectors() { + return Stream.of( + arguments(Vector.int8Vector(new byte[]{0, 1, 2, 3, 4}), + // `multi` is used here only to verify that it is tolerated + fieldPath(VECTOR_FIELD_INT_8).multi("ignored"), + approximateVectorSearchOptions(LIMIT * 2)), + arguments(Vector.int8Vector(new byte[]{0, 1, 2, 3, 4}), + fieldPath(VECTOR_FIELD_INT_8), + approximateVectorSearchOptions(LIMIT * 2)), + + arguments(Vector.floatVector(new float[]{0.0001f, 1.12345f, 2.23456f, 3.34567f, 4.45678f}), + // `multi` is used here only to verify that it is tolerated + fieldPath(VECTOR_FIELD_FLOAT_32).multi("ignored"), + approximateVectorSearchOptions(LIMIT * 2)), + arguments(Vector.floatVector(new float[]{0.0001f, 1.12345f, 2.23456f, 3.34567f, 4.45678f}), + fieldPath(VECTOR_FIELD_FLOAT_32), + approximateVectorSearchOptions(LIMIT * 2)), + + arguments(Vector.floatVector(new float[]{0.0001f, 1.12345f, 2.23456f, 3.34567f, 4.45678f}), + // `multi` is used here only to verify that it is tolerated + fieldPath(VECTOR_FIELD_FLOAT_32).multi("ignored"), + exactVectorSearchOptions()), + arguments(Vector.floatVector(new float[]{0.0001f, 1.12345f, 2.23456f, 3.34567f, 4.45678f}), + fieldPath(VECTOR_FIELD_FLOAT_32), + exactVectorSearchOptions()), + + arguments(Vector.floatVector(new float[]{0.0001f, 1.12345f, 2.23456f, 3.34567f, 4.45678f}), + // `multi` is used here only to verify that it is tolerated + fieldPath(VECTOR_FIELD_LEGACY_DOUBLE_LIST).multi("ignored"), + exactVectorSearchOptions()), + arguments(Vector.floatVector(new float[]{0.0001f, 1.12345f, 2.23456f, 3.34567f, 4.45678f}), + fieldPath(VECTOR_FIELD_LEGACY_DOUBLE_LIST), + exactVectorSearchOptions()), + + arguments(Vector.floatVector(new float[]{0.0001f, 1.12345f, 2.23456f, 3.34567f, 4.45678f}), + // `multi` is used here only to verify that it is tolerated + fieldPath(VECTOR_FIELD_LEGACY_DOUBLE_LIST).multi("ignored"), + approximateVectorSearchOptions(LIMIT * 2)), + arguments(Vector.floatVector(new float[]{0.0001f, 1.12345f, 2.23456f, 3.34567f, 4.45678f}), + fieldPath(VECTOR_FIELD_LEGACY_DOUBLE_LIST), + approximateVectorSearchOptions(LIMIT * 2)) + ); + } + + @ParameterizedTest + @MethodSource("provideSupportedVectors") + void shouldSearchByVectorWithSearchScore(final Vector vector, + final FieldSearchPath fieldSearchPath, + final VectorSearchOptions vectorSearchOptions) { + //given + List pipeline = asList( + Aggregates.vectorSearch( + fieldSearchPath, + vector, + VECTOR_INDEX, LIMIT, + vectorSearchOptions), + Aggregates.project( + fields( + metaVectorSearchScore("vectorSearchScore") + )) + ); + + //when + List aggregate = collectionHelper.aggregate(pipeline); + + //then + Assertions.assertEquals(LIMIT, aggregate.size()); + assertScoreIsDecreasing(aggregate); + Document highestScoreDocument = aggregate.get(0); + assertEquals(1, highestScoreDocument.getDouble("vectorSearchScore")); + } + + @ParameterizedTest + @MethodSource("provideSupportedVectors") + void shouldSearchByVector(final Vector vector, + final FieldSearchPath fieldSearchPath, + final VectorSearchOptions vectorSearchOptions) { + //given + List pipeline = asList( + Aggregates.vectorSearch( + fieldSearchPath, + vector, + VECTOR_INDEX, LIMIT, + vectorSearchOptions) + ); + + //when + List aggregate = collectionHelper.aggregate(pipeline); + + //then + Assertions.assertEquals(LIMIT, aggregate.size()); + assertFalse( + aggregate.stream() + .anyMatch(document -> document.containsKey("vectorSearchScore")) + ); + } + + @ParameterizedTest + @MethodSource("provideSupportedVectors") + void shouldSearchByVectorWithFilter(final Vector vector, + final FieldSearchPath fieldSearchPath, + final VectorSearchOptions vectorSearchOptions) { + Consumer asserter = filter -> { + List pipeline = singletonList( + Aggregates.vectorSearch( + fieldSearchPath, vector, VECTOR_INDEX, 1, + vectorSearchOptions.filter(filter)) + ); + + List aggregate = collectionHelper.aggregate(pipeline); + Assertions.assertFalse(aggregate.isEmpty()); + }; + + assertAll( + () -> asserter.accept(lt("year", 2020)), + () -> asserter.accept(lte("year", 2020)), + () -> asserter.accept(eq("year", 2020)), + () -> asserter.accept(gte("year", 2016)), + () -> asserter.accept(gt("year", 2015)), + () -> asserter.accept(ne("year", 2016)), + () -> asserter.accept(in("year", 2000, 2024)), + () -> asserter.accept(nin("year", 2000, 2024)), + () -> asserter.accept(and(gte("year", 2015), lte("year", 2017))), + () -> asserter.accept(or(eq("year", 2015), eq("year", 2017))) + ); + } + + private static void assertScoreIsDecreasing(final List aggregate) { + double previousScore = Integer.MAX_VALUE; + for (Document document : aggregate) { + Double vectorSearchScore = document.getDouble("vectorSearchScore"); + assertTrue(vectorSearchScore > 0, "Expected positive score"); + assertTrue(vectorSearchScore < previousScore, "Expected decreasing score"); + previousScore = vectorSearchScore; + } + } + + private static void awaitIndexCreation() { + int attempts = 10; + Optional searchIndex = Optional.empty(); + + while (attempts-- > 0) { + searchIndex = collectionHelper.listSearchIndex(VECTOR_INDEX); + if (searchIndex.filter(document -> document.getBoolean("queryable")) + .isPresent()) { + return; + } + + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new MongoInterruptedException(null, e); + } + } + + searchIndex.ifPresent(document -> + Assertions.fail(format(EXCEED_WAIT_ATTEMPTS_ERROR_MESSAGE, document.toJson()))); + Assertions.fail(format(EXCEED_WAIT_ATTEMPTS_ERROR_MESSAGE, "null")); + } +} diff --git a/driver-core/src/test/functional/com/mongodb/client/test/CollectionHelper.java b/driver-core/src/test/functional/com/mongodb/client/test/CollectionHelper.java index e297726d325..adce165ee51 100644 --- a/driver-core/src/test/functional/com/mongodb/client/test/CollectionHelper.java +++ b/driver-core/src/test/functional/com/mongodb/client/test/CollectionHelper.java @@ -43,11 +43,14 @@ import com.mongodb.internal.operation.CountDocumentsOperation; import com.mongodb.internal.operation.CreateCollectionOperation; import com.mongodb.internal.operation.CreateIndexesOperation; +import com.mongodb.internal.operation.CreateSearchIndexesOperation; import com.mongodb.internal.operation.DropCollectionOperation; import com.mongodb.internal.operation.DropDatabaseOperation; import com.mongodb.internal.operation.FindOperation; import com.mongodb.internal.operation.ListIndexesOperation; +import com.mongodb.internal.operation.ListSearchIndexesOperation; import com.mongodb.internal.operation.MixedBulkWriteOperation; +import com.mongodb.internal.operation.SearchIndexRequest; import org.bson.BsonArray; import org.bson.BsonDocument; import org.bson.BsonDocumentWrapper; @@ -56,6 +59,7 @@ import org.bson.BsonString; import org.bson.BsonValue; import org.bson.Document; +import org.bson.assertions.Assertions; import org.bson.codecs.BsonDocumentCodec; import org.bson.codecs.Codec; import org.bson.codecs.Decoder; @@ -65,6 +69,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import static com.mongodb.ClusterFixture.executeAsync; @@ -297,6 +302,25 @@ public List find() { return find(codec); } + public Optional listSearchIndex(final String indexName) { + ListSearchIndexesOperation listSearchIndexesOperation = + new ListSearchIndexesOperation<>(namespace, codec, indexName, null, null, null, null, true); + BatchCursor cursor = listSearchIndexesOperation.execute(getBinding()); + + List results = new ArrayList<>(); + while (cursor.hasNext()) { + results.addAll(cursor.next()); + } + Assertions.assertTrue("Expected at most one result, but found " + results.size(), results.size() <= 1); + return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0)); + } + + public void createSearchIndex(final SearchIndexRequest searchIndexModel) { + CreateSearchIndexesOperation searchIndexesOperation = + new CreateSearchIndexesOperation(namespace, singletonList(searchIndexModel)); + searchIndexesOperation.execute(getBinding()); + } + public List find(final Codec codec) { BatchCursor cursor = new FindOperation<>(namespace, codec) .sort(new BsonDocument("_id", new BsonInt32(1))) diff --git a/driver-core/src/test/resources/client-side-encryption/legacy/explain.json b/driver-core/src/test/resources/client-side-encryption/legacy/explain.json index 0e451e4818a..8ca3b48d37d 100644 --- a/driver-core/src/test/resources/client-side-encryption/legacy/explain.json +++ b/driver-core/src/test/resources/client-side-encryption/legacy/explain.json @@ -1,7 +1,7 @@ { "runOn": [ { - "minServerVersion": "4.1.10" + "minServerVersion": "7.0.0" } ], "database_name": "default", diff --git a/driver-core/src/test/resources/client-side-encryption/legacy/fle2v2-Compact.json b/driver-core/src/test/resources/client-side-encryption/legacy/fle2v2-Compact.json index 85fb8bf607a..868095e1e64 100644 --- a/driver-core/src/test/resources/client-side-encryption/legacy/fle2v2-Compact.json +++ b/driver-core/src/test/resources/client-side-encryption/legacy/fle2v2-Compact.json @@ -230,4 +230,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/driver-core/src/test/resources/client-side-encryption/legacy/fle2v2-Rangev2-Compact.json b/driver-core/src/test/resources/client-side-encryption/legacy/fle2v2-Rangev2-Compact.json index 59241927ca1..bba9f25535a 100644 --- a/driver-core/src/test/resources/client-side-encryption/legacy/fle2v2-Rangev2-Compact.json +++ b/driver-core/src/test/resources/client-side-encryption/legacy/fle2v2-Rangev2-Compact.json @@ -6,7 +6,8 @@ "replicaset", "sharded", "load-balanced" - ] + ], + "serverless": "forbid" } ], "database_name": "default", diff --git a/driver-core/src/test/resources/client-side-encryption/legacy/getMore.json b/driver-core/src/test/resources/client-side-encryption/legacy/getMore.json index ee99bf7537e..94e788ef61d 100644 --- a/driver-core/src/test/resources/client-side-encryption/legacy/getMore.json +++ b/driver-core/src/test/resources/client-side-encryption/legacy/getMore.json @@ -216,7 +216,10 @@ "command_started_event": { "command": { "getMore": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "collection": "default", "batchSize": 2 diff --git a/driver-core/src/test/resources/client-side-encryption/legacy/namedKMS.json b/driver-core/src/test/resources/client-side-encryption/legacy/namedKMS.json index 394a6ac5484..c859443585b 100644 --- a/driver-core/src/test/resources/client-side-encryption/legacy/namedKMS.json +++ b/driver-core/src/test/resources/client-side-encryption/legacy/namedKMS.json @@ -194,4 +194,4 @@ } } ] -} \ No newline at end of file +} diff --git a/driver-core/src/test/resources/initial-dns-seedlist-discovery/README.md b/driver-core/src/test/resources/initial-dns-seedlist-discovery/README.md new file mode 100644 index 00000000000..99a1e8e3f06 --- /dev/null +++ b/driver-core/src/test/resources/initial-dns-seedlist-discovery/README.md @@ -0,0 +1,182 @@ +# Initial DNS Seedlist Discovery tests + +This directory contains platform-independent tests that drivers can use to prove their conformance to the Initial DNS +Seedlist Discovery spec. + +## Prose Tests + +For the following prose tests, it is assumed drivers are be able to stub DNS results to easily test invalid DNS +resolution results. + +### 1. Allow SRVs with fewer than 3 `.` separated parts + +When running validation on an SRV string before DNS resolution, do not throw a error due to number of SRV parts. + +- `mongodb+srv://localhost` +- `mongodb+srv://mongo.local` + +### 2. Throw when return address does not end with SRV domain + +When given a returned address that does NOT end with the original SRV's domain name, throw a runtime error. + +For this test, run each of the following cases: + +- the SRV `mongodb+srv://localhost` resolving to `localhost.mongodb` +- the SRV `mongodb+srv://mongo.local` resolving to `test_1.evil.local` +- the SRV `mongodb+srv://blogs.mongodb.com` resolving to `blogs.evil.com` + +Remember, the domain of an SRV with one or two `.` separated parts is the SRVs entire hostname. + +### 3. Throw when return address is identical to SRV hostname + +When given a returned address that is identical to the SRV hostname and the SRV hostname has fewer than three `.` +separated parts, throw a runtime error. + +For this test, run each of the following cases: + +- the SRV `mongodb+srv://localhost` resolving to `localhost` +- the SRV `mongodb+srv://mongo.local` resolving to `mongo.local` + +### 4. Throw when return address does not contain `.` separating shared part of domain + +When given a returned address that does NOT share the domain name of the SRV record because it's missing a `.`, throw a +runtime error. + +For this test, run each of the following cases: + +- the SRV `mongodb+srv://localhost` resolving to `test_1.cluster_1localhost` +- the SRV `mongodb+srv://mongo.local` resolving to `test_1.my_hostmongo.local` +- the SRV `mongodb+srv://blogs.mongodb.com` resolving to `cluster.testmongodb.com` + +## Test Setup + +The tests in the `replica-set` directory MUST be executed against a three-node replica set on localhost ports 27017, +27018, and 27019 with replica set name `repl0`. + +The tests in the `load-balanced` directory MUST be executed against a load-balanced sharded cluster with the mongos +servers running on localhost ports 27017 and 27018 and `--loadBalancerPort` 27050 and 27051, respectively (corresponding +to the script in +[drivers-evergreen-tools](https://github.com/mongodb-labs/drivers-evergreen-tools/blob/master/.evergreen/run-load-balancer.sh)). +The load balancers, shard servers, and config servers may run on any open ports. + +The tests in the `sharded` directory MUST be executed against a sharded cluster with the mongos servers running on +localhost ports 27017 and 27018. Shard servers and config servers may run on any open ports. + +In all cases, the clusters MUST be started with SSL enabled. + +To run the tests that accompany this spec, you need to configure the SRV and TXT records with a real name server. The +following records are required for these tests: + +``` +Record TTL Class Address +localhost.test.build.10gen.cc. 86400 IN A 127.0.0.1 +localhost.sub.test.build.10gen.cc. 86400 IN A 127.0.0.1 + +Record TTL Class Port Target +_mongodb._tcp.test1.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. +_mongodb._tcp.test1.test.build.10gen.cc. 86400 IN SRV 27018 localhost.test.build.10gen.cc. +_mongodb._tcp.test2.test.build.10gen.cc. 86400 IN SRV 27018 localhost.test.build.10gen.cc. +_mongodb._tcp.test2.test.build.10gen.cc. 86400 IN SRV 27019 localhost.test.build.10gen.cc. +_mongodb._tcp.test3.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. +_mongodb._tcp.test5.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. +_mongodb._tcp.test6.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. +_mongodb._tcp.test7.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. +_mongodb._tcp.test8.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. +_mongodb._tcp.test10.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. +_mongodb._tcp.test11.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. +_mongodb._tcp.test12.test.build.10gen.cc. 86400 IN SRV 27017 localhost.build.10gen.cc. +_mongodb._tcp.test13.test.build.10gen.cc. 86400 IN SRV 27017 test.build.10gen.cc. +_mongodb._tcp.test14.test.build.10gen.cc. 86400 IN SRV 27017 localhost.not-test.build.10gen.cc. +_mongodb._tcp.test15.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.not-build.10gen.cc. +_mongodb._tcp.test16.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.not-10gen.cc. +_mongodb._tcp.test17.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.not-cc. +_mongodb._tcp.test18.test.build.10gen.cc. 86400 IN SRV 27017 localhost.sub.test.build.10gen.cc. +_mongodb._tcp.test19.test.build.10gen.cc. 86400 IN SRV 27017 localhost.evil.build.10gen.cc. +_mongodb._tcp.test19.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. +_mongodb._tcp.test20.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. +_mongodb._tcp.test21.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. +_customname._tcp.test22.test.build.10gen.cc 86400 IN SRV 27017 localhost.test.build.10gen.cc. +_mongodb._tcp.test23.test.build.10gen.cc. 86400 IN SRV 8000 localhost.test.build.10gen.cc. +_mongodb._tcp.test24.test.build.10gen.cc. 86400 IN SRV 8000 localhost.test.build.10gen.cc. + +Record TTL Class Text +test5.test.build.10gen.cc. 86400 IN TXT "replicaSet=repl0&authSource=thisDB" +test6.test.build.10gen.cc. 86400 IN TXT "replicaSet=repl0" +test6.test.build.10gen.cc. 86400 IN TXT "authSource=otherDB" +test7.test.build.10gen.cc. 86400 IN TXT "ssl=false" +test8.test.build.10gen.cc. 86400 IN TXT "authSource" +test10.test.build.10gen.cc. 86400 IN TXT "socketTimeoutMS=500" +test11.test.build.10gen.cc. 86400 IN TXT "replicaS" "et=rep" "l0" +test20.test.build.10gen.cc. 86400 IN TXT "loadBalanced=true" +test21.test.build.10gen.cc. 86400 IN TXT "loadBalanced=false" +test24.test.build.10gen.cc. 86400 IN TXT "loadBalanced=true" +``` + +Notes: + +- `test4` is omitted deliberately to test what happens with no SRV record. +- `test9` is missing because it was deleted during the development of the tests. +- The missing `test.` sub-domain in the SRV record target for `test12` is deliberate. +- `test22` is used to test a custom service name (`customname`). +- `test23` and `test24` point to port 8000 (HAProxy) and are used for load-balanced tests. + +In our tests we have used `localhost.test.build.10gen.cc` as the domain, and then configured +`localhost.test.build.10gen.cc` to resolve to 127.0.0.1. + +You need to adapt the records shown above to replace `test.build.10gen.cc` with your own domain name, and update the +"uri" field in the YAML or JSON files in this directory with the actual domain. + +## Test Format and Use + +These YAML and JSON files contain the following fields: + +- `uri`: a `mongodb+srv` connection string +- `seeds`: the expected set of initial seeds discovered from the SRV record +- `numSeeds`: the expected number of initial seeds discovered from the SRV record. This is mainly used to test + `srvMaxHosts`, since randomly selected hosts cannot be deterministically asserted. +- `hosts`: the discovered topology's list of hosts once SDAM completes a scan +- `numHosts`: the expected number of hosts discovered once SDAM completes a scan. This is mainly used to test + `srvMaxHosts`, since randomly selected hosts cannot be deterministically asserted. +- `options`: the parsed [URI options](../../uri-options/uri-options.md) as discovered from the + [Connection String](../../connection-string/connection-string-spec.md)'s "Connection Options" component and SRV + resolution (e.g. TXT records, implicit `tls` default). +- `parsed_options`: additional, parsed options from other + [Connection String](../../connection-string/connection-string-spec.md) components. This is mainly used for asserting + `UserInfo` (as `user` and `password`) and `Auth database` (as `auth_database`). +- `error`: indicates that the parsing of the URI, or the resolving or contents of the SRV or TXT records included + errors. +- `comment`: a comment to indicate why a test would fail. +- `ping`: if false, the test runner should not run a "ping" operation. + +For each YAML file: + +- Create a MongoClient initialized with the `mongodb+srv` connection string. +- Run a "ping" operation unless `ping` is false or `error` is true. + +Assertions: + +- If `seeds` is specified, drivers SHOULD verify that the set of hosts in the client's initial seedlist matches the list + in `seeds`. If `numSeeds` is specified, drivers SHOULD verify that the size of that set matches `numSeeds`. + +- If `hosts` is specified, drivers MUST verify that the set of ServerDescriptions in the client's TopologyDescription + eventually matches the list in `hosts`. If `numHosts` is specified, drivers MUST verify that the size of that set + matches `numHosts`. + +- If `options` is specified, drivers MUST verify each of the values under `options` match the MongoClient's parsed value + for that option. There may be other options parsed by the MongoClient as well, which a test does not verify. + +- If `parsed_options` is specified, drivers MUST verify that each of the values under `parsed_options` match the + MongoClient's parsed value for that option. Supported values include, but are not limited to, `user` and `password` + (parsed from `UserInfo`) and `auth_database` (parsed from `Auth database`). + +- If `error` is specified and `true`, drivers MUST verify that initializing the MongoClient throws an error. If `error` + is not specified or is `false`, both initializing the MongoClient and running a ping operation must succeed without + throwing any errors. + +- If `ping` is not specified or `true`, drivers MUST verify that running a "ping" operation using the initialized + MongoClient succeeds. If `ping` is `false`, drivers MUST NOT run a "ping" operation. + + > **Note:** These tests are expected to be run against MongoDB databases with and without authentication enabled. The + > "ping" operation does not require authentication so should succeed with URIs that contain no userinfo (i.e. no + > username and password). Tests with URIs that contain userinfo always set `ping` to `false` because some drivers will + > fail handshake on a connection if userinfo is provided but incorrect. diff --git a/driver-core/src/test/resources/initial-dns-seedlist-discovery/README.rst b/driver-core/src/test/resources/initial-dns-seedlist-discovery/README.rst deleted file mode 100644 index e8b33936845..00000000000 --- a/driver-core/src/test/resources/initial-dns-seedlist-discovery/README.rst +++ /dev/null @@ -1,164 +0,0 @@ -==================================== -Initial DNS Seedlist Discovery tests -==================================== - -This directory contains platform-independent tests that drivers can use -to prove their conformance to the Initial DNS Seedlist Discovery spec. - -Test Setup ----------- - -The tests in the ``replica-set`` directory MUST be executed against a -three-node replica set on localhost ports 27017, 27018, and 27019 with -replica set name ``repl0``. - -The tests in the ``load-balanced`` directory MUST be executed against a -load-balanced sharded cluster with the mongos servers running on localhost ports -27017 and 27018 and ``--loadBalancerPort`` 27050 and 27051, respectively -(corresponding to the script in `drivers-evergreen-tools`_). The load balancers, -shard servers, and config servers may run on any open ports. - -.. _`drivers-evergreen-tools`: https://github.com/mongodb-labs/drivers-evergreen-tools/blob/master/.evergreen/run-load-balancer.sh - -The tests in the ``sharded`` directory MUST be executed against a sharded -cluster with the mongos servers running on localhost ports 27017 and 27018. -Shard servers and config servers may run on any open ports. - -In all cases, the clusters MUST be started with SSL enabled. - -To run the tests that accompany this spec, you need to configure the SRV and -TXT records with a real name server. The following records are required for -these tests:: - - Record TTL Class Address - localhost.test.build.10gen.cc. 86400 IN A 127.0.0.1 - localhost.sub.test.build.10gen.cc. 86400 IN A 127.0.0.1 - - Record TTL Class Port Target - _mongodb._tcp.test1.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. - _mongodb._tcp.test1.test.build.10gen.cc. 86400 IN SRV 27018 localhost.test.build.10gen.cc. - _mongodb._tcp.test2.test.build.10gen.cc. 86400 IN SRV 27018 localhost.test.build.10gen.cc. - _mongodb._tcp.test2.test.build.10gen.cc. 86400 IN SRV 27019 localhost.test.build.10gen.cc. - _mongodb._tcp.test3.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. - _mongodb._tcp.test5.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. - _mongodb._tcp.test6.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. - _mongodb._tcp.test7.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. - _mongodb._tcp.test8.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. - _mongodb._tcp.test10.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. - _mongodb._tcp.test11.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. - _mongodb._tcp.test12.test.build.10gen.cc. 86400 IN SRV 27017 localhost.build.10gen.cc. - _mongodb._tcp.test13.test.build.10gen.cc. 86400 IN SRV 27017 test.build.10gen.cc. - _mongodb._tcp.test14.test.build.10gen.cc. 86400 IN SRV 27017 localhost.not-test.build.10gen.cc. - _mongodb._tcp.test15.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.not-build.10gen.cc. - _mongodb._tcp.test16.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.not-10gen.cc. - _mongodb._tcp.test17.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.not-cc. - _mongodb._tcp.test18.test.build.10gen.cc. 86400 IN SRV 27017 localhost.sub.test.build.10gen.cc. - _mongodb._tcp.test19.test.build.10gen.cc. 86400 IN SRV 27017 localhost.evil.build.10gen.cc. - _mongodb._tcp.test19.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. - _mongodb._tcp.test20.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. - _mongodb._tcp.test21.test.build.10gen.cc. 86400 IN SRV 27017 localhost.test.build.10gen.cc. - _customname._tcp.test22.test.build.10gen.cc 86400 IN SRV 27017 localhost.test.build.10gen.cc. - _mongodb._tcp.test23.test.build.10gen.cc. 86400 IN SRV 8000 localhost.test.build.10gen.cc. - _mongodb._tcp.test24.test.build.10gen.cc. 86400 IN SRV 8000 localhost.test.build.10gen.cc. - - Record TTL Class Text - test5.test.build.10gen.cc. 86400 IN TXT "replicaSet=repl0&authSource=thisDB" - test6.test.build.10gen.cc. 86400 IN TXT "replicaSet=repl0" - test6.test.build.10gen.cc. 86400 IN TXT "authSource=otherDB" - test7.test.build.10gen.cc. 86400 IN TXT "ssl=false" - test8.test.build.10gen.cc. 86400 IN TXT "authSource" - test10.test.build.10gen.cc. 86400 IN TXT "socketTimeoutMS=500" - test11.test.build.10gen.cc. 86400 IN TXT "replicaS" "et=rep" "l0" - test20.test.build.10gen.cc. 86400 IN TXT "loadBalanced=true" - test21.test.build.10gen.cc. 86400 IN TXT "loadBalanced=false" - test24.test.build.10gen.cc. 86400 IN TXT "loadBalanced=true" - -Notes: - -- ``test4`` is omitted deliberately to test what happens with no SRV record. -- ``test9`` is missing because it was deleted during the development of the - tests. -- The missing ``test.`` sub-domain in the SRV record target for ``test12`` is - deliberate. -- ``test22`` is used to test a custom service name (``customname``). -- ``test23`` and ``test24`` point to port 8000 (HAProxy) and are used for - load-balanced tests. - -In our tests we have used ``localhost.test.build.10gen.cc`` as the domain, and -then configured ``localhost.test.build.10gen.cc`` to resolve to 127.0.0.1. - -You need to adapt the records shown above to replace ``test.build.10gen.cc`` -with your own domain name, and update the "uri" field in the YAML or JSON files -in this directory with the actual domain. - -Test Format and Use -------------------- - -These YAML and JSON files contain the following fields: - -- ``uri``: a ``mongodb+srv`` connection string -- ``seeds``: the expected set of initial seeds discovered from the SRV record -- ``numSeeds``: the expected number of initial seeds discovered from the SRV - record. This is mainly used to test ``srvMaxHosts``, since randomly selected - hosts cannot be deterministically asserted. -- ``hosts``: the discovered topology's list of hosts once SDAM completes a scan -- ``numHosts``: the expected number of hosts discovered once SDAM completes a - scan. This is mainly used to test ``srvMaxHosts``, since randomly selected - hosts cannot be deterministically asserted. -- ``options``: the parsed `URI options`_ as discovered from the - `Connection String`_'s "Connection Options" component and SRV resolution - (e.g. TXT records, implicit ``tls`` default). -- ``parsed_options``: additional, parsed options from other `Connection String`_ - components. This is mainly used for asserting ``UserInfo`` (as ``user`` and - ``password``) and ``Auth database`` (as ``auth_database``). -- ``error``: indicates that the parsing of the URI, or the resolving or - contents of the SRV or TXT records included errors. -- ``comment``: a comment to indicate why a test would fail. -- ``ping``: if false, the test runner should not run a "ping" operation. - -.. _`Connection String`: ../../connection-string/connection-string-spec.rst -.. _`URI options`: ../../uri-options/uri-options.rst - -For each YAML file: - -- Create a MongoClient initialized with the ``mongodb+srv`` - connection string. -- Run a "ping" operation unless ``ping`` is false or ``error`` is true. - -Assertions: - -- If ``seeds`` is specified, drivers SHOULD verify that the set of hosts in the - client's initial seedlist matches the list in ``seeds``. If ``numSeeds`` is - specified, drivers SHOULD verify that the size of that set matches - ``numSeeds``. - -- If ``hosts`` is specified, drivers MUST verify that the set of - ServerDescriptions in the client's TopologyDescription eventually matches the - list in ``hosts``. If ``numHosts`` is specified, drivers MUST verify that the - size of that set matches ``numHosts``. - -- If ``options`` is specified, drivers MUST verify each of the values under - ``options`` match the MongoClient's parsed value for that option. There may be - other options parsed by the MongoClient as well, which a test does not verify. - -- If ``parsed_options`` is specified, drivers MUST verify that each of the - values under ``parsed_options`` match the MongoClient's parsed value for that - option. Supported values include, but are not limited to, ``user`` and - ``password`` (parsed from ``UserInfo``) and ``auth_database`` (parsed from - ``Auth database``). - -- If ``error`` is specified and ``true``, drivers MUST verify that initializing - the MongoClient throws an error. If ``error`` is not specified or is - ``false``, both initializing the MongoClient and running a ping operation must - succeed without throwing any errors. - -- If ``ping`` is not specified or ``true``, drivers MUST verify that running a - "ping" operation using the initialized MongoClient succeeds. If ``ping`` is - ``false``, drivers MUST NOT run a "ping" operation. - - **Note:** These tests are expected to be run against MongoDB databases with - and without authentication enabled. The "ping" operation does not require - authentication so should succeed with URIs that contain no userinfo (i.e. - no username and password). Tests with URIs that contain userinfo always set - ``ping`` to ``false`` because some drivers will fail handshake on a - connection if userinfo is provided but incorrect. diff --git a/driver-core/src/test/resources/initial-dns-seedlist-discovery/replica-set/not-enough-parts.json b/driver-core/src/test/resources/initial-dns-seedlist-discovery/replica-set/not-enough-parts.json deleted file mode 100644 index 7cfce2ec57e..00000000000 --- a/driver-core/src/test/resources/initial-dns-seedlist-discovery/replica-set/not-enough-parts.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "uri": "mongodb+srv://10gen.cc/", - "seeds": [], - "hosts": [], - "error": true, - "comment": "Should fail because host in URI does not have {hostname}, {domainname} and {tld}." -} diff --git a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-clusterTime.json b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-clusterTime.json index 55b4ae3fbcb..2b09e548f1d 100644 --- a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-clusterTime.json +++ b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-clusterTime.json @@ -28,7 +28,6 @@ "minServerVersion": "4.0.0", "topologies": [ "replicaset", - "sharded-replicaset", "load-balanced", "sharded" ], diff --git a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-disambiguatedPaths.json b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-disambiguatedPaths.json index 91d8e66da20..e6cc5ef66ed 100644 --- a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-disambiguatedPaths.json +++ b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-disambiguatedPaths.json @@ -28,7 +28,6 @@ "minServerVersion": "6.1.0", "topologies": [ "replicaset", - "sharded-replicaset", "load-balanced", "sharded" ], diff --git a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-errors.json b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-errors.json index 04fe8f04f33..65e99e541ed 100644 --- a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-errors.json +++ b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-errors.json @@ -145,7 +145,7 @@ "minServerVersion": "4.1.11", "topologies": [ "replicaset", - "sharded-replicaset", + "sharded", "load-balanced" ] } @@ -190,7 +190,7 @@ "minServerVersion": "4.2", "topologies": [ "replicaset", - "sharded-replicaset", + "sharded", "load-balanced" ] } diff --git a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-pre_and_post_images.json b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-pre_and_post_images.json index 8beefb2bc82..e62fc034596 100644 --- a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-pre_and_post_images.json +++ b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-pre_and_post_images.json @@ -6,7 +6,7 @@ "minServerVersion": "6.0.0", "topologies": [ "replicaset", - "sharded-replicaset", + "sharded", "load-balanced" ], "serverless": "forbid" diff --git a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-resume-allowlist.json b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-resume-allowlist.json index b4953ec736a..1ec72b432be 100644 --- a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-resume-allowlist.json +++ b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-resume-allowlist.json @@ -6,7 +6,7 @@ "minServerVersion": "3.6", "topologies": [ "replicaset", - "sharded-replicaset", + "sharded", "load-balanced" ], "serverless": "forbid" diff --git a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-resume-errorLabels.json b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-resume-errorLabels.json index f5f4505a9f9..7fd70108f07 100644 --- a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-resume-errorLabels.json +++ b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-resume-errorLabels.json @@ -6,7 +6,7 @@ "minServerVersion": "4.3.1", "topologies": [ "replicaset", - "sharded-replicaset", + "sharded", "load-balanced" ], "serverless": "forbid" diff --git a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-showExpandedEvents.json b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-showExpandedEvents.json index a59a818493c..b9594e0c1e1 100644 --- a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-showExpandedEvents.json +++ b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams-showExpandedEvents.json @@ -6,7 +6,6 @@ "minServerVersion": "6.0.0", "topologies": [ "replicaset", - "sharded-replicaset", "sharded" ], "serverless": "forbid" @@ -463,7 +462,6 @@ "runOnRequirements": [ { "topologies": [ - "sharded-replicaset", "sharded" ] } diff --git a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams.json b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams.json index 476154f4e15..c8b60ed4e25 100644 --- a/driver-core/src/test/resources/unified-test-format/change-streams/change-streams.json +++ b/driver-core/src/test/resources/unified-test-format/change-streams/change-streams.json @@ -210,7 +210,6 @@ "expectEvents": [ { "client": "client0", - "ignoreExtraEvents": true, "events": [ { "commandStartedEvent": { @@ -256,7 +255,6 @@ "expectEvents": [ { "client": "client0", - "ignoreExtraEvents": true, "events": [ { "commandStartedEvent": { @@ -293,7 +291,6 @@ "expectEvents": [ { "client": "client0", - "ignoreExtraEvents": true, "events": [ { "commandStartedEvent": { @@ -349,7 +346,6 @@ "expectEvents": [ { "client": "client0", - "ignoreExtraEvents": true, "events": [ { "commandStartedEvent": { @@ -436,7 +432,6 @@ "expectEvents": [ { "client": "client0", - "ignoreExtraEvents": true, "events": [ { "commandStartedEvent": { diff --git a/driver-core/src/test/resources/unified-test-format/client-side-operation-timeout/command-execution.json b/driver-core/src/test/resources/unified-test-format/client-side-operation-timeout/command-execution.json index b9b306c7fb6..aa9c3eb23f3 100644 --- a/driver-core/src/test/resources/unified-test-format/client-side-operation-timeout/command-execution.json +++ b/driver-core/src/test/resources/unified-test-format/client-side-operation-timeout/command-execution.json @@ -3,7 +3,7 @@ "schemaVersion": "1.9", "runOnRequirements": [ { - "minServerVersion": "4.9", + "minServerVersion": "4.4.7", "topologies": [ "single", "replicaset", diff --git a/driver-core/src/test/resources/unified-test-format/collection-management/modifyCollection-pre_and_post_images.json b/driver-core/src/test/resources/unified-test-format/collection-management/modifyCollection-pre_and_post_images.json index 62c00b5a986..8026faeb17a 100644 --- a/driver-core/src/test/resources/unified-test-format/collection-management/modifyCollection-pre_and_post_images.json +++ b/driver-core/src/test/resources/unified-test-format/collection-management/modifyCollection-pre_and_post_images.json @@ -1,6 +1,6 @@ { "description": "modifyCollection-pre_and_post_images", - "schemaVersion": "1.0", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "6.0", diff --git a/driver-core/src/test/resources/unified-test-format/command-logging/command.json b/driver-core/src/test/resources/unified-test-format/command-logging/command.json index 3d5c2570be2..d2970df692f 100644 --- a/driver-core/src/test/resources/unified-test-format/command-logging/command.json +++ b/driver-core/src/test/resources/unified-test-format/command-logging/command.json @@ -93,6 +93,7 @@ "component": "command", "data": { "message": "Command succeeded", + "databaseName": "logging-tests", "commandName": "ping", "reply": { "$$type": "string" @@ -177,6 +178,7 @@ "component": "command", "data": { "message": "Command failed", + "databaseName": "logging-tests", "commandName": "find", "failure": { "$$exists": true diff --git a/driver-core/src/test/resources/unified-test-format/command-monitoring/command.json b/driver-core/src/test/resources/unified-test-format/command-monitoring/command.json index d2970df692f..c28af95fed2 100644 --- a/driver-core/src/test/resources/unified-test-format/command-monitoring/command.json +++ b/driver-core/src/test/resources/unified-test-format/command-monitoring/command.json @@ -1,34 +1,36 @@ { - "description": "command-logging", - "schemaVersion": "1.13", + "description": "command", + "schemaVersion": "1.0", "createEntities": [ { "client": { "id": "client", - "observeLogMessages": { - "command": "debug" - } + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent", + "commandFailedEvent" + ] } }, { "database": { "id": "database", "client": "client", - "databaseName": "logging-tests" + "databaseName": "command-monitoring-tests" } }, { "collection": { "id": "collection", "database": "database", - "collectionName": "logging-tests-collection" + "collectionName": "test" } } ], "initialData": [ { - "collectionName": "logging-tests-collection", - "databaseName": "logging-tests", + "collectionName": "test", + "databaseName": "command-monitoring-tests", "documents": [ { "_id": 1, @@ -52,159 +54,25 @@ } } ], - "expectLogMessages": [ + "expectEvents": [ { "client": "client", - "messages": [ + "events": [ { - "level": "debug", - "component": "command", - "data": { - "message": "Command started", - "databaseName": "logging-tests", - "commandName": "ping", + "commandStartedEvent": { "command": { - "$$matchAsDocument": { - "$$matchAsRoot": { - "ping": 1, - "$db": "logging-tests" - } - } - }, - "requestId": { - "$$type": [ - "int", - "long" - ] + "ping": 1 }, - "serverHost": { - "$$type": "string" - }, - "serverPort": { - "$$type": [ - "int", - "long" - ] - } - } - }, - { - "level": "debug", - "component": "command", - "data": { - "message": "Command succeeded", - "databaseName": "logging-tests", "commandName": "ping", - "reply": { - "$$type": "string" - }, - "requestId": { - "$$type": [ - "int", - "long" - ] - }, - "serverHost": { - "$$type": "string" - }, - "serverPort": { - "$$type": [ - "int", - "long" - ] - }, - "durationMS": { - "$$type": [ - "double", - "int", - "long" - ] - } - } - } - ] - } - ] - }, - { - "description": "A failed command", - "operations": [ - { - "name": "find", - "object": "collection", - "arguments": { - "filter": { - "$or": true - } - }, - "expectError": { - "isClientError": false - } - } - ], - "expectLogMessages": [ - { - "client": "client", - "messages": [ - { - "level": "debug", - "component": "command", - "data": { - "message": "Command started", - "databaseName": "logging-tests", - "commandName": "find", - "command": { - "$$type": "string" - }, - "requestId": { - "$$type": [ - "int", - "long" - ] - }, - "serverHost": { - "$$type": "string" - }, - "serverPort": { - "$$type": [ - "int", - "long" - ] - } + "databaseName": "command-monitoring-tests" } }, { - "level": "debug", - "component": "command", - "data": { - "message": "Command failed", - "databaseName": "logging-tests", - "commandName": "find", - "failure": { - "$$exists": true - }, - "requestId": { - "$$type": [ - "int", - "long" - ] - }, - "serverHost": { - "$$type": "string" - }, - "serverPort": { - "$$type": [ - "int", - "long" - ] + "commandSucceededEvent": { + "reply": { + "ok": 1 }, - "durationMS": { - "$$type": [ - "double", - "int", - "long" - ] - } + "commandName": "ping" } } ] diff --git a/driver-core/src/test/resources/unified-test-format/command-monitoring/unacknowledgedBulkWrite.json b/driver-core/src/test/resources/unified-test-format/command-monitoring/unacknowledgedBulkWrite.json index ed6ceafa5fd..78ddde767ff 100644 --- a/driver-core/src/test/resources/unified-test-format/command-monitoring/unacknowledgedBulkWrite.json +++ b/driver-core/src/test/resources/unified-test-format/command-monitoring/unacknowledgedBulkWrite.json @@ -71,17 +71,7 @@ "object": "collection", "arguments": { "filter": {} - }, - "expectResult": [ - { - "_id": 1, - "x": 11 - }, - { - "_id": "unorderedBulkWriteInsertW0", - "x": 44 - } - ] + } } ], "expectEvents": [ diff --git a/driver-core/src/test/resources/unified-test-format/command-monitoring/writeConcernError.json b/driver-core/src/test/resources/unified-test-format/command-monitoring/writeConcernError.json index cc97450687d..455e5422b72 100644 --- a/driver-core/src/test/resources/unified-test-format/command-monitoring/writeConcernError.json +++ b/driver-core/src/test/resources/unified-test-format/command-monitoring/writeConcernError.json @@ -1,9 +1,9 @@ { "description": "writeConcernError", - "schemaVersion": "1.13", + "schemaVersion": "1.4", "runOnRequirements": [ { - "minServerVersion": "4.1.0", + "minServerVersion": "4.3.1", "topologies": [ "replicaset" ], @@ -66,11 +66,11 @@ "failCommands": [ "insert" ], + "errorLabels": [ + "RetryableWriteError" + ], "writeConcernError": { - "code": 91, - "errorLabels": [ - "RetryableWriteError" - ] + "code": 91 } } } @@ -112,11 +112,11 @@ "reply": { "ok": 1, "n": 1, + "errorLabels": [ + "RetryableWriteError" + ], "writeConcernError": { - "code": 91, - "errorLabels": [ - "RetryableWriteError" - ] + "code": 91 } }, "commandName": "insert" diff --git a/driver-core/src/test/resources/unified-test-format/crud/aggregate-write-readPreference.json b/driver-core/src/test/resources/unified-test-format/crud/aggregate-write-readPreference.json index bc887e83cbc..c1fa3b4574a 100644 --- a/driver-core/src/test/resources/unified-test-format/crud/aggregate-write-readPreference.json +++ b/driver-core/src/test/resources/unified-test-format/crud/aggregate-write-readPreference.json @@ -78,11 +78,6 @@ "x": 33 } ] - }, - { - "collectionName": "coll1", - "databaseName": "db0", - "documents": [] } ], "tests": [ @@ -159,22 +154,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll1", - "databaseName": "db0", - "documents": [ - { - "_id": 2, - "x": 22 - }, - { - "_id": 3, - "x": 33 - } - ] - } ] }, { @@ -250,22 +229,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll1", - "databaseName": "db0", - "documents": [ - { - "_id": 2, - "x": 22 - }, - { - "_id": 3, - "x": 33 - } - ] - } ] }, { @@ -344,22 +307,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll1", - "databaseName": "db0", - "documents": [ - { - "_id": 2, - "x": 22 - }, - { - "_id": 3, - "x": 33 - } - ] - } ] }, { @@ -438,22 +385,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll1", - "databaseName": "db0", - "documents": [ - { - "_id": 2, - "x": 22 - }, - { - "_id": 3, - "x": 33 - } - ] - } ] } ] diff --git a/driver-core/src/test/resources/unified-test-format/crud/bulkWrite-replaceOne-sort.json b/driver-core/src/test/resources/unified-test-format/crud/bulkWrite-replaceOne-sort.json new file mode 100644 index 00000000000..c0bd3835142 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/bulkWrite-replaceOne-sort.json @@ -0,0 +1,239 @@ +{ + "description": "BulkWrite replaceOne-sort", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "BulkWrite replaceOne with sort option", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "operations": [ + { + "object": "collection0", + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "replaceOne": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "replacement": { + "x": 1 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 1 + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "n": 1 + }, + "commandName": "update" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 1 + } + ] + } + ] + }, + { + "description": "BulkWrite replaceOne with sort option unsupported (server-side error)", + "runOnRequirements": [ + { + "maxServerVersion": "7.99" + } + ], + "operations": [ + { + "object": "collection0", + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "replaceOne": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "replacement": { + "x": 1 + } + } + } + ] + }, + "expectError": { + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 1 + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/crud/bulkWrite-updateOne-sort.json b/driver-core/src/test/resources/unified-test-format/crud/bulkWrite-updateOne-sort.json new file mode 100644 index 00000000000..f78bd3bf3e3 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/bulkWrite-updateOne-sort.json @@ -0,0 +1,255 @@ +{ + "description": "BulkWrite updateOne-sort", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "BulkWrite updateOne with sort option", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "operations": [ + { + "object": "collection0", + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "updateOne": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": [ + { + "$set": { + "x": 1 + } + } + ] + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": [ + { + "$set": { + "x": 1 + } + } + ], + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "n": 1 + }, + "commandName": "update" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 1 + } + ] + } + ] + }, + { + "description": "BulkWrite updateOne with sort option unsupported (server-side error)", + "runOnRequirements": [ + { + "maxServerVersion": "7.99" + } + ], + "operations": [ + { + "object": "collection0", + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "updateOne": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": [ + { + "$set": { + "x": 1 + } + } + ] + } + } + ] + }, + "expectError": { + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": [ + { + "$set": { + "x": 1 + } + } + ], + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/crud/db-aggregate-write-readPreference.json b/driver-core/src/test/resources/unified-test-format/crud/db-aggregate-write-readPreference.json index 2a81282de81..b6460f001f2 100644 --- a/driver-core/src/test/resources/unified-test-format/crud/db-aggregate-write-readPreference.json +++ b/driver-core/src/test/resources/unified-test-format/crud/db-aggregate-write-readPreference.json @@ -52,13 +52,6 @@ } } ], - "initialData": [ - { - "collectionName": "coll0", - "databaseName": "db0", - "documents": [] - } - ], "tests": [ { "description": "Database-level aggregate with $out includes read preference for 5.0+ server", @@ -141,17 +134,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll0", - "databaseName": "db0", - "documents": [ - { - "_id": 1 - } - ] - } ] }, { @@ -235,17 +217,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll0", - "databaseName": "db0", - "documents": [ - { - "_id": 1 - } - ] - } ] }, { @@ -332,17 +303,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll0", - "databaseName": "db0", - "documents": [ - { - "_id": 1 - } - ] - } ] }, { @@ -429,17 +389,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll0", - "databaseName": "db0", - "documents": [ - { - "_id": 1 - } - ] - } ] } ] diff --git a/driver-core/src/test/resources/unified-test-format/crud/replaceOne-sort.json b/driver-core/src/test/resources/unified-test-format/crud/replaceOne-sort.json new file mode 100644 index 00000000000..cf2271dda57 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/replaceOne-sort.json @@ -0,0 +1,232 @@ +{ + "description": "replaceOne-sort", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "ReplaceOne with sort option", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "operations": [ + { + "name": "replaceOne", + "object": "collection0", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "replacement": { + "x": 1 + } + }, + "expectResult": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 1 + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "n": 1 + }, + "commandName": "update" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 1 + } + ] + } + ] + }, + { + "description": "replaceOne with sort option unsupported (server-side error)", + "runOnRequirements": [ + { + "maxServerVersion": "7.99" + } + ], + "operations": [ + { + "name": "replaceOne", + "object": "collection0", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "replacement": { + "x": 1 + } + }, + "expectError": { + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 1 + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/crud/updateOne-sort.json b/driver-core/src/test/resources/unified-test-format/crud/updateOne-sort.json new file mode 100644 index 00000000000..8fe4f50b94f --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/updateOne-sort.json @@ -0,0 +1,240 @@ +{ + "description": "updateOne-sort", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "UpdateOne with sort option", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "operations": [ + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": { + "$inc": { + "x": 1 + } + } + }, + "expectResult": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "n": 1 + }, + "commandName": "update" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 34 + } + ] + } + ] + }, + { + "description": "updateOne with sort option unsupported (server-side error)", + "runOnRequirements": [ + { + "maxServerVersion": "7.99" + } + ], + "operations": [ + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": { + "$inc": { + "x": 1 + } + } + }, + "expectError": { + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/index-management/createSearchIndex.json b/driver-core/src/test/resources/unified-test-format/index-management/createSearchIndex.json index 327cb612593..f4f2a6c6612 100644 --- a/driver-core/src/test/resources/unified-test-format/index-management/createSearchIndex.json +++ b/driver-core/src/test/resources/unified-test-format/index-management/createSearchIndex.json @@ -28,7 +28,17 @@ ], "runOnRequirements": [ { - "minServerVersion": "7.0.0", + "minServerVersion": "7.0.5", + "maxServerVersion": "7.0.99", + "topologies": [ + "replicaset", + "load-balanced", + "sharded" + ], + "serverless": "forbid" + }, + { + "minServerVersion": "7.2.0", "topologies": [ "replicaset", "load-balanced", diff --git a/driver-core/src/test/resources/unified-test-format/index-management/createSearchIndexes.json b/driver-core/src/test/resources/unified-test-format/index-management/createSearchIndexes.json index d91d7d9cf3c..01300b1b7f4 100644 --- a/driver-core/src/test/resources/unified-test-format/index-management/createSearchIndexes.json +++ b/driver-core/src/test/resources/unified-test-format/index-management/createSearchIndexes.json @@ -28,7 +28,17 @@ ], "runOnRequirements": [ { - "minServerVersion": "7.0.0", + "minServerVersion": "7.0.5", + "maxServerVersion": "7.0.99", + "topologies": [ + "replicaset", + "load-balanced", + "sharded" + ], + "serverless": "forbid" + }, + { + "minServerVersion": "7.2.0", "topologies": [ "replicaset", "load-balanced", diff --git a/driver-core/src/test/resources/unified-test-format/load-balancers/README.md b/driver-core/src/test/resources/unified-test-format/load-balancers/README.md index e69de29bb2d..45f185caa61 100644 --- a/driver-core/src/test/resources/unified-test-format/load-balancers/README.md +++ b/driver-core/src/test/resources/unified-test-format/load-balancers/README.md @@ -0,0 +1,49 @@ +# Load Balancer Support Tests + +______________________________________________________________________ + +## Introduction + +This document describes how drivers should create load balanced clusters for testing and how tests should be executed +for such clusters. + +## Testing Requirements + +For each server version that supports load balanced clusters, drivers MUST add two Evergreen tasks: one with a sharded +cluster with both authentication and TLS enabled and one with a sharded cluster with authentication and TLS disabled. In +each task, the sharded cluster MUST be configured with two mongos nodes running on localhost ports 27017 and 27018. The +shard and config servers may run on any free ports. Each task MUST also start up two TCP load balancers operating in +round-robin mode: one fronting both mongos servers and one fronting a single mongos. + +### Load Balancer Configuration + +Drivers MUST use the `run-load-balancer.sh` script in `drivers-evergreen-tools` to start the TCP load balancers for +Evergreen tasks. This script MUST be run after the backing sharded cluster has already been started. The script writes +the URIs of the load balancers to a YAML expansions file, which can be read by drivers via the `expansions.update` +Evergreen command. This will store the URIs into the `SINGLE_MONGOS_LB_URI` and `MULTI_MONGOS_LB_URI` environment +variables. + +### Test Runner Configuration + +If the backing sharded cluster is configured with TLS enabled, drivers MUST add the relevant TLS options to both +`SINGLE_MONGOS_LB_URI` and `MULTI_MONGOS_LB_URI` to ensure that test clients can connect to the cluster. Drivers MUST +use the final URI stored in `SINGLE_MONGOS_LB_URI` (with additional TLS options if required) to configure internal +clients for test runners (e.g. the internal MongoClient described by the +[Unified Test Format spec](../../unified-test-format/unified-test-format.md)). + +In addition to modifying load balancer URIs, drivers MUST also mock server support for returning a `serviceId` field in +`hello` or legacy `hello` command responses when running tests against a load-balanced cluster. This can be done by +using the value of `topologyVersion.processId` to set `serviceId`. This MUST be done for all connections established by +the test runner, including those made by any internal clients. + +## Tests + +The YAML and JSON files in this directory contain platform-independent tests written in the +[Unified Test Format](../../unified-test-format/unified-test-format.md). Drivers MUST run the following test suites +against a load balanced cluster: + +1. All test suites written in the Unified Test Format +2. Retryable Reads +3. Retryable Writes +4. Change Streams +5. Initial DNS Seedlist Discovery diff --git a/driver-core/src/test/resources/unified-test-format/load-balancers/cursors.json b/driver-core/src/test/resources/unified-test-format/load-balancers/cursors.json index 7da08c94d6c..b11bf2c6fae 100644 --- a/driver-core/src/test/resources/unified-test-format/load-balancers/cursors.json +++ b/driver-core/src/test/resources/unified-test-format/load-balancers/cursors.json @@ -1,6 +1,6 @@ { "description": "cursors are correctly pinned to connections for load-balanced clusters", - "schemaVersion": "1.3", + "schemaVersion": "1.4", "runOnRequirements": [ { "topologies": [ @@ -222,7 +222,10 @@ "reply": { "cursor": { "id": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "firstBatch": { "$$type": "array" @@ -239,7 +242,10 @@ "commandStartedEvent": { "command": { "getMore": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "collection": "coll0" }, @@ -333,7 +339,10 @@ "reply": { "cursor": { "id": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "firstBatch": { "$$type": "array" @@ -475,7 +484,10 @@ "reply": { "cursor": { "id": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "firstBatch": { "$$type": "array" @@ -492,7 +504,10 @@ "commandStartedEvent": { "command": { "getMore": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "collection": "coll0" }, @@ -605,7 +620,10 @@ "reply": { "cursor": { "id": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "firstBatch": { "$$type": "array" @@ -750,7 +768,10 @@ "reply": { "cursor": { "id": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "firstBatch": { "$$type": "array" @@ -767,7 +788,10 @@ "commandStartedEvent": { "command": { "getMore": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "collection": "coll0" }, @@ -858,7 +882,10 @@ "commandStartedEvent": { "command": { "getMore": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "collection": "coll0" }, @@ -950,7 +977,10 @@ "commandStartedEvent": { "command": { "getMore": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "collection": { "$$type": "string" @@ -996,11 +1026,6 @@ }, { "description": "listIndexes pins the cursor to a connection", - "runOnRequirements": [ - { - "serverless": "forbid" - } - ], "operations": [ { "name": "createIndex", @@ -1105,7 +1130,10 @@ "commandStartedEvent": { "command": { "getMore": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "collection": "coll0" }, diff --git a/driver-core/src/test/resources/unified-test-format/load-balancers/non-lb-connection-establishment.json b/driver-core/src/test/resources/unified-test-format/load-balancers/non-lb-connection-establishment.json index d2d26856d97..6aaa7bdf98b 100644 --- a/driver-core/src/test/resources/unified-test-format/load-balancers/non-lb-connection-establishment.json +++ b/driver-core/src/test/resources/unified-test-format/load-balancers/non-lb-connection-establishment.json @@ -3,7 +3,6 @@ "schemaVersion": "1.3", "runOnRequirements": [ { - "minServerVersion": "3.6", "topologies": [ "single", "sharded" diff --git a/driver-core/src/test/resources/unified-test-format/load-balancers/sdam-error-handling.json b/driver-core/src/test/resources/unified-test-format/load-balancers/sdam-error-handling.json index 4ab34b1fed4..47323fae4f3 100644 --- a/driver-core/src/test/resources/unified-test-format/load-balancers/sdam-error-handling.json +++ b/driver-core/src/test/resources/unified-test-format/load-balancers/sdam-error-handling.json @@ -1,6 +1,6 @@ { "description": "state change errors are correctly handled", - "schemaVersion": "1.3", + "schemaVersion": "1.4", "runOnRequirements": [ { "topologies": [ @@ -263,7 +263,7 @@ "description": "errors during the initial connection hello are ignored", "runOnRequirements": [ { - "minServerVersion": "4.9" + "minServerVersion": "4.4.7" } ], "operations": [ diff --git a/driver-core/src/test/resources/unified-test-format/load-balancers/transactions.json b/driver-core/src/test/resources/unified-test-format/load-balancers/transactions.json index 8cf24f4ca4f..0dd04ee8540 100644 --- a/driver-core/src/test/resources/unified-test-format/load-balancers/transactions.json +++ b/driver-core/src/test/resources/unified-test-format/load-balancers/transactions.json @@ -1,6 +1,6 @@ { "description": "transactions are correctly pinned to connections for load-balanced clusters", - "schemaVersion": "1.3", + "schemaVersion": "1.4", "runOnRequirements": [ { "topologies": [ diff --git a/driver-core/src/test/resources/unified-test-format/retryable-reads/readConcernMajorityNotAvailableYet.json b/driver-core/src/test/resources/unified-test-format/retryable-reads/readConcernMajorityNotAvailableYet.json new file mode 100644 index 00000000000..8aa6a6b5e5e --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/retryable-reads/readConcernMajorityNotAvailableYet.json @@ -0,0 +1,147 @@ +{ + "description": "ReadConcernMajorityNotAvailableYet is a retryable read", + "schemaVersion": "1.3", + "runOnRequirements": [ + { + "minServerVersion": "4.0", + "topologies": [ + "single", + "replicaset" + ] + }, + { + "minServerVersion": "4.1.7", + "topologies": [ + "sharded", + "load-balanced" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "retryable-reads-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "readconcernmajoritynotavailableyet_test" + } + } + ], + "initialData": [ + { + "collectionName": "readconcernmajoritynotavailableyet_test", + "databaseName": "retryable-reads-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "Find succeeds on second attempt after ReadConcernMajorityNotAvailableYet", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "find" + ], + "errorCode": 134 + } + } + } + }, + { + "name": "find", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + } + }, + "object": "collection0", + "expectResult": [ + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "readconcernmajoritynotavailableyet_test", + "filter": { + "_id": { + "$gt": 1 + } + } + }, + "commandName": "find", + "databaseName": "retryable-reads-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "readconcernmajoritynotavailableyet_test", + "filter": { + "_id": { + "$gt": 1 + } + } + }, + "commandName": "find", + "databaseName": "retryable-reads-tests" + } + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/retryable-writes/client-bulkWrite-clientErrors.json b/driver-core/src/test/resources/unified-test-format/retryable-writes/client-bulkWrite-clientErrors.json new file mode 100644 index 00000000000..d16e0c9c8d6 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/retryable-writes/client-bulkWrite-clientErrors.json @@ -0,0 +1,351 @@ +{ + "description": "client bulkWrite retryable writes with client errors", + "schemaVersion": "1.21", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ], + "useMultipleMongoses": false + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "retryable-writes-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "retryable-writes-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "retryable-writes-tests.coll0" + }, + "tests": [ + { + "description": "client bulkWrite with one network error succeeds after retry", + "operations": [ + { + "object": "testRunner", + "name": "failPoint", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "closeConnection": true + } + } + } + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "retryable-writes-tests.coll0", + "document": { + "_id": 4, + "x": 44 + } + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 4 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "retryable-writes-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + ] + }, + { + "description": "client bulkWrite with two network errors fails after retry", + "operations": [ + { + "object": "testRunner", + "name": "failPoint", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "closeConnection": true + } + } + } + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "retryable-writes-tests.coll0", + "document": { + "_id": 4, + "x": 44 + } + } + } + ], + "verboseResults": true + }, + "expectError": { + "isClientError": true, + "errorLabelsContain": [ + "RetryableWriteError" + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "retryable-writes-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/retryable-writes/client-bulkWrite-serverErrors.json b/driver-core/src/test/resources/unified-test-format/retryable-writes/client-bulkWrite-serverErrors.json new file mode 100644 index 00000000000..f58c82bcc73 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/retryable-writes/client-bulkWrite-serverErrors.json @@ -0,0 +1,873 @@ +{ + "description": "client bulkWrite retryable writes", + "schemaVersion": "1.21", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ], + "useMultipleMongoses": false + } + }, + { + "client": { + "id": "clientRetryWritesFalse", + "uriOptions": { + "retryWrites": false + }, + "observeEvents": [ + "commandStartedEvent" + ], + "useMultipleMongoses": false + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "retryable-writes-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "retryable-writes-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "retryable-writes-tests.coll0" + }, + "tests": [ + { + "description": "client bulkWrite with no multi: true operations succeeds after retryable top-level error", + "operations": [ + { + "object": "testRunner", + "name": "failPoint", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "errorCode": 189, + "errorLabels": [ + "RetryableWriteError" + ] + } + } + } + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "retryable-writes-tests.coll0", + "document": { + "_id": 4, + "x": 44 + } + } + }, + { + "updateOne": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "replaceOne": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 2 + }, + "replacement": { + "x": 222 + } + } + }, + { + "deleteOne": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 3 + } + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 2, + "modifiedCount": 2, + "deletedCount": 1, + "insertResults": { + "0": { + "insertedId": 4 + } + }, + "updateResults": { + "1": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + }, + "2": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + } + }, + "deleteResults": { + "3": { + "deletedCount": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + }, + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": false + }, + { + "update": 0, + "filter": { + "_id": 2 + }, + "updateMods": { + "x": 222 + }, + "multi": false + }, + { + "delete": 0, + "filter": { + "_id": 3 + }, + "multi": false + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + }, + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": false + }, + { + "update": 0, + "filter": { + "_id": 2 + }, + "updateMods": { + "x": 222 + }, + "multi": false + }, + { + "delete": 0, + "filter": { + "_id": 3 + }, + "multi": false + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "retryable-writes-tests", + "documents": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 222 + }, + { + "_id": 4, + "x": 44 + } + ] + } + ] + }, + { + "description": "client bulkWrite with multi: true operations fails after retryable top-level error", + "operations": [ + { + "object": "testRunner", + "name": "failPoint", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "errorCode": 189, + "errorLabels": [ + "RetryableWriteError" + ] + } + } + } + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "updateMany": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "deleteMany": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 3 + } + } + } + ] + }, + "expectError": { + "errorCode": 189, + "errorLabelsContain": [ + "RetryableWriteError" + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": true, + "ordered": true, + "ops": [ + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": true + }, + { + "delete": 0, + "filter": { + "_id": 3 + }, + "multi": true + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ] + } + } + } + ] + } + ] + }, + { + "description": "client bulkWrite with no multi: true operations succeeds after retryable writeConcernError", + "operations": [ + { + "object": "testRunner", + "name": "failPoint", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "errorLabels": [ + "RetryableWriteError" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + } + } + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "retryable-writes-tests.coll0", + "document": { + "_id": 4, + "x": 44 + } + } + }, + { + "updateOne": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "replaceOne": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 2 + }, + "replacement": { + "x": 222 + } + } + }, + { + "deleteOne": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 3 + } + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 2, + "modifiedCount": 2, + "deletedCount": 1, + "insertResults": { + "0": { + "insertedId": 4 + } + }, + "updateResults": { + "1": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + }, + "2": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + } + }, + "deleteResults": { + "3": { + "deletedCount": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + }, + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": false + }, + { + "update": 0, + "filter": { + "_id": 2 + }, + "updateMods": { + "x": 222 + }, + "multi": false + }, + { + "delete": 0, + "filter": { + "_id": 3 + }, + "multi": false + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + }, + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": false + }, + { + "update": 0, + "filter": { + "_id": 2 + }, + "updateMods": { + "x": 222 + }, + "multi": false + }, + { + "delete": 0, + "filter": { + "_id": 3 + }, + "multi": false + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + } + ] + } + ] + }, + { + "description": "client bulkWrite with multi: true operations fails after retryable writeConcernError", + "operations": [ + { + "object": "testRunner", + "name": "failPoint", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "errorLabels": [ + "RetryableWriteError" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + } + } + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "updateMany": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "deleteMany": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 3 + } + } + } + ] + }, + "expectError": { + "writeConcernErrors": [ + { + "code": 91, + "message": "Replication is being shut down" + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": true, + "ordered": true, + "ops": [ + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": true + }, + { + "delete": 0, + "filter": { + "_id": 3 + }, + "multi": true + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ] + } + } + } + ] + } + ] + }, + { + "description": "client bulkWrite with retryWrites: false does not retry", + "operations": [ + { + "object": "testRunner", + "name": "failPoint", + "arguments": { + "client": "clientRetryWritesFalse", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "errorCode": 189, + "errorLabels": [ + "RetryableWriteError" + ] + } + } + } + }, + { + "object": "clientRetryWritesFalse", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "retryable-writes-tests.coll0", + "document": { + "_id": 4, + "x": 44 + } + } + } + ] + }, + "expectError": { + "errorCode": 189, + "errorLabelsContain": [ + "RetryableWriteError" + ] + } + } + ], + "expectEvents": [ + { + "client": "clientRetryWritesFalse", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": true, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ] + } + } + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/retryable-writes/handshakeError.json b/driver-core/src/test/resources/unified-test-format/retryable-writes/handshakeError.json index df37bd72322..93cb2e849ec 100644 --- a/driver-core/src/test/resources/unified-test-format/retryable-writes/handshakeError.json +++ b/driver-core/src/test/resources/unified-test-format/retryable-writes/handshakeError.json @@ -1,6 +1,6 @@ { "description": "retryable writes handshake failures", - "schemaVersion": "1.3", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.2", @@ -53,6 +53,224 @@ } ], "tests": [ + { + "description": "client.clientBulkWrite succeeds after retryable handshake network error", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "ping", + "saslContinue" + ], + "closeConnection": true + } + } + } + }, + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectError": { + "isError": true + } + }, + { + "name": "clientBulkWrite", + "object": "client", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "retryable-writes-handshake-tests.coll", + "document": { + "_id": 8, + "x": 88 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "cmap", + "events": [ + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + } + ] + }, + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "ping": 1 + }, + "databaseName": "retryable-writes-handshake-tests" + } + }, + { + "commandFailedEvent": { + "commandName": "ping" + } + }, + { + "commandStartedEvent": { + "commandName": "bulkWrite" + } + }, + { + "commandSucceededEvent": { + "commandName": "bulkWrite" + } + } + ] + } + ] + }, + { + "description": "client.clientBulkWrite succeeds after retryable handshake server error (ShutdownInProgress)", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "ping", + "saslContinue" + ], + "closeConnection": true + } + } + } + }, + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectError": { + "isError": true + } + }, + { + "name": "clientBulkWrite", + "object": "client", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "retryable-writes-handshake-tests.coll", + "document": { + "_id": 8, + "x": 88 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "cmap", + "events": [ + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + } + ] + }, + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "ping": 1 + }, + "databaseName": "retryable-writes-handshake-tests" + } + }, + { + "commandFailedEvent": { + "commandName": "ping" + } + }, + { + "commandStartedEvent": { + "commandName": "bulkWrite" + } + }, + { + "commandSucceededEvent": { + "commandName": "bulkWrite" + } + } + ] + } + ] + }, { "description": "collection.insertOne succeeds after retryable handshake network error", "operations": [ diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-error.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-error.json index 5c78ecfe503..62d26494c7c 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-error.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-error.json @@ -1,6 +1,6 @@ { "description": "auth-error", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.4", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-misc-command-error.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-misc-command-error.json index 6e1b645461e..fd62fe604e9 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-misc-command-error.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-misc-command-error.json @@ -1,6 +1,6 @@ { "description": "auth-misc-command-error", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.4", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-network-error.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-network-error.json index 7606d2db7ab..84763af32e4 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-network-error.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-network-error.json @@ -1,6 +1,6 @@ { "description": "auth-network-error", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.4", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-network-timeout-error.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-network-timeout-error.json index 22066e8baeb..3cf9576eba9 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-network-timeout-error.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-network-timeout-error.json @@ -1,6 +1,6 @@ { "description": "auth-network-timeout-error", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.4", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-shutdown-error.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-shutdown-error.json index 5dd7b5bb6fe..b9e503af66e 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-shutdown-error.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/auth-shutdown-error.json @@ -1,6 +1,6 @@ { "description": "auth-shutdown-error", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.4", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/cancel-server-check.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/cancel-server-check.json index 896cc8d0871..a60ccfcb414 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/cancel-server-check.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/cancel-server-check.json @@ -1,6 +1,6 @@ { "description": "cancel-server-check", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.0", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/connectTimeoutMS.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/connectTimeoutMS.json index 67a4d9da1d3..d3e860a9cb2 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/connectTimeoutMS.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/connectTimeoutMS.json @@ -1,6 +1,6 @@ { "description": "connectTimeoutMS", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.4", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/find-network-error.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/find-network-error.json index 651466bfa6d..c1b6db40ca3 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/find-network-error.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/find-network-error.json @@ -1,6 +1,6 @@ { "description": "find-network-error", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.4", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/find-network-timeout-error.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/find-network-timeout-error.json index 2bde6daa5df..e5ac9f21aa7 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/find-network-timeout-error.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/find-network-timeout-error.json @@ -1,6 +1,6 @@ { "description": "find-network-timeout-error", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.4", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/find-shutdown-error.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/find-shutdown-error.json index 624ad352fc9..6e5a2cac055 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/find-shutdown-error.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/find-shutdown-error.json @@ -1,6 +1,6 @@ { "description": "find-shutdown-error", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.4", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/hello-command-error.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/hello-command-error.json index 7d6046b76f5..87958cb2c0b 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/hello-command-error.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/hello-command-error.json @@ -1,9 +1,9 @@ { "description": "hello-command-error", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { - "minServerVersion": "4.9", + "minServerVersion": "4.4.7", "serverless": "forbid", "topologies": [ "single", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/hello-network-error.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/hello-network-error.json index f44b26a9f91..15ed2b605e2 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/hello-network-error.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/hello-network-error.json @@ -1,9 +1,9 @@ { "description": "hello-network-error", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { - "minServerVersion": "4.9", + "minServerVersion": "4.4.7", "serverless": "forbid", "topologies": [ "single", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/hello-timeout.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/hello-timeout.json index dfa6b48d66b..fe7cf4e78d1 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/hello-timeout.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/hello-timeout.json @@ -1,6 +1,6 @@ { "description": "hello-timeout", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.4", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/insert-network-error.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/insert-network-error.json index e4ba6684ae2..bfe41a4cb66 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/insert-network-error.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/insert-network-error.json @@ -1,6 +1,6 @@ { "description": "insert-network-error", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.4", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/insert-shutdown-error.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/insert-shutdown-error.json index 3c724fa5e4c..af7c6c987af 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/insert-shutdown-error.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/insert-shutdown-error.json @@ -1,6 +1,6 @@ { "description": "insert-shutdown-error", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.4", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/interruptInUse-pool-clear.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/interruptInUse-pool-clear.json new file mode 100644 index 00000000000..d9329646d4c --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/interruptInUse-pool-clear.json @@ -0,0 +1,591 @@ +{ + "description": "interruptInUse", + "schemaVersion": "1.11", + "runOnRequirements": [ + { + "minServerVersion": "4.4", + "serverless": "forbid", + "topologies": [ + "replicaset", + "sharded" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "setupClient", + "useMultipleMongoses": false + } + } + ], + "initialData": [ + { + "collectionName": "interruptInUse", + "databaseName": "sdam-tests", + "documents": [] + } + ], + "tests": [ + { + "description": "Connection pool clear uses interruptInUseConnections=true after monitor timeout", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "useMultipleMongoses": false, + "observeEvents": [ + "poolClearedEvent", + "connectionClosedEvent", + "commandStartedEvent", + "commandSucceededEvent", + "commandFailedEvent", + "connectionCheckedOutEvent", + "connectionCheckedInEvent" + ], + "uriOptions": { + "connectTimeoutMS": 500, + "heartbeatFrequencyMS": 500, + "appname": "interruptInUse", + "retryReads": false, + "minPoolSize": 0 + } + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "sdam-tests" + } + }, + { + "collection": { + "id": "collection", + "database": "database", + "collectionName": "interruptInUse" + } + }, + { + "thread": { + "id": "thread1" + } + } + ] + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "document": { + "_id": 1 + } + } + }, + { + "name": "runOnThread", + "object": "testRunner", + "arguments": { + "thread": "thread1", + "operation": { + "name": "find", + "object": "collection", + "arguments": { + "filter": { + "$where": "sleep(2000) || true" + } + }, + "expectError": { + "isError": true + } + } + } + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "setupClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 4 + }, + "data": { + "failCommands": [ + "hello", + "isMaster" + ], + "blockConnection": true, + "blockTimeMS": 1500, + "appName": "interruptInUse" + } + } + } + }, + { + "name": "waitForThread", + "object": "testRunner", + "arguments": { + "thread": "thread1" + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "command", + "events": [ + { + "commandStartedEvent": { + "commandName": "insert" + } + }, + { + "commandSucceededEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "commandName": "find" + } + }, + { + "commandFailedEvent": { + "commandName": "find" + } + } + ] + }, + { + "client": "client", + "eventType": "cmap", + "events": [ + { + "connectionCheckedOutEvent": {} + }, + { + "connectionCheckedInEvent": {} + }, + { + "connectionCheckedOutEvent": {} + }, + { + "poolClearedEvent": { + "interruptInUseConnections": true + } + }, + { + "connectionCheckedInEvent": {} + }, + { + "connectionClosedEvent": {} + } + ] + } + ], + "outcome": [ + { + "collectionName": "interruptInUse", + "databaseName": "sdam-tests", + "documents": [ + { + "_id": 1 + } + ] + } + ] + }, + { + "description": "Error returned from connection pool clear with interruptInUseConnections=true is retryable", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "useMultipleMongoses": false, + "observeEvents": [ + "poolClearedEvent", + "connectionClosedEvent", + "commandStartedEvent", + "commandFailedEvent", + "commandSucceededEvent", + "connectionCheckedOutEvent", + "connectionCheckedInEvent" + ], + "uriOptions": { + "connectTimeoutMS": 500, + "heartbeatFrequencyMS": 500, + "appname": "interruptInUseRetryable", + "retryReads": true, + "minPoolSize": 0 + } + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "sdam-tests" + } + }, + { + "collection": { + "id": "collection", + "database": "database", + "collectionName": "interruptInUse" + } + }, + { + "thread": { + "id": "thread1" + } + } + ] + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "document": { + "_id": 1 + } + } + }, + { + "name": "runOnThread", + "object": "testRunner", + "arguments": { + "thread": "thread1", + "operation": { + "name": "find", + "object": "collection", + "arguments": { + "filter": { + "$where": "sleep(2000) || true" + } + } + } + } + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "setupClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 4 + }, + "data": { + "failCommands": [ + "hello", + "isMaster" + ], + "blockConnection": true, + "blockTimeMS": 1500, + "appName": "interruptInUseRetryable" + } + } + } + }, + { + "name": "waitForThread", + "object": "testRunner", + "arguments": { + "thread": "thread1" + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "command", + "events": [ + { + "commandStartedEvent": { + "commandName": "insert" + } + }, + { + "commandSucceededEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "commandName": "find" + } + }, + { + "commandFailedEvent": { + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "commandName": "find" + } + }, + { + "commandSucceededEvent": { + "commandName": "find" + } + } + ] + }, + { + "client": "client", + "eventType": "cmap", + "events": [ + { + "connectionCheckedOutEvent": {} + }, + { + "connectionCheckedInEvent": {} + }, + { + "connectionCheckedOutEvent": {} + }, + { + "poolClearedEvent": { + "interruptInUseConnections": true + } + }, + { + "connectionCheckedInEvent": {} + }, + { + "connectionClosedEvent": {} + }, + { + "connectionCheckedOutEvent": {} + }, + { + "connectionCheckedInEvent": {} + } + ] + } + ], + "outcome": [ + { + "collectionName": "interruptInUse", + "databaseName": "sdam-tests", + "documents": [ + { + "_id": 1 + } + ] + } + ] + }, + { + "description": "Error returned from connection pool clear with interruptInUseConnections=true is retryable for write", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "useMultipleMongoses": false, + "observeEvents": [ + "poolClearedEvent", + "connectionClosedEvent", + "commandStartedEvent", + "commandFailedEvent", + "commandSucceededEvent", + "connectionCheckedOutEvent", + "connectionCheckedInEvent" + ], + "uriOptions": { + "connectTimeoutMS": 500, + "heartbeatFrequencyMS": 500, + "appname": "interruptInUseRetryableWrite", + "retryWrites": true, + "minPoolSize": 0 + } + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "sdam-tests" + } + }, + { + "collection": { + "id": "collection", + "database": "database", + "collectionName": "interruptInUse" + } + }, + { + "thread": { + "id": "thread1" + } + } + ] + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "document": { + "_id": 1 + } + } + }, + { + "name": "runOnThread", + "object": "testRunner", + "arguments": { + "thread": "thread1", + "operation": { + "name": "updateOne", + "object": "collection", + "arguments": { + "filter": { + "$where": "sleep(2000) || true" + }, + "update": { + "$set": { + "a": "bar" + } + } + } + } + } + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "setupClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 4 + }, + "data": { + "failCommands": [ + "hello", + "isMaster" + ], + "blockConnection": true, + "blockTimeMS": 1500, + "appName": "interruptInUseRetryableWrite" + } + } + } + }, + { + "name": "waitForThread", + "object": "testRunner", + "arguments": { + "thread": "thread1" + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "command", + "events": [ + { + "commandStartedEvent": { + "commandName": "insert" + } + }, + { + "commandSucceededEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "commandName": "update" + } + }, + { + "commandFailedEvent": { + "commandName": "update" + } + }, + { + "commandStartedEvent": { + "commandName": "update" + } + }, + { + "commandSucceededEvent": { + "commandName": "update" + } + } + ] + }, + { + "client": "client", + "eventType": "cmap", + "events": [ + { + "connectionCheckedOutEvent": {} + }, + { + "connectionCheckedInEvent": {} + }, + { + "connectionCheckedOutEvent": {} + }, + { + "poolClearedEvent": { + "interruptInUseConnections": true + } + }, + { + "connectionCheckedInEvent": {} + }, + { + "connectionClosedEvent": {} + }, + { + "connectionCheckedOutEvent": {} + }, + { + "connectionCheckedInEvent": {} + } + ] + } + ], + "outcome": [ + { + "collectionName": "interruptInUse", + "databaseName": "sdam-tests", + "documents": [ + { + "_id": 1, + "a": "bar" + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/loadbalanced-emit-topology-changed-before-close.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/loadbalanced-emit-topology-changed-before-close.json new file mode 100644 index 00000000000..30c0657630b --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/loadbalanced-emit-topology-changed-before-close.json @@ -0,0 +1,88 @@ +{ + "description": "loadbalanced-emit-topology-description-changed-before-close", + "schemaVersion": "1.20", + "runOnRequirements": [ + { + "topologies": [ + "load-balanced" + ], + "minServerVersion": "4.4" + } + ], + "tests": [ + { + "description": "Topology lifecycle", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "topologyDescriptionChangedEvent", + "topologyOpeningEvent", + "topologyClosedEvent" + ] + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "topologyDescriptionChangedEvent": {} + }, + "count": 2 + } + }, + { + "name": "close", + "object": "client" + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "sdam", + "events": [ + { + "topologyOpeningEvent": {} + }, + { + "topologyDescriptionChangedEvent": { + "previousDescription": { + "type": "Unknown" + }, + "newDescription": {} + } + }, + { + "topologyDescriptionChangedEvent": { + "newDescription": { + "type": "LoadBalanced" + } + } + }, + { + "topologyDescriptionChangedEvent": { + "newDescription": { + "type": "Unknown" + } + } + }, + { + "topologyClosedEvent": {} + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/logging-loadbalanced.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/logging-loadbalanced.json new file mode 100644 index 00000000000..0ad3b0ceaa5 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/logging-loadbalanced.json @@ -0,0 +1,166 @@ +{ + "description": "loadbalanced-logging", + "schemaVersion": "1.16", + "runOnRequirements": [ + { + "topologies": [ + "load-balanced" + ], + "minServerVersion": "4.4" + } + ], + "tests": [ + { + "description": "Topology lifecycle", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "observeLogMessages": { + "topology": "debug" + }, + "observeEvents": [ + "topologyDescriptionChangedEvent" + ] + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "topologyDescriptionChangedEvent": {} + }, + "count": 2 + } + }, + { + "name": "close", + "object": "client" + } + ], + "expectLogMessages": [ + { + "client": "client", + "messages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting topology monitoring", + "topologyId": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed", + "topologyId": { + "$$exists": true + }, + "previousDescription": { + "$$exists": true + }, + "newDescription": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting server monitoring", + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed", + "topologyId": { + "$$exists": true + }, + "previousDescription": { + "$$exists": true + }, + "newDescription": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped server monitoring", + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed", + "topologyId": { + "$$exists": true + }, + "previousDescription": { + "$$exists": true + }, + "newDescription": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped topology monitoring", + "topologyId": { + "$$exists": true + } + } + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/logging-replicaset.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/logging-replicaset.json new file mode 100644 index 00000000000..e6738225cd0 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/logging-replicaset.json @@ -0,0 +1,606 @@ +{ + "description": "replicaset-logging", + "schemaVersion": "1.16", + "runOnRequirements": [ + { + "topologies": [ + "replicaset" + ], + "minServerVersion": "4.4" + } + ], + "createEntities": [ + { + "client": { + "id": "setupClient" + } + } + ], + "tests": [ + { + "description": "Topology lifecycle", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "observeLogMessages": { + "topology": "debug" + }, + "observeEvents": [ + "topologyDescriptionChangedEvent" + ] + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "topologyDescriptionChangedEvent": {} + }, + "count": 4 + } + }, + { + "name": "close", + "object": "client" + } + ], + "expectLogMessages": [ + { + "client": "client", + "ignoreMessages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting server monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat started" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat succeeded" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat failed" + } + } + ], + "messages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting topology monitoring", + "topologyId": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed", + "topologyId": { + "$$exists": true + }, + "previousDescription": { + "$$exists": true + }, + "newDescription": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed", + "topologyId": { + "$$exists": true + }, + "previousDescription": { + "$$exists": true + }, + "newDescription": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed", + "topologyId": { + "$$exists": true + }, + "previousDescription": { + "$$exists": true + }, + "newDescription": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed", + "topologyId": { + "$$exists": true + }, + "previousDescription": { + "$$exists": true + }, + "newDescription": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped server monitoring", + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped server monitoring", + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped server monitoring", + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed", + "topologyId": { + "$$exists": true + }, + "previousDescription": { + "$$exists": true + }, + "newDescription": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped topology monitoring", + "topologyId": { + "$$exists": true + } + } + } + ] + } + ] + }, + { + "description": "Successful heartbeat", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "observeLogMessages": { + "topology": "debug" + }, + "observeEvents": [ + "serverHeartbeatSucceededEvent" + ] + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverHeartbeatSucceededEvent": {} + }, + "count": 3 + } + } + ], + "expectLogMessages": [ + { + "client": "client", + "ignoreExtraMessages": true, + "ignoreMessages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat started" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting server monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped server monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed" + } + } + ], + "messages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting topology monitoring", + "topologyId": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat succeeded", + "awaited": { + "$$exists": true + }, + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + }, + "driverConnectionId": { + "$$exists": true + }, + "serverConnectionId": { + "$$exists": true + }, + "durationMS": { + "$$type": [ + "int", + "long" + ] + }, + "reply": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "ok": 1 + } + } + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat succeeded", + "awaited": { + "$$exists": true + }, + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + }, + "driverConnectionId": { + "$$exists": true + }, + "serverConnectionId": { + "$$exists": true + }, + "durationMS": { + "$$type": [ + "int", + "long" + ] + }, + "reply": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "ok": 1 + } + } + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat succeeded", + "awaited": { + "$$exists": true + }, + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + }, + "driverConnectionId": { + "$$exists": true + }, + "serverConnectionId": { + "$$exists": true + }, + "durationMS": { + "$$type": [ + "int", + "long" + ] + }, + "reply": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "ok": 1 + } + } + } + } + } + ] + } + ] + }, + { + "description": "Failing heartbeat", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "observeLogMessages": { + "topology": "debug" + }, + "observeEvents": [ + "serverHeartbeatFailedEvent" + ], + "uriOptions": { + "appname": "failingHeartbeatLoggingTest" + } + } + } + ] + } + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "failPoint": { + "configureFailPoint": "failCommand", + "mode": "alwaysOn", + "data": { + "failCommands": [ + "hello", + "isMaster" + ], + "appName": "failingHeartbeatLoggingTest", + "closeConnection": true + } + }, + "client": "setupClient" + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverHeartbeatFailedEvent": {} + }, + "count": 1 + } + } + ], + "expectLogMessages": [ + { + "client": "client", + "ignoreExtraMessages": true, + "ignoreMessages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat started" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat succeeded" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting server monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped server monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed" + } + } + ], + "messages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting topology monitoring", + "topologyId": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat failed", + "awaited": { + "$$exists": true + }, + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + }, + "driverConnectionId": { + "$$exists": true + }, + "durationMS": { + "$$type": [ + "int", + "long" + ] + }, + "failure": { + "$$exists": true + } + } + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/logging-sharded.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/logging-sharded.json new file mode 100644 index 00000000000..61b27f5be0b --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/logging-sharded.json @@ -0,0 +1,492 @@ +{ + "description": "sharded-logging", + "schemaVersion": "1.16", + "runOnRequirements": [ + { + "topologies": [ + "sharded" + ], + "minServerVersion": "4.4" + } + ], + "createEntities": [ + { + "client": { + "id": "setupClient", + "useMultipleMongoses": false + } + } + ], + "tests": [ + { + "description": "Topology lifecycle", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "observeLogMessages": { + "topology": "debug" + }, + "observeEvents": [ + "topologyDescriptionChangedEvent" + ], + "useMultipleMongoses": true + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "topologyDescriptionChangedEvent": {} + }, + "count": 3 + } + }, + { + "name": "close", + "object": "client" + } + ], + "expectLogMessages": [ + { + "client": "client", + "ignoreMessages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting server monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat started" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat succeeded" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat failed" + } + } + ], + "messages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting topology monitoring", + "topologyId": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed", + "topologyId": { + "$$exists": true + }, + "previousDescription": { + "$$exists": true + }, + "newDescription": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed", + "topologyId": { + "$$exists": true + }, + "previousDescription": { + "$$exists": true + }, + "newDescription": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed", + "topologyId": { + "$$exists": true + }, + "previousDescription": { + "$$exists": true + }, + "newDescription": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped server monitoring", + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped server monitoring", + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed", + "topologyId": { + "$$exists": true + }, + "previousDescription": { + "$$exists": true + }, + "newDescription": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped topology monitoring", + "topologyId": { + "$$exists": true + } + } + } + ] + } + ] + }, + { + "description": "Successful heartbeat", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "observeLogMessages": { + "topology": "debug" + }, + "observeEvents": [ + "serverHeartbeatSucceededEvent" + ] + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverHeartbeatSucceededEvent": {} + }, + "count": 1 + } + } + ], + "expectLogMessages": [ + { + "client": "client", + "ignoreExtraMessages": true, + "ignoreMessages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat started" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting server monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped server monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed" + } + } + ], + "messages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting topology monitoring", + "topologyId": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat succeeded", + "awaited": { + "$$exists": true + }, + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + }, + "driverConnectionId": { + "$$exists": true + }, + "serverConnectionId": { + "$$exists": true + }, + "durationMS": { + "$$type": [ + "int", + "long" + ] + }, + "reply": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "ok": 1 + } + } + } + } + } + ] + } + ] + }, + { + "description": "Failing heartbeat", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "observeLogMessages": { + "topology": "debug" + }, + "observeEvents": [ + "serverHeartbeatStartedEvent", + "serverHeartbeatFailedEvent" + ], + "uriOptions": { + "appname": "failingHeartbeatLoggingTest" + } + } + } + ] + } + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "failPoint": { + "configureFailPoint": "failCommand", + "mode": "alwaysOn", + "data": { + "failCommands": [ + "hello", + "isMaster" + ], + "appName": "failingHeartbeatLoggingTest", + "closeConnection": true + } + }, + "client": "setupClient" + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverHeartbeatFailedEvent": {} + }, + "count": 1 + } + } + ], + "expectLogMessages": [ + { + "client": "client", + "ignoreExtraMessages": true, + "ignoreMessages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat started" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat succeeded" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting server monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped server monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed" + } + } + ], + "messages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting topology monitoring", + "topologyId": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat failed", + "awaited": { + "$$exists": true + }, + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + }, + "driverConnectionId": { + "$$exists": true + }, + "durationMS": { + "$$type": [ + "int", + "long" + ] + }, + "failure": { + "$$exists": true + } + } + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/logging-standalone.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/logging-standalone.json new file mode 100644 index 00000000000..1ee6dbe8995 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/logging-standalone.json @@ -0,0 +1,517 @@ +{ + "description": "standalone-logging", + "schemaVersion": "1.16", + "runOnRequirements": [ + { + "topologies": [ + "single" + ], + "minServerVersion": "4.4" + } + ], + "createEntities": [ + { + "client": { + "id": "setupClient" + } + } + ], + "tests": [ + { + "description": "Topology lifecycle", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "observeLogMessages": { + "topology": "debug" + }, + "observeEvents": [ + "topologyDescriptionChangedEvent" + ] + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "topologyDescriptionChangedEvent": {} + }, + "count": 2 + } + }, + { + "name": "close", + "object": "client" + } + ], + "expectLogMessages": [ + { + "client": "client", + "ignoreMessages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat started" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat succeeded" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat failed" + } + } + ], + "messages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting topology monitoring", + "topologyId": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed", + "topologyId": { + "$$exists": true + }, + "previousDescription": { + "$$exists": true + }, + "newDescription": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting server monitoring", + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed", + "topologyId": { + "$$exists": true + }, + "previousDescription": { + "$$exists": true + }, + "newDescription": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped server monitoring", + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed", + "topologyId": { + "$$exists": true + }, + "previousDescription": { + "$$exists": true + }, + "newDescription": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped topology monitoring", + "topologyId": { + "$$exists": true + } + } + } + ] + } + ] + }, + { + "description": "Successful heartbeat", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "observeLogMessages": { + "topology": "debug" + }, + "observeEvents": [ + "serverHeartbeatSucceededEvent" + ] + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverHeartbeatSucceededEvent": {} + }, + "count": 1 + } + }, + { + "name": "close", + "object": "client" + } + ], + "expectLogMessages": [ + { + "client": "client", + "ignoreExtraMessages": true, + "ignoreMessages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped topology monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped server monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting server monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting topology monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat failed" + } + } + ], + "messages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat started", + "awaited": { + "$$exists": true + }, + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + }, + "driverConnectionId": { + "$$exists": true + } + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat succeeded", + "awaited": { + "$$exists": true + }, + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + }, + "driverConnectionId": { + "$$exists": true + }, + "serverConnectionId": { + "$$exists": true + }, + "durationMS": { + "$$type": [ + "int", + "long" + ] + }, + "reply": { + "$$matchAsDocument": { + "$$matchAsRoot": { + "ok": 1 + } + } + } + } + } + ] + } + ] + }, + { + "description": "Failing heartbeat", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "observeLogMessages": { + "topology": "debug" + }, + "observeEvents": [ + "serverHeartbeatFailedEvent" + ], + "uriOptions": { + "appname": "failingHeartbeatLoggingTest" + } + } + } + ] + } + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "setupClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": "alwaysOn", + "data": { + "failCommands": [ + "hello", + "isMaster" + ], + "appName": "failingHeartbeatLoggingTest", + "closeConnection": true + } + } + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverHeartbeatFailedEvent": {} + }, + "count": 1 + } + } + ], + "expectLogMessages": [ + { + "client": "client", + "ignoreExtraMessages": true, + "ignoreMessages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped topology monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Stopped server monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Topology description changed" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting server monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Starting topology monitoring" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat started" + } + }, + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat succeeded" + } + } + ], + "messages": [ + { + "level": "debug", + "component": "topology", + "data": { + "message": "Server heartbeat failed", + "awaited": { + "$$exists": true + }, + "topologyId": { + "$$exists": true + }, + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + }, + "driverConnectionId": { + "$$exists": true + }, + "durationMS": { + "$$type": [ + "int", + "long" + ] + }, + "failure": { + "$$exists": true + } + } + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/minPoolSize-error.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/minPoolSize-error.json index 0234ac99292..bd9e9fcdec7 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/minPoolSize-error.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/minPoolSize-error.json @@ -1,9 +1,9 @@ { "description": "minPoolSize-error", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { - "minServerVersion": "4.9", + "minServerVersion": "4.4.7", "serverless": "forbid", "topologies": [ "single" diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/pool-cleared-error.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/pool-cleared-error.json index 9a7dfd901c5..b7f6924f2ba 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/pool-cleared-error.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/pool-cleared-error.json @@ -1,6 +1,6 @@ { "description": "pool-cleared-error", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.9", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/rediscover-quickly-after-step-down.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/rediscover-quickly-after-step-down.json index c7c2494857a..3147a07a1e6 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/rediscover-quickly-after-step-down.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/rediscover-quickly-after-step-down.json @@ -1,6 +1,6 @@ { "description": "rediscover-quickly-after-step-down", - "schemaVersion": "1.10", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.4", diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/replicaset-emit-topology-changed-before-close.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/replicaset-emit-topology-changed-before-close.json new file mode 100644 index 00000000000..066a4ffee5f --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/replicaset-emit-topology-changed-before-close.json @@ -0,0 +1,89 @@ +{ + "description": "replicaset-emit-topology-description-changed-before-close", + "schemaVersion": "1.20", + "runOnRequirements": [ + { + "topologies": [ + "replicaset" + ], + "minServerVersion": "4.4" + } + ], + "tests": [ + { + "description": "Topology lifecycle", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "topologyDescriptionChangedEvent", + "topologyOpeningEvent", + "topologyClosedEvent" + ] + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "topologyDescriptionChangedEvent": {} + }, + "count": 4 + } + }, + { + "name": "close", + "object": "client" + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "sdam", + "ignoreExtraEvents": false, + "events": [ + { + "topologyOpeningEvent": {} + }, + { + "topologyDescriptionChangedEvent": {} + }, + { + "topologyDescriptionChangedEvent": {} + }, + { + "topologyDescriptionChangedEvent": {} + }, + { + "topologyDescriptionChangedEvent": {} + }, + { + "topologyDescriptionChangedEvent": { + "previousDescription": { + "type": "ReplicaSetWithPrimary" + }, + "newDescription": { + "type": "Unknown" + } + } + }, + { + "topologyClosedEvent": {} + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/serverMonitoringMode.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/serverMonitoringMode.json index 7d681b4f9ec..4b492f7d853 100644 --- a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/serverMonitoringMode.json +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/serverMonitoringMode.json @@ -444,6 +444,69 @@ ] } ] + }, + { + "description": "poll waits after successful heartbeat", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "uriOptions": { + "serverMonitoringMode": "poll", + "heartbeatFrequencyMS": 1000000 + }, + "useMultipleMongoses": false, + "observeEvents": [ + "serverHeartbeatStartedEvent", + "serverHeartbeatSucceededEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "sdam-tests" + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverHeartbeatSucceededEvent": {} + }, + "count": 1 + } + }, + { + "name": "wait", + "object": "testRunner", + "arguments": { + "ms": 500 + } + }, + { + "name": "assertEventCount", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverHeartbeatStartedEvent": {} + }, + "count": 1 + } + } + ] } ] } diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/sharded-emit-topology-changed-before-close.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/sharded-emit-topology-changed-before-close.json new file mode 100644 index 00000000000..98fb5855314 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/sharded-emit-topology-changed-before-close.json @@ -0,0 +1,108 @@ +{ + "description": "sharded-emit-topology-description-changed-before-close", + "schemaVersion": "1.20", + "runOnRequirements": [ + { + "topologies": [ + "sharded" + ], + "minServerVersion": "4.4" + } + ], + "tests": [ + { + "description": "Topology lifecycle", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "topologyDescriptionChangedEvent", + "topologyOpeningEvent", + "topologyClosedEvent" + ], + "useMultipleMongoses": true + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "topologyDescriptionChangedEvent": {} + }, + "count": 3 + } + }, + { + "name": "close", + "object": "client" + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "sdam", + "ignoreExtraEvents": false, + "events": [ + { + "topologyOpeningEvent": {} + }, + { + "topologyDescriptionChangedEvent": { + "previousDescription": { + "type": "Unknown" + }, + "newDescription": { + "type": "Unknown" + } + } + }, + { + "topologyDescriptionChangedEvent": { + "previousDescription": { + "type": "Unknown" + }, + "newDescription": { + "type": "Sharded" + } + } + }, + { + "topologyDescriptionChangedEvent": { + "previousDescription": { + "type": "Sharded" + }, + "newDescription": { + "type": "Sharded" + } + } + }, + { + "topologyDescriptionChangedEvent": { + "previousDescription": { + "type": "Sharded" + }, + "newDescription": { + "type": "Unknown" + } + } + }, + { + "topologyClosedEvent": {} + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/standalone-emit-topology-changed-before-close.json b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/standalone-emit-topology-changed-before-close.json new file mode 100644 index 00000000000..27b5444d541 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/server-discovery-and-monitoring/standalone-emit-topology-changed-before-close.json @@ -0,0 +1,97 @@ +{ + "description": "standalone-emit-topology-description-changed-before-close", + "schemaVersion": "1.20", + "runOnRequirements": [ + { + "topologies": [ + "single" + ], + "minServerVersion": "4.4" + } + ], + "tests": [ + { + "description": "Topology lifecycle", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "topologyDescriptionChangedEvent", + "topologyOpeningEvent", + "topologyClosedEvent" + ] + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "topologyDescriptionChangedEvent": {} + }, + "count": 2 + } + }, + { + "name": "close", + "object": "client" + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "sdam", + "ignoreExtraEvents": false, + "events": [ + { + "topologyOpeningEvent": {} + }, + { + "topologyDescriptionChangedEvent": { + "previousDescription": { + "type": "Unknown" + }, + "newDescription": { + "type": "Unknown" + } + } + }, + { + "topologyDescriptionChangedEvent": { + "previousDescription": { + "type": "Unknown" + }, + "newDescription": { + "type": "Single" + } + } + }, + { + "topologyDescriptionChangedEvent": { + "previousDescription": { + "type": "Single" + }, + "newDescription": { + "type": "Unknown" + } + } + }, + { + "topologyClosedEvent": {} + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/sessions/driver-sessions-dirty-session-errors.json b/driver-core/src/test/resources/unified-test-format/sessions/driver-sessions-dirty-session-errors.json index 361ea83d7b5..6aa1da1df5e 100644 --- a/driver-core/src/test/resources/unified-test-format/sessions/driver-sessions-dirty-session-errors.json +++ b/driver-core/src/test/resources/unified-test-format/sessions/driver-sessions-dirty-session-errors.json @@ -11,7 +11,7 @@ { "minServerVersion": "4.1.8", "topologies": [ - "sharded-replicaset" + "sharded" ] } ], diff --git a/driver-core/src/test/resources/unified-test-format/sessions/snapshot-sessions-unsupported-ops.json b/driver-core/src/test/resources/unified-test-format/sessions/snapshot-sessions-unsupported-ops.json index 1021b7f2642..c41f74d3370 100644 --- a/driver-core/src/test/resources/unified-test-format/sessions/snapshot-sessions-unsupported-ops.json +++ b/driver-core/src/test/resources/unified-test-format/sessions/snapshot-sessions-unsupported-ops.json @@ -6,7 +6,7 @@ "minServerVersion": "5.0", "topologies": [ "replicaset", - "sharded-replicaset" + "sharded" ] } ], diff --git a/driver-core/src/test/resources/unified-test-format/sessions/snapshot-sessions.json b/driver-core/src/test/resources/unified-test-format/sessions/snapshot-sessions.json index 75b577b039f..260f8b6f489 100644 --- a/driver-core/src/test/resources/unified-test-format/sessions/snapshot-sessions.json +++ b/driver-core/src/test/resources/unified-test-format/sessions/snapshot-sessions.json @@ -6,7 +6,7 @@ "minServerVersion": "5.0", "topologies": [ "replicaset", - "sharded-replicaset" + "sharded" ] } ], diff --git a/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/callback-retry.json b/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/callback-retry.json index 1e07a2a656c..277dfa18ed6 100644 --- a/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/callback-retry.json +++ b/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/callback-retry.json @@ -1,6 +1,6 @@ { "description": "callback-retry", - "schemaVersion": "1.3", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.0", diff --git a/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/commit-retry.json b/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/commit-retry.json index 853562e32ea..928f0167e47 100644 --- a/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/commit-retry.json +++ b/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/commit-retry.json @@ -1,6 +1,6 @@ { "description": "commit-retry", - "schemaVersion": "1.3", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.0", diff --git a/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/commit-transienttransactionerror-4.2.json b/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/commit-transienttransactionerror-4.2.json index 07f190ffb43..0f5a782452c 100644 --- a/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/commit-transienttransactionerror-4.2.json +++ b/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/commit-transienttransactionerror-4.2.json @@ -1,6 +1,6 @@ { "description": "commit-transienttransactionerror-4.2", - "schemaVersion": "1.3", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.1.6", diff --git a/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/commit-transienttransactionerror.json b/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/commit-transienttransactionerror.json index 9584bb61b5b..dd5158d8134 100644 --- a/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/commit-transienttransactionerror.json +++ b/driver-core/src/test/resources/unified-test-format/transactions-convenient-api/commit-transienttransactionerror.json @@ -1,6 +1,6 @@ { "description": "commit-transienttransactionerror", - "schemaVersion": "1.3", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.0", diff --git a/driver-core/src/test/resources/unified-test-format/transactions/retryable-commit-errorLabels-forbid_serverless.json b/driver-core/src/test/resources/unified-test-format/transactions/retryable-commit-errorLabels-forbid_serverless.json deleted file mode 100644 index 2a9c44d4b07..00000000000 --- a/driver-core/src/test/resources/unified-test-format/transactions/retryable-commit-errorLabels-forbid_serverless.json +++ /dev/null @@ -1,351 +0,0 @@ -{ - "description": "retryable-commit-errorLabels-forbid_serverless", - "schemaVersion": "1.4", - "runOnRequirements": [ - { - "minServerVersion": "4.3.1", - "serverless": "forbid", - "topologies": [ - "replicaset", - "sharded", - "load-balanced" - ] - } - ], - "createEntities": [ - { - "client": { - "id": "client0", - "useMultipleMongoses": false, - "observeEvents": [ - "commandStartedEvent" - ] - } - }, - { - "database": { - "id": "database0", - "client": "client0", - "databaseName": "transaction-tests" - } - }, - { - "collection": { - "id": "collection0", - "database": "database0", - "collectionName": "test" - } - }, - { - "session": { - "id": "session0", - "client": "client0" - } - }, - { - "session": { - "id": "session1", - "client": "client0" - } - } - ], - "initialData": [ - { - "collectionName": "test", - "databaseName": "transaction-tests", - "documents": [] - } - ], - "tests": [ - { - "description": "commitTransaction succeeds after InterruptedAtShutdown", - "operations": [ - { - "object": "testRunner", - "name": "failPoint", - "arguments": { - "client": "client0", - "failPoint": { - "configureFailPoint": "failCommand", - "mode": { - "times": 1 - }, - "data": { - "failCommands": [ - "commitTransaction" - ], - "errorCode": 11600, - "errorLabels": [ - "RetryableWriteError" - ], - "closeConnection": false - } - } - } - }, - { - "object": "session0", - "name": "startTransaction" - }, - { - "object": "collection0", - "name": "insertOne", - "arguments": { - "session": "session0", - "document": { - "_id": 1 - } - }, - "expectResult": { - "$$unsetOrMatches": { - "insertedId": { - "$$unsetOrMatches": 1 - } - } - } - }, - { - "object": "session0", - "name": "commitTransaction" - } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "insert": "test", - "documents": [ - { - "_id": 1 - } - ], - "ordered": true, - "readConcern": { - "$$exists": false - }, - "lsid": { - "$$sessionLsid": "session0" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": true, - "autocommit": false, - "writeConcern": { - "$$exists": false - } - }, - "commandName": "insert", - "databaseName": "transaction-tests" - } - }, - { - "commandStartedEvent": { - "command": { - "commitTransaction": 1, - "lsid": { - "$$sessionLsid": "session0" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": { - "$$exists": false - }, - "autocommit": false, - "writeConcern": { - "$$exists": false - } - }, - "commandName": "commitTransaction", - "databaseName": "admin" - } - }, - { - "commandStartedEvent": { - "command": { - "commitTransaction": 1, - "lsid": { - "$$sessionLsid": "session0" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": { - "$$exists": false - }, - "autocommit": false, - "writeConcern": { - "w": "majority", - "wtimeout": 10000 - } - }, - "commandName": "commitTransaction", - "databaseName": "admin" - } - } - ] - } - ], - "outcome": [ - { - "collectionName": "test", - "databaseName": "transaction-tests", - "documents": [ - { - "_id": 1 - } - ] - } - ] - }, - { - "description": "commitTransaction succeeds after ShutdownInProgress", - "operations": [ - { - "object": "testRunner", - "name": "failPoint", - "arguments": { - "client": "client0", - "failPoint": { - "configureFailPoint": "failCommand", - "mode": { - "times": 1 - }, - "data": { - "failCommands": [ - "commitTransaction" - ], - "errorCode": 91, - "errorLabels": [ - "RetryableWriteError" - ], - "closeConnection": false - } - } - } - }, - { - "object": "session0", - "name": "startTransaction" - }, - { - "object": "collection0", - "name": "insertOne", - "arguments": { - "session": "session0", - "document": { - "_id": 1 - } - }, - "expectResult": { - "$$unsetOrMatches": { - "insertedId": { - "$$unsetOrMatches": 1 - } - } - } - }, - { - "object": "session0", - "name": "commitTransaction" - } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "insert": "test", - "documents": [ - { - "_id": 1 - } - ], - "ordered": true, - "readConcern": { - "$$exists": false - }, - "lsid": { - "$$sessionLsid": "session0" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": true, - "autocommit": false, - "writeConcern": { - "$$exists": false - } - }, - "commandName": "insert", - "databaseName": "transaction-tests" - } - }, - { - "commandStartedEvent": { - "command": { - "commitTransaction": 1, - "lsid": { - "$$sessionLsid": "session0" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": { - "$$exists": false - }, - "autocommit": false, - "writeConcern": { - "$$exists": false - } - }, - "commandName": "commitTransaction", - "databaseName": "admin" - } - }, - { - "commandStartedEvent": { - "command": { - "commitTransaction": 1, - "lsid": { - "$$sessionLsid": "session0" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": { - "$$exists": false - }, - "autocommit": false, - "writeConcern": { - "w": "majority", - "wtimeout": 10000 - } - }, - "commandName": "commitTransaction", - "databaseName": "admin" - } - } - ] - } - ], - "outcome": [ - { - "collectionName": "test", - "databaseName": "transaction-tests", - "documents": [ - { - "_id": 1 - } - ] - } - ] - } - ] -} diff --git a/driver-core/src/test/resources/unified-test-format/transactions/retryable-commit-forbid_serverless.json b/driver-core/src/test/resources/unified-test-format/transactions/retryable-commit-forbid_serverless.json deleted file mode 100644 index c99dd816bda..00000000000 --- a/driver-core/src/test/resources/unified-test-format/transactions/retryable-commit-forbid_serverless.json +++ /dev/null @@ -1,598 +0,0 @@ -{ - "description": "retryable-commit-forbid_serverless", - "schemaVersion": "1.4", - "runOnRequirements": [ - { - "minServerVersion": "4.0", - "topologies": [ - "replicaset" - ] - }, - { - "minServerVersion": "4.1.8", - "serverless": "forbid", - "topologies": [ - "sharded", - "load-balanced" - ] - } - ], - "createEntities": [ - { - "client": { - "id": "client0", - "useMultipleMongoses": false, - "observeEvents": [ - "commandStartedEvent" - ] - } - }, - { - "database": { - "id": "database0", - "client": "client0", - "databaseName": "transaction-tests" - } - }, - { - "collection": { - "id": "collection0", - "database": "database0", - "collectionName": "test" - } - }, - { - "session": { - "id": "session0", - "client": "client0" - } - }, - { - "client": { - "id": "client1", - "useMultipleMongoses": false, - "uriOptions": { - "retryWrites": false - }, - "observeEvents": [ - "commandStartedEvent" - ] - } - }, - { - "database": { - "id": "database1", - "client": "client1", - "databaseName": "transaction-tests" - } - }, - { - "collection": { - "id": "collection1", - "database": "database1", - "collectionName": "test" - } - }, - { - "session": { - "id": "session1", - "client": "client1" - } - } - ], - "initialData": [ - { - "collectionName": "test", - "databaseName": "transaction-tests", - "documents": [] - } - ], - "tests": [ - { - "description": "commitTransaction fails after two errors", - "operations": [ - { - "object": "testRunner", - "name": "failPoint", - "arguments": { - "client": "client1", - "failPoint": { - "configureFailPoint": "failCommand", - "mode": { - "times": 2 - }, - "data": { - "failCommands": [ - "commitTransaction" - ], - "closeConnection": true - } - } - } - }, - { - "object": "session1", - "name": "startTransaction" - }, - { - "object": "collection1", - "name": "insertOne", - "arguments": { - "session": "session1", - "document": { - "_id": 1 - } - }, - "expectResult": { - "$$unsetOrMatches": { - "insertedId": { - "$$unsetOrMatches": 1 - } - } - } - }, - { - "object": "session1", - "name": "commitTransaction", - "expectError": { - "errorLabelsContain": [ - "RetryableWriteError", - "UnknownTransactionCommitResult" - ], - "errorLabelsOmit": [ - "TransientTransactionError" - ] - } - }, - { - "object": "session1", - "name": "commitTransaction" - } - ], - "expectEvents": [ - { - "client": "client1", - "events": [ - { - "commandStartedEvent": { - "command": { - "insert": "test", - "documents": [ - { - "_id": 1 - } - ], - "ordered": true, - "readConcern": { - "$$exists": false - }, - "lsid": { - "$$sessionLsid": "session1" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": true, - "autocommit": false, - "writeConcern": { - "$$exists": false - } - }, - "commandName": "insert", - "databaseName": "transaction-tests" - } - }, - { - "commandStartedEvent": { - "command": { - "commitTransaction": 1, - "lsid": { - "$$sessionLsid": "session1" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": { - "$$exists": false - }, - "autocommit": false, - "writeConcern": { - "$$exists": false - } - }, - "commandName": "commitTransaction", - "databaseName": "admin" - } - }, - { - "commandStartedEvent": { - "command": { - "commitTransaction": 1, - "lsid": { - "$$sessionLsid": "session1" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": { - "$$exists": false - }, - "autocommit": false, - "writeConcern": { - "w": "majority", - "wtimeout": 10000 - } - }, - "commandName": "commitTransaction", - "databaseName": "admin" - } - }, - { - "commandStartedEvent": { - "command": { - "commitTransaction": 1, - "lsid": { - "$$sessionLsid": "session1" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": { - "$$exists": false - }, - "autocommit": false, - "writeConcern": { - "w": "majority", - "wtimeout": 10000 - } - }, - "commandName": "commitTransaction", - "databaseName": "admin" - } - } - ] - } - ], - "outcome": [ - { - "collectionName": "test", - "databaseName": "transaction-tests", - "documents": [ - { - "_id": 1 - } - ] - } - ] - }, - { - "description": "commitTransaction applies majority write concern on retries", - "operations": [ - { - "object": "testRunner", - "name": "failPoint", - "arguments": { - "client": "client1", - "failPoint": { - "configureFailPoint": "failCommand", - "mode": { - "times": 2 - }, - "data": { - "failCommands": [ - "commitTransaction" - ], - "closeConnection": true - } - } - } - }, - { - "object": "session1", - "name": "startTransaction", - "arguments": { - "writeConcern": { - "w": 2, - "journal": true, - "wtimeoutMS": 5000 - } - } - }, - { - "object": "collection1", - "name": "insertOne", - "arguments": { - "session": "session1", - "document": { - "_id": 1 - } - }, - "expectResult": { - "$$unsetOrMatches": { - "insertedId": { - "$$unsetOrMatches": 1 - } - } - } - }, - { - "object": "session1", - "name": "commitTransaction", - "expectError": { - "errorLabelsContain": [ - "RetryableWriteError", - "UnknownTransactionCommitResult" - ], - "errorLabelsOmit": [ - "TransientTransactionError" - ] - } - }, - { - "object": "session1", - "name": "commitTransaction" - } - ], - "expectEvents": [ - { - "client": "client1", - "events": [ - { - "commandStartedEvent": { - "command": { - "insert": "test", - "documents": [ - { - "_id": 1 - } - ], - "ordered": true, - "readConcern": { - "$$exists": false - }, - "lsid": { - "$$sessionLsid": "session1" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": true, - "autocommit": false, - "writeConcern": { - "$$exists": false - } - }, - "commandName": "insert", - "databaseName": "transaction-tests" - } - }, - { - "commandStartedEvent": { - "command": { - "commitTransaction": 1, - "lsid": { - "$$sessionLsid": "session1" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": { - "$$exists": false - }, - "autocommit": false, - "writeConcern": { - "w": 2, - "j": true, - "wtimeout": 5000 - } - }, - "commandName": "commitTransaction", - "databaseName": "admin" - } - }, - { - "commandStartedEvent": { - "command": { - "commitTransaction": 1, - "lsid": { - "$$sessionLsid": "session1" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": { - "$$exists": false - }, - "autocommit": false, - "writeConcern": { - "w": "majority", - "j": true, - "wtimeout": 5000 - } - }, - "commandName": "commitTransaction", - "databaseName": "admin" - } - }, - { - "commandStartedEvent": { - "command": { - "commitTransaction": 1, - "lsid": { - "$$sessionLsid": "session1" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": { - "$$exists": false - }, - "autocommit": false, - "writeConcern": { - "w": "majority", - "j": true, - "wtimeout": 5000 - } - }, - "commandName": "commitTransaction", - "databaseName": "admin" - } - } - ] - } - ], - "outcome": [ - { - "collectionName": "test", - "databaseName": "transaction-tests", - "documents": [ - { - "_id": 1 - } - ] - } - ] - }, - { - "description": "commitTransaction succeeds after connection error", - "operations": [ - { - "object": "testRunner", - "name": "failPoint", - "arguments": { - "client": "client0", - "failPoint": { - "configureFailPoint": "failCommand", - "mode": { - "times": 1 - }, - "data": { - "failCommands": [ - "commitTransaction" - ], - "closeConnection": true - } - } - } - }, - { - "object": "session0", - "name": "startTransaction" - }, - { - "object": "collection0", - "name": "insertOne", - "arguments": { - "session": "session0", - "document": { - "_id": 1 - } - }, - "expectResult": { - "$$unsetOrMatches": { - "insertedId": { - "$$unsetOrMatches": 1 - } - } - } - }, - { - "object": "session0", - "name": "commitTransaction" - } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "insert": "test", - "documents": [ - { - "_id": 1 - } - ], - "ordered": true, - "readConcern": { - "$$exists": false - }, - "lsid": { - "$$sessionLsid": "session0" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": true, - "autocommit": false, - "writeConcern": { - "$$exists": false - } - }, - "commandName": "insert", - "databaseName": "transaction-tests" - } - }, - { - "commandStartedEvent": { - "command": { - "commitTransaction": 1, - "lsid": { - "$$sessionLsid": "session0" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": { - "$$exists": false - }, - "autocommit": false, - "writeConcern": { - "$$exists": false - } - }, - "commandName": "commitTransaction", - "databaseName": "admin" - } - }, - { - "commandStartedEvent": { - "command": { - "commitTransaction": 1, - "lsid": { - "$$sessionLsid": "session0" - }, - "txnNumber": { - "$numberLong": "1" - }, - "startTransaction": { - "$$exists": false - }, - "autocommit": false, - "writeConcern": { - "w": "majority", - "wtimeout": 10000 - } - }, - "commandName": "commitTransaction", - "databaseName": "admin" - } - } - ] - } - ], - "outcome": [ - { - "collectionName": "test", - "databaseName": "transaction-tests", - "documents": [ - { - "_id": 1 - } - ] - } - ] - } - ] -} diff --git a/driver-core/src/test/resources/unified-test-format/valid-fail/kmsProviders-missing_aws_kms_credentials.json b/driver-core/src/test/resources/unified-test-format/valid-fail/kmsProviders-missing_aws_kms_credentials.json new file mode 100644 index 00000000000..e62de800332 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/valid-fail/kmsProviders-missing_aws_kms_credentials.json @@ -0,0 +1,36 @@ +{ + "description": "kmsProviders-missing_aws_kms_credentials", + "schemaVersion": "1.8", + "runOnRequirements": [ + { + "csfle": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "clientEncryption": { + "id": "clientEncryption0", + "clientEncryptionOpts": { + "keyVaultClient": "client0", + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": { + "aws": { + "accessKeyId": "accessKeyId" + } + } + } + } + } + ], + "tests": [ + { + "description": "", + "operations": [] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/valid-fail/kmsProviders-missing_azure_kms_credentials.json b/driver-core/src/test/resources/unified-test-format/valid-fail/kmsProviders-missing_azure_kms_credentials.json new file mode 100644 index 00000000000..8ef805d0fa6 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/valid-fail/kmsProviders-missing_azure_kms_credentials.json @@ -0,0 +1,36 @@ +{ + "description": "kmsProviders-missing_azure_kms_credentials", + "schemaVersion": "1.8", + "runOnRequirements": [ + { + "csfle": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "clientEncryption": { + "id": "clientEncryption0", + "clientEncryptionOpts": { + "keyVaultClient": "client0", + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": { + "azure": { + "tenantId": "tenantId" + } + } + } + } + } + ], + "tests": [ + { + "description": "", + "operations": [] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/valid-fail/kmsProviders-missing_gcp_kms_credentials.json b/driver-core/src/test/resources/unified-test-format/valid-fail/kmsProviders-missing_gcp_kms_credentials.json new file mode 100644 index 00000000000..c6da1ce58ca --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/valid-fail/kmsProviders-missing_gcp_kms_credentials.json @@ -0,0 +1,36 @@ +{ + "description": "kmsProviders-missing_gcp_kms_credentials", + "schemaVersion": "1.8", + "runOnRequirements": [ + { + "csfle": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "clientEncryption": { + "id": "clientEncryption0", + "clientEncryptionOpts": { + "keyVaultClient": "client0", + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": { + "gcp": { + "email": "email" + } + } + } + } + } + ], + "tests": [ + { + "description": "", + "operations": [] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/valid-fail/kmsProviders-no_kms.json b/driver-core/src/test/resources/unified-test-format/valid-fail/kmsProviders-no_kms.json new file mode 100644 index 00000000000..57499b4eaf4 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/valid-fail/kmsProviders-no_kms.json @@ -0,0 +1,32 @@ +{ + "description": "clientEncryptionOpts-no_kms", + "schemaVersion": "1.8", + "runOnRequirements": [ + { + "csfle": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "clientEncryption": { + "id": "clientEncryption0", + "clientEncryptionOpts": { + "keyVaultClient": "client0", + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": {} + } + } + } + ], + "tests": [ + { + "description": "", + "operations": [] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/collectionData-createOptions.json b/driver-core/src/test/resources/unified-test-format/valid-pass/collectionData-createOptions.json new file mode 100644 index 00000000000..19edc2247b0 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/collectionData-createOptions.json @@ -0,0 +1,79 @@ +{ + "description": "collectionData-createOptions", + "schemaVersion": "1.9", + "runOnRequirements": [ + { + "minServerVersion": "3.6", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "database0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "database0", + "createOptions": { + "capped": true, + "size": 4096 + }, + "documents": [ + { + "_id": 1, + "x": 11 + } + ] + } + ], + "tests": [ + { + "description": "collection is created with the correct options", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$collStats": { + "storageStats": {} + } + }, + { + "$project": { + "capped": "$storageStats.capped", + "maxSize": "$storageStats.maxSize" + } + } + ] + }, + "expectResult": [ + { + "capped": true, + "maxSize": 4096 + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/createEntities-operation.json b/driver-core/src/test/resources/unified-test-format/valid-pass/createEntities-operation.json new file mode 100644 index 00000000000..3fde42919d7 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/createEntities-operation.json @@ -0,0 +1,74 @@ +{ + "description": "createEntities-operation", + "schemaVersion": "1.9", + "tests": [ + { + "description": "createEntities operation", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client1", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database1", + "client": "client1", + "databaseName": "database1" + } + }, + { + "collection": { + "id": "collection1", + "database": "database1", + "collectionName": "coll1" + } + } + ] + } + }, + { + "name": "deleteOne", + "object": "collection1", + "arguments": { + "filter": { + "_id": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client1", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "coll1", + "deletes": [ + { + "q": { + "_id": 1 + }, + "limit": 1 + } + ] + }, + "commandName": "delete", + "databaseName": "database1" + } + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/entity-commandCursor.json b/driver-core/src/test/resources/unified-test-format/valid-pass/entity-commandCursor.json new file mode 100644 index 00000000000..72b74b4a9a8 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/entity-commandCursor.json @@ -0,0 +1,278 @@ +{ + "description": "entity-commandCursor", + "schemaVersion": "1.3", + "createEntities": [ + { + "client": { + "id": "client", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "db" + } + }, + { + "collection": { + "id": "collection", + "database": "db", + "collectionName": "collection" + } + } + ], + "initialData": [ + { + "collectionName": "collection", + "databaseName": "db", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "tests": [ + { + "description": "runCursorCommand creates and exhausts cursor by running getMores", + "operations": [ + { + "name": "runCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "batchSize": 2, + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2, + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "collection", + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "getMore" + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "collection", + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "getMore" + } + } + ] + } + ] + }, + { + "description": "createCommandCursor creates a cursor and stores it as an entity that can be iterated one document at a time", + "operations": [ + { + "name": "createCommandCursor", + "object": "db", + "arguments": { + "commandName": "find", + "batchSize": 2, + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2 + } + }, + "saveResultAsEntity": "myRunCommandCursor" + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 2, + "x": 22 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 3, + "x": 33 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 4, + "x": 44 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 5, + "x": 55 + } + } + ] + }, + { + "description": "createCommandCursor's cursor can be closed and will perform a killCursors operation", + "operations": [ + { + "name": "createCommandCursor", + "object": "db", + "arguments": { + "commandName": "find", + "batchSize": 2, + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2 + } + }, + "saveResultAsEntity": "myRunCommandCursor" + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "close", + "object": "myRunCommandCursor" + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2, + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "killCursors": "collection", + "cursors": { + "$$type": "array" + } + }, + "commandName": "killCursors" + } + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/entity-cursor-iterateOnce.json b/driver-core/src/test/resources/unified-test-format/valid-pass/entity-cursor-iterateOnce.json new file mode 100644 index 00000000000..b17ae78b942 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/entity-cursor-iterateOnce.json @@ -0,0 +1,111 @@ +{ + "description": "entity-cursor-iterateOnce", + "schemaVersion": "1.9", + "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": [ + "int", + "long" + ] + }, + "collection": "coll0" + }, + "commandName": "getMore" + } + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/entity-find-cursor.json b/driver-core/src/test/resources/unified-test-format/valid-pass/entity-find-cursor.json index 85b8f69d7f3..6f955d81f4a 100644 --- a/driver-core/src/test/resources/unified-test-format/valid-pass/entity-find-cursor.json +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/entity-find-cursor.json @@ -109,7 +109,10 @@ "reply": { "cursor": { "id": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "ns": { "$$type": "string" @@ -126,7 +129,10 @@ "commandStartedEvent": { "command": { "getMore": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "collection": "coll0" }, @@ -138,7 +144,10 @@ "reply": { "cursor": { "id": { - "$$type": "long" + "$$type": [ + "int", + "long" + ] }, "ns": { "$$type": "string" diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/expectedEventsForClient-topologyDescriptionChangedEvent.json b/driver-core/src/test/resources/unified-test-format/valid-pass/expectedEventsForClient-topologyDescriptionChangedEvent.json new file mode 100644 index 00000000000..cf7bd60826b --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/expectedEventsForClient-topologyDescriptionChangedEvent.json @@ -0,0 +1,68 @@ +{ + "description": "expectedEventsForClient-topologyDescriptionChangedEvent", + "schemaVersion": "1.20", + "runOnRequirements": [ + { + "topologies": [ + "replicaset" + ], + "minServerVersion": "4.4" + } + ], + "tests": [ + { + "description": "can assert on values of newDescription and previousDescription fields", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "uriOptions": { + "directConnection": true + }, + "observeEvents": [ + "topologyDescriptionChangedEvent" + ] + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "topologyDescriptionChangedEvent": {} + }, + "count": 1 + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "sdam", + "ignoreExtraEvents": true, + "events": [ + { + "topologyDescriptionChangedEvent": { + "previousDescription": { + "type": "Unknown" + }, + "newDescription": { + "type": "Single" + } + } + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/kmsProviders-explicit_kms_credentials.json b/driver-core/src/test/resources/unified-test-format/valid-pass/kmsProviders-explicit_kms_credentials.json new file mode 100644 index 00000000000..7cc74939ebc --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/kmsProviders-explicit_kms_credentials.json @@ -0,0 +1,52 @@ +{ + "description": "kmsProviders-explicit_kms_credentials", + "schemaVersion": "1.8", + "runOnRequirements": [ + { + "csfle": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "clientEncryption": { + "id": "clientEncryption0", + "clientEncryptionOpts": { + "keyVaultClient": "client0", + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": { + "aws": { + "accessKeyId": "accessKeyId", + "secretAccessKey": "secretAccessKey" + }, + "azure": { + "tenantId": "tenantId", + "clientId": "clientId", + "clientSecret": "clientSecret" + }, + "gcp": { + "email": "email", + "privateKey": "cHJpdmF0ZUtleQo=" + }, + "kmip": { + "endpoint": "endpoint" + }, + "local": { + "key": "a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5" + } + } + } + } + } + ], + "tests": [ + { + "description": "", + "operations": [] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/kmsProviders-mixed_kms_credential_fields.json b/driver-core/src/test/resources/unified-test-format/valid-pass/kmsProviders-mixed_kms_credential_fields.json new file mode 100644 index 00000000000..363f2a45761 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/kmsProviders-mixed_kms_credential_fields.json @@ -0,0 +1,54 @@ +{ + "description": "kmsProviders-mixed_kms_credential_fields", + "schemaVersion": "1.8", + "runOnRequirements": [ + { + "csfle": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "clientEncryption": { + "id": "clientEncryption0", + "clientEncryptionOpts": { + "keyVaultClient": "client0", + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": { + "aws": { + "accessKeyId": "accessKeyId", + "secretAccessKey": { + "$$placeholder": 1 + } + }, + "azure": { + "tenantId": "tenantId", + "clientId": { + "$$placeholder": 1 + }, + "clientSecret": { + "$$placeholder": 1 + } + }, + "gcp": { + "email": "email", + "privateKey": { + "$$placeholder": 1 + } + } + } + } + } + } + ], + "tests": [ + { + "description": "", + "operations": [] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/kmsProviders-placeholder_kms_credentials.json b/driver-core/src/test/resources/unified-test-format/valid-pass/kmsProviders-placeholder_kms_credentials.json new file mode 100644 index 00000000000..3f7721f01d5 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/kmsProviders-placeholder_kms_credentials.json @@ -0,0 +1,70 @@ +{ + "description": "kmsProviders-placeholder_kms_credentials", + "schemaVersion": "1.8", + "runOnRequirements": [ + { + "csfle": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "clientEncryption": { + "id": "clientEncryption0", + "clientEncryptionOpts": { + "keyVaultClient": "client0", + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": { + "aws": { + "accessKeyId": { + "$$placeholder": 1 + }, + "secretAccessKey": { + "$$placeholder": 1 + } + }, + "azure": { + "tenantId": { + "$$placeholder": 1 + }, + "clientId": { + "$$placeholder": 1 + }, + "clientSecret": { + "$$placeholder": 1 + } + }, + "gcp": { + "email": { + "$$placeholder": 1 + }, + "privateKey": { + "$$placeholder": 1 + } + }, + "kmip": { + "endpoint": { + "$$placeholder": 1 + } + }, + "local": { + "key": { + "$$placeholder": 1 + } + } + } + } + } + } + ], + "tests": [ + { + "description": "", + "operations": [] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/kmsProviders-unconfigured_kms.json b/driver-core/src/test/resources/unified-test-format/valid-pass/kmsProviders-unconfigured_kms.json new file mode 100644 index 00000000000..12ca580941b --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/kmsProviders-unconfigured_kms.json @@ -0,0 +1,39 @@ +{ + "description": "kmsProviders-unconfigured_kms", + "schemaVersion": "1.8", + "runOnRequirements": [ + { + "csfle": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "clientEncryption": { + "id": "clientEncryption0", + "clientEncryptionOpts": { + "keyVaultClient": "client0", + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": { + "aws": {}, + "azure": {}, + "gcp": {}, + "kmip": {}, + "local": {} + } + } + } + } + ], + "tests": [ + { + "description": "", + "skipReason": "DRIVERS-2280: waiting on driver support for on-demand credentials", + "operations": [] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/matches-lte-operator.json b/driver-core/src/test/resources/unified-test-format/valid-pass/matches-lte-operator.json new file mode 100644 index 00000000000..4de65c58387 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/matches-lte-operator.json @@ -0,0 +1,78 @@ +{ + "description": "matches-lte-operator", + "schemaVersion": "1.9", + "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/driver-core/src/test/resources/unified-test-format/valid-pass/poc-crud.json b/driver-core/src/test/resources/unified-test-format/valid-pass/poc-crud.json index 0790d9b789f..94e4ec56829 100644 --- a/driver-core/src/test/resources/unified-test-format/valid-pass/poc-crud.json +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/poc-crud.json @@ -322,7 +322,7 @@ "minServerVersion": "4.1.0", "topologies": [ "replicaset", - "sharded-replicaset" + "sharded" ], "serverless": "forbid" } diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/poc-retryable-writes.json b/driver-core/src/test/resources/unified-test-format/valid-pass/poc-retryable-writes.json index 50160799f33..f19aa3f9d87 100644 --- a/driver-core/src/test/resources/unified-test-format/valid-pass/poc-retryable-writes.json +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/poc-retryable-writes.json @@ -1,14 +1,6 @@ { "description": "poc-retryable-writes", "schemaVersion": "1.0", - "runOnRequirements": [ - { - "minServerVersion": "3.6", - "topologies": [ - "replicaset" - ] - } - ], "createEntities": [ { "client": { @@ -79,6 +71,14 @@ "tests": [ { "description": "FindOneAndUpdate is committed on first attempt", + "runOnRequirements": [ + { + "minServerVersion": "3.6", + "topologies": [ + "replicaset" + ] + } + ], "operations": [ { "name": "failPoint", @@ -132,6 +132,14 @@ }, { "description": "FindOneAndUpdate is not committed on first attempt", + "runOnRequirements": [ + { + "minServerVersion": "3.6", + "topologies": [ + "replicaset" + ] + } + ], "operations": [ { "name": "failPoint", @@ -188,6 +196,14 @@ }, { "description": "FindOneAndUpdate is never committed", + "runOnRequirements": [ + { + "minServerVersion": "3.6", + "topologies": [ + "replicaset" + ] + } + ], "operations": [ { "name": "failPoint", @@ -245,15 +261,10 @@ "description": "InsertMany succeeds after PrimarySteppedDown", "runOnRequirements": [ { - "minServerVersion": "4.0", - "topologies": [ - "replicaset" - ] - }, - { - "minServerVersion": "4.1.7", + "minServerVersion": "4.3.1", "topologies": [ - "sharded-replicaset" + "replicaset", + "sharded" ] } ], @@ -345,7 +356,7 @@ { "minServerVersion": "4.1.7", "topologies": [ - "sharded-replicaset" + "sharded" ] } ], @@ -406,15 +417,10 @@ "description": "InsertOne fails after multiple retryable writeConcernErrors", "runOnRequirements": [ { - "minServerVersion": "4.0", - "topologies": [ - "replicaset" - ] - }, - { - "minServerVersion": "4.1.7", + "minServerVersion": "4.3.1", "topologies": [ - "sharded-replicaset" + "replicaset", + "sharded" ] } ], @@ -433,6 +439,9 @@ "failCommands": [ "insert" ], + "errorLabels": [ + "RetryableWriteError" + ], "writeConcernError": { "code": 91, "errmsg": "Replication is being shut down" diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/poc-sessions.json b/driver-core/src/test/resources/unified-test-format/valid-pass/poc-sessions.json index 75f34894286..117c9e7d009 100644 --- a/driver-core/src/test/resources/unified-test-format/valid-pass/poc-sessions.json +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/poc-sessions.json @@ -264,7 +264,7 @@ { "minServerVersion": "4.1.8", "topologies": [ - "sharded-replicaset" + "sharded" ] } ], diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/poc-transactions-convenient-api.json b/driver-core/src/test/resources/unified-test-format/valid-pass/poc-transactions-convenient-api.json index 820ed659276..9ab44a9c548 100644 --- a/driver-core/src/test/resources/unified-test-format/valid-pass/poc-transactions-convenient-api.json +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/poc-transactions-convenient-api.json @@ -11,7 +11,7 @@ { "minServerVersion": "4.1.8", "topologies": [ - "sharded-replicaset" + "sharded" ] } ], diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/poc-transactions-mongos-pin-auto.json b/driver-core/src/test/resources/unified-test-format/valid-pass/poc-transactions-mongos-pin-auto.json index a0b297d59a5..de08edec442 100644 --- a/driver-core/src/test/resources/unified-test-format/valid-pass/poc-transactions-mongos-pin-auto.json +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/poc-transactions-mongos-pin-auto.json @@ -5,7 +5,7 @@ { "minServerVersion": "4.1.8", "topologies": [ - "sharded-replicaset" + "sharded" ] } ], diff --git a/driver-core/src/test/resources/unified-test-format/valid-pass/poc-transactions.json b/driver-core/src/test/resources/unified-test-format/valid-pass/poc-transactions.json index 0355ca20605..2055a3b7057 100644 --- a/driver-core/src/test/resources/unified-test-format/valid-pass/poc-transactions.json +++ b/driver-core/src/test/resources/unified-test-format/valid-pass/poc-transactions.json @@ -11,7 +11,7 @@ { "minServerVersion": "4.1.8", "topologies": [ - "sharded-replicaset" + "sharded" ] } ], @@ -93,7 +93,7 @@ "minServerVersion": "4.3.4", "topologies": [ "replicaset", - "sharded-replicaset" + "sharded" ] } ], @@ -203,7 +203,7 @@ "minServerVersion": "4.3.4", "topologies": [ "replicaset", - "sharded-replicaset" + "sharded" ] } ], diff --git a/driver-core/src/test/unit/com/mongodb/client/model/AggregatesSpecification.groovy b/driver-core/src/test/unit/com/mongodb/client/model/AggregatesSpecification.groovy index 21df76e401e..3af81fc992c 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/AggregatesSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/client/model/AggregatesSpecification.groovy @@ -23,6 +23,7 @@ import com.mongodb.client.model.search.SearchOperator import org.bson.BsonDocument import org.bson.BsonInt32 import org.bson.Document +import org.bson.Vector import org.bson.conversions.Bson import spock.lang.IgnoreIf import spock.lang.Specification @@ -855,7 +856,7 @@ class AggregatesSpecification extends Specification { BsonDocument vectorSearchDoc = toBson( vectorSearch( fieldPath('fieldName').multi('ignored'), - [1.0d, 2.0d], + vector, 'indexName', 1, approximateVectorSearchOptions(2) @@ -868,13 +869,20 @@ class AggregatesSpecification extends Specification { vectorSearchDoc == parse('''{ "$vectorSearch": { "path": "fieldName", - "queryVector": [1.0, 2.0], + "queryVector": ''' + queryVector + ''', "index": "indexName", "numCandidates": {"$numberLong": "2"}, "limit": {"$numberLong": "1"}, "filter": {"fieldName": {"$ne": "fieldValue"}} } }''') + + where: + vector | queryVector + Vector.int8Vector(new byte[]{127, 7}) | '{"$binary": {"base64": "AwB/Bw==", "subType": "09"}}' + Vector.floatVector(new float[]{127.0f, 7.0f}) | '{"$binary": {"base64": "JwAAAP5CAADgQA==", "subType": "09"}}' + Vector.packedBitVector(new byte[]{127, 7}, (byte) 0) | '{"$binary": {"base64": "EAB/Bw==", "subType": "09"}}' + [1.0d, 2.0d] | "[1.0, 2.0]" } def 'should render exact $vectorSearch'() { @@ -882,7 +890,7 @@ class AggregatesSpecification extends Specification { BsonDocument vectorSearchDoc = toBson( vectorSearch( fieldPath('fieldName').multi('ignored'), - [1.0d, 2.0d], + vector, 'indexName', 1, exactVectorSearchOptions() @@ -895,13 +903,19 @@ class AggregatesSpecification extends Specification { vectorSearchDoc == parse('''{ "$vectorSearch": { "path": "fieldName", - "queryVector": [1.0, 2.0], + "queryVector": ''' + queryVector + ''', "index": "indexName", "exact": true, "limit": {"$numberLong": "1"}, "filter": {"fieldName": {"$ne": "fieldValue"}} } }''') + + where: + vector | queryVector + Vector.int8Vector(new byte[]{127, 7}) | '{"$binary": {"base64": "AwB/Bw==", "subType": "09"}}' + Vector.floatVector(new float[]{127.0f, 7.0f}) | '{"$binary": {"base64": "JwAAAP5CAADgQA==", "subType": "09"}}' + [1.0d, 2.0d] | "[1.0, 2.0]" } def 'should create string representation for simple stages'() { diff --git a/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy b/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy index 9898d0f0569..36da5c61e2d 100644 --- a/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy @@ -180,16 +180,6 @@ class ClusterSettingsSpecification extends Specification { thrown(IllegalArgumentException) } - def 'when srvHost contains less than three parts (host, domain, top-level domain, should throw IllegalArgumentException'() { - when: - def builder = ClusterSettings.builder() - builder.srvHost('foo.bar') - builder.build() - - then: - thrown(IllegalArgumentException) - } - def 'when connection string is applied to builder, all properties should be set'() { when: def settings = ClusterSettings.builder() diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/InitialDnsSeedListDiscoveryProseTest.java b/driver-core/src/test/unit/com/mongodb/internal/connection/InitialDnsSeedListDiscoveryProseTest.java new file mode 100644 index 00000000000..27ed86e7b63 --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/internal/connection/InitialDnsSeedListDiscoveryProseTest.java @@ -0,0 +1,132 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.internal.connection; + +import com.mongodb.ClusterFixture; +import com.mongodb.MongoException; +import com.mongodb.ServerAddress; +import com.mongodb.connection.ClusterConnectionMode; +import com.mongodb.connection.ClusterId; +import com.mongodb.connection.ClusterSettings; +import com.mongodb.connection.ClusterType; +import com.mongodb.connection.ServerSettings; +import com.mongodb.internal.dns.DefaultDnsResolver; +import com.mongodb.internal.dns.DnsResolver; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static java.util.Collections.singletonList; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * See https://github.com/mongodb/specifications/blob/master/source/initial-dns-seedlist-discovery/tests/README.md + */ +class InitialDnsSeedListDiscoveryProseTest { + private static final String SRV_SERVICE_NAME = "mongodb"; + + private DnsMultiServerCluster cluster; + + @AfterEach + void tearDown() { + if (cluster != null) { + cluster.close(); + } + } + + @ParameterizedTest(name = "mongodb+srv://{0} => {1}") + @CsvSource({ + "localhost, test.mongo.localhost", + "mongo.local, test.driver.mongo.local" + }) + @DisplayName("1. Allow SRVs with fewer than 3 '.' separated parts") + void testAllowSRVsWithFewerThanThreeParts(final String srvHost, final String resolvedHost) { + doTest(srvHost, resolvedHost, false); + } + + @ParameterizedTest(name = "mongodb+srv://{0} => {1}") + @CsvSource({ + "localhost, localhost.mongodb", + "mongo.local, test_1.evil.local", + "blogs.mongodb.com, blogs.evil.com" + }) + @DisplayName("2. Throw when return address does not end with SRV domain") + void testThrowWhenReturnAddressDoesnotEndWithSRVDomain(final String srvHost, final String resolvedHost) { + doTest(srvHost, resolvedHost, true); + } + + @ParameterizedTest(name = "mongodb+srv://{0} => {1}") + @CsvSource({ + "localhost, localhost", + "mongo.local, mongo.local" + }) + @DisplayName("3. Throw when return address is identical to SRV hostname and the SRV hostname has fewer than three `.` separated parts") + void testThrowWhenReturnAddressIsIdenticalToSRVHostname(final String srvHost, final String resolvedHost) { + doTest(srvHost, resolvedHost, true); + } + + @ParameterizedTest(name = "mongodb+srv://{0} => {1}") + @CsvSource({ + "localhost, test_1.cluster_1localhost", + "mongo.local, test_1.my_hostmongo.local", + "blogs.mongodb.com, cluster.testmongodb.com" + }) + @DisplayName("4. Throw when return address does not contain '.' separating shared part of domain") + void testThrowWhenReturnAddressDoesnotContainSharedPartOfDomain(final String srvHost, final String resolvedHost) { + doTest(srvHost, resolvedHost, true); + } + + private void doTest(final String srvHost, final String resolvedHost, final boolean throwException) { + final ClusterId clusterId = new ClusterId(); + + final DnsResolver dnsResolver = new DefaultDnsResolver((name, type) -> singletonList(String.format("10 5 27017 %s", + resolvedHost))); + + final DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory = mock(DnsSrvRecordMonitorFactory.class); + when(dnsSrvRecordMonitorFactory.create(eq(srvHost), eq(SRV_SERVICE_NAME), any(DnsSrvRecordInitializer.class))).thenAnswer( + invocation -> new DefaultDnsSrvRecordMonitor(srvHost, SRV_SERVICE_NAME, 10, 10, + invocation.getArgument(2), clusterId, dnsResolver)); + + final ClusterSettings.Builder settingsBuilder = ClusterSettings.builder() + .mode(ClusterConnectionMode.MULTIPLE) + .requiredClusterType(ClusterType.SHARDED) + .srvHost(srvHost); + + final ClusterableServerFactory serverFactory = mock(ClusterableServerFactory.class); + when(serverFactory.getSettings()).thenReturn(ServerSettings.builder().build()); + when(serverFactory.create(any(Cluster.class), any(ServerAddress.class))).thenReturn(mock(ClusterableServer.class)); + + cluster = new DnsMultiServerCluster(clusterId, settingsBuilder.build(), + serverFactory, + dnsSrvRecordMonitorFactory); + + ClusterFixture.sleep(100); + + final MongoException mongoException = cluster.getSrvResolutionException(); + if (throwException) { + Assertions.assertNotNull(mongoException); + } else { + Assertions.assertNull(mongoException); + } + } +} + diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/ChangeStreamsTest.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/ChangeStreamsTest.java index 5a48dc343af..db5537b12d2 100644 --- a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/ChangeStreamsTest.java +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/ChangeStreamsTest.java @@ -48,6 +48,12 @@ final class ChangeStreamsTest extends UnifiedReactiveStreamsTest { "Test that comment is not set on getMore - pre 4.4" ); + private static final List TESTS_WITH_EXTRA_EVENTS = + Arrays.asList( + "Test with document comment", + "Test with string comment" + ); + private static final List REQUIRES_BATCH_CURSOR_CREATION_WAITING = Arrays.asList( "Change Stream should error when an invalid aggregation stage is passed in", @@ -59,6 +65,7 @@ final class ChangeStreamsTest extends UnifiedReactiveStreamsTest { protected void skips(final String fileDescription, final String testDescription) { assumeFalse(ERROR_REQUIRED_FROM_CHANGE_STREAM_INITIALIZATION_TESTS.contains(testDescription)); assumeFalse(EVENT_SENSITIVE_TESTS.contains(testDescription)); + assumeFalse(TESTS_WITH_EXTRA_EVENTS.contains(testDescription)); } @BeforeEach diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/UnifiedServerDiscoveryAndMonitoringTest.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/UnifiedServerDiscoveryAndMonitoringTest.java index 5b12ba14de9..7301a21b372 100644 --- a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/UnifiedServerDiscoveryAndMonitoringTest.java +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/UnifiedServerDiscoveryAndMonitoringTest.java @@ -29,6 +29,6 @@ private static Collection data() throws URISyntaxException, IOExcepti @Override protected void skips(final String fileDescription, final String testDescription) { - com.mongodb.client.unified.UnifiedServerDiscoveryAndMonitoringTest.doSkips(getDefinition()); + com.mongodb.client.unified.UnifiedServerDiscoveryAndMonitoringTest.doSkips(fileDescription, testDescription); } } diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/vector/VectorFunctionalTest.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/vector/VectorFunctionalTest.java new file mode 100644 index 00000000000..f5b8e63f8c3 --- /dev/null +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/vector/VectorFunctionalTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.reactivestreams.client.vector; + +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.vector.AbstractVectorFunctionalTest; +import com.mongodb.reactivestreams.client.MongoClients; +import com.mongodb.reactivestreams.client.syncadapter.SyncMongoClient; + +public class VectorFunctionalTest extends AbstractVectorFunctionalTest { + @Override + protected MongoClient getMongoClient(final MongoClientSettings settings) { + return new SyncMongoClient(MongoClients.create(settings)); + } +} diff --git a/driver-scala/build.gradle b/driver-scala/build.gradle index 4490ed39538..e9e9e15040e 100644 --- a/driver-scala/build.gradle +++ b/driver-scala/build.gradle @@ -19,8 +19,8 @@ archivesBaseName = 'mongo-scala-driver' dependencies { - implementation project(path: ':bson-scala', configuration: 'default') - implementation project(path: ':driver-reactive-streams', configuration: 'default') + api project(path: ':bson-scala', configuration: 'default') + api project(path: ':driver-reactive-streams', configuration: 'default') compileOnly 'com.google.code.findbugs:jsr305:1.3.9' testImplementation project(':driver-sync') diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudTest.java index 5c494452823..22c5a5e3807 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudTest.java @@ -43,6 +43,22 @@ public static void doSkips(final String fileDescription, final String testDescri assumeFalse(testDescription.equals("Aggregate with $out includes read preference for 5.0+ server")); assumeFalse(testDescription.equals("Database-level aggregate with $out includes read preference for 5.0+ server")); } + if (fileDescription.equals("updateOne-sort")) { + assumeFalse(testDescription.equals("UpdateOne with sort option"), "Skipping until JAVA-5622 is implemented"); + assumeFalse(testDescription.equals("updateOne with sort option unsupported (server-side error)"), "Skipping until JAVA-5622 is implemented"); + } + if (fileDescription.equals("replaceOne-sort")) { + assumeFalse(testDescription.equals("ReplaceOne with sort option"), "Skipping until JAVA-5622 is implemented"); + assumeFalse(testDescription.equals("replaceOne with sort option unsupported (server-side error)"), "Skipping until JAVA-5622 is implemented"); + } + if (fileDescription.equals("BulkWrite updateOne-sort")) { + assumeFalse(testDescription.equals("BulkWrite updateOne with sort option"), "Skipping until JAVA-5622 is implemented"); + assumeFalse(testDescription.equals("BulkWrite updateOne with sort option unsupported (server-side error)"), "Skipping until JAVA-5622 is implemented"); + } + if (fileDescription.equals("BulkWrite replaceOne-sort")) { + assumeFalse(testDescription.equals("BulkWrite replaceOne with sort option"), "Skipping until JAVA-5622 is implemented"); + assumeFalse(testDescription.equals("BulkWrite replaceOne with sort option unsupported (server-side error)"), "Skipping until JAVA-5622 is implemented"); + } } @Override diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedRetryableReadsTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedRetryableReadsTest.java index 62712eaab0e..4099191556d 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedRetryableReadsTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedRetryableReadsTest.java @@ -39,6 +39,11 @@ public static void doSkips(final String fileDescription, @SuppressWarnings("unus assumeFalse(fileDescription.equals("listDatabaseObjects-serverErrors")); assumeFalse(fileDescription.equals("listCollectionObjects")); assumeFalse(fileDescription.equals("listCollectionObjects-serverErrors")); + + // JAVA-5224 + assumeFalse(fileDescription.equals("ReadConcernMajorityNotAvailableYet is a retryable read") + && testDescription.equals("Find succeeds on second attempt after ReadConcernMajorityNotAvailableYet"), + "JAVA-5224"); } private static Collection data() throws URISyntaxException, IOException { diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedRetryableWritesTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedRetryableWritesTest.java index 794a027ebaf..ffd44441c81 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedRetryableWritesTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedRetryableWritesTest.java @@ -41,6 +41,8 @@ public static void doSkips(final String description) { if (isDiscoverableReplicaSet() && serverVersionLessThan(4, 4)) { assumeFalse(description.equals("RetryableWriteError label is added based on writeConcernError in pre-4.4 mongod response")); } + assumeFalse(description.contains("client bulkWrite"), "JAVA-4586"); + assumeFalse(description.contains("client.clientBulkWrite"), "JAVA-4586"); } private static Collection data() throws URISyntaxException, IOException { diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedServerDiscoveryAndMonitoringTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedServerDiscoveryAndMonitoringTest.java index c384a50967e..fbdc5f4ea3e 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedServerDiscoveryAndMonitoringTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedServerDiscoveryAndMonitoringTest.java @@ -16,8 +16,6 @@ package com.mongodb.client.unified; -import org.bson.BsonDocument; -import org.bson.BsonString; import org.junit.jupiter.params.provider.Arguments; import java.io.IOException; @@ -33,14 +31,30 @@ private static Collection data() throws URISyntaxException, IOExcepti @Override protected void skips(final String fileDescription, final String testDescription) { - doSkips(getDefinition()); + doSkips(fileDescription, testDescription); } - public static void doSkips(final BsonDocument definition) { - String description = definition.getString("description", new BsonString("")).getValue(); - assumeFalse(description.equals("connect with serverMonitoringMode=auto >=4.4"), + public static void doSkips(final String fileDescription, final String testDescription) { + assumeFalse(testDescription.equals("connect with serverMonitoringMode=auto >=4.4"), "Skipping because our server monitoring events behave differently for now"); - assumeFalse(description.equals("connect with serverMonitoringMode=stream >=4.4"), + assumeFalse(testDescription.equals("connect with serverMonitoringMode=stream >=4.4"), "Skipping because our server monitoring events behave differently for now"); + + assumeFalse(fileDescription.equals("standalone-logging"), "Skipping until JAVA-4770 is implemented"); + assumeFalse(fileDescription.equals("replicaset-logging"), "Skipping until JAVA-4770 is implemented"); + assumeFalse(fileDescription.equals("sharded-logging"), "Skipping until JAVA-4770 is implemented"); + assumeFalse(fileDescription.equals("loadbalanced-logging"), "Skipping until JAVA-4770 is implemented"); + + assumeFalse(fileDescription.equals("standalone-emit-topology-description-changed-before-close"), + "Skipping until JAVA-5229 is implemented"); + assumeFalse(fileDescription.equals("replicaset-emit-topology-description-changed-before-close"), + "Skipping until JAVA-5229 is implemented"); + assumeFalse(fileDescription.equals("sharded-emit-topology-description-changed-before-close"), + "Skipping until JAVA-5229 is implemented"); + assumeFalse(fileDescription.equals("loadbalanced-emit-topology-description-changed-before-close"), + "Skipping until JAVA-5229 is implemented"); + + assumeFalse(testDescription.equals("poll waits after successful heartbeat"), "Skipping until JAVA-5564 is implemented"); + assumeFalse(fileDescription.equals("interruptInUse"), "Skipping until JAVA-4536 is implemented"); } } diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestValidator.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestValidator.java index ecb04294bf8..0626ab89e09 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestValidator.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestValidator.java @@ -31,6 +31,13 @@ protected void skips(final String fileDescription, final String testDescription) assumeFalse(testDescription.equals("InsertOne fails after multiple retryable writeConcernErrors") && serverVersionLessThan(4, 4), "MongoDB releases prior to 4.4 incorrectly add errorLabels as a field within the writeConcernError document " + "instead of as a top-level field. Rather than handle that in code, we skip the test on older server versions."); + // Feature to be implemented in scope of JAVA-5389 + assumeFalse(fileDescription.equals("expectedEventsForClient-topologyDescriptionChangedEvent")); + // Feature to be implemented in scope JAVA-4862 + assumeFalse(fileDescription.equals("entity-commandCursor")); + // To be investigated in JAVA-5631 + assumeFalse(fileDescription.equals("kmsProviders-explicit_kms_credentials")); + assumeFalse(fileDescription.equals("kmsProviders-mixed_kms_credential_fields")); } private static Collection data() throws URISyntaxException, IOException { diff --git a/driver-sync/src/test/functional/com/mongodb/client/vector/AbstractVectorFunctionalTest.java b/driver-sync/src/test/functional/com/mongodb/client/vector/AbstractVectorFunctionalTest.java new file mode 100644 index 00000000000..c3edf6983da --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/vector/AbstractVectorFunctionalTest.java @@ -0,0 +1,346 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client.vector; + +import com.mongodb.MongoClientSettings; +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.client.Fixture; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.OperationTest; +import org.bson.BsonBinary; +import org.bson.BsonBinarySubType; +import org.bson.BsonInvalidOperationException; +import org.bson.Document; +import org.bson.Float32Vector; +import org.bson.Int8Vector; +import org.bson.PackedBitVector; +import org.bson.Vector; +import org.bson.codecs.configuration.CodecRegistry; +import org.bson.codecs.pojo.PojoCodecProvider; +import org.bson.types.Binary; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static com.mongodb.MongoClientSettings.getDefaultCodecRegistry; +import static org.bson.Vector.DataType.FLOAT32; +import static org.bson.Vector.DataType.INT8; +import static org.bson.Vector.DataType.PACKED_BIT; +import static org.bson.codecs.configuration.CodecRegistries.fromProviders; +import static org.bson.codecs.configuration.CodecRegistries.fromRegistries; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public abstract class AbstractVectorFunctionalTest extends OperationTest { + + private static final byte VECTOR_SUBTYPE = BsonBinarySubType.VECTOR.getValue(); + private static final String FIELD_VECTOR = "vector"; + private static final CodecRegistry CODEC_REGISTRY = fromRegistries(getDefaultCodecRegistry(), + fromProviders(PojoCodecProvider + .builder() + .automatic(true).build())); + private MongoCollection documentCollection; + + private MongoClient mongoClient; + + @BeforeEach + public void setUp() { + super.beforeEach(); + mongoClient = getMongoClient(getMongoClientSettingsBuilder() + .codecRegistry(CODEC_REGISTRY) + .build()); + documentCollection = mongoClient + .getDatabase(getDatabaseName()) + .getCollection(getCollectionName()); + } + + @AfterEach + @SuppressWarnings("try") + public void afterEach() { + try (MongoClient ignore = mongoClient) { + super.afterEach(); + } + } + + private static MongoClientSettings.Builder getMongoClientSettingsBuilder() { + return Fixture.getMongoClientSettingsBuilder() + .readConcern(ReadConcern.MAJORITY) + .writeConcern(WriteConcern.MAJORITY) + .readPreference(ReadPreference.primary()); + } + + protected abstract MongoClient getMongoClient(MongoClientSettings settings); + + @ParameterizedTest + @ValueSource(bytes = {-1, 1, 2, 3, 4, 5, 6, 7, 8}) + void shouldThrowExceptionForInvalidPackedBitArrayPaddingWhenDecodeEmptyVector(final byte invalidPadding) { + //given + Binary invalidVector = new Binary(VECTOR_SUBTYPE, new byte[]{PACKED_BIT.getValue(), invalidPadding}); + documentCollection.insertOne(new Document(FIELD_VECTOR, invalidVector)); + + // when & then + BsonInvalidOperationException exception = Assertions.assertThrows(BsonInvalidOperationException.class, ()-> { + findExactlyOne(documentCollection) + .get(FIELD_VECTOR, Vector.class); + }); + assertEquals("Padding must be 0 if vector is empty, but found: " + invalidPadding, exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(bytes = {-1, 1}) + void shouldThrowExceptionForInvalidFloat32Padding(final byte invalidPadding) { + // given + Binary invalidVector = new Binary(VECTOR_SUBTYPE, new byte[]{FLOAT32.getValue(), invalidPadding, 10, 20, 30, 40}); + documentCollection.insertOne(new Document(FIELD_VECTOR, invalidVector)); + + // when & then + BsonInvalidOperationException exception = Assertions.assertThrows(BsonInvalidOperationException.class, ()-> { + findExactlyOne(documentCollection) + .get(FIELD_VECTOR, Vector.class); + }); + assertEquals("Padding must be 0 for FLOAT32 data type, but found: " + invalidPadding, exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(bytes = {-1, 1}) + void shouldThrowExceptionForInvalidInt8Padding(final byte invalidPadding) { + // given + Binary invalidVector = new Binary(VECTOR_SUBTYPE, new byte[]{INT8.getValue(), invalidPadding, 10, 20, 30, 40}); + documentCollection.insertOne(new Document(FIELD_VECTOR, invalidVector)); + + // when & then + BsonInvalidOperationException exception = Assertions.assertThrows(BsonInvalidOperationException.class, ()-> { + findExactlyOne(documentCollection) + .get(FIELD_VECTOR, Vector.class); + }); + assertEquals("Padding must be 0 for INT8 data type, but found: " + invalidPadding, exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(bytes = {-1, 8}) + void shouldThrowExceptionForInvalidPackedBitPadding(final byte invalidPadding) { + // given + Binary invalidVector = new Binary(VECTOR_SUBTYPE, new byte[]{PACKED_BIT.getValue(), invalidPadding, 10, 20, 30, 40}); + documentCollection.insertOne(new Document(FIELD_VECTOR, invalidVector)); + + // when & then + BsonInvalidOperationException exception = Assertions.assertThrows(BsonInvalidOperationException.class, ()-> { + findExactlyOne(documentCollection) + .get(FIELD_VECTOR, Vector.class); + }); + assertEquals("Padding must be between 0 and 7 bits, but found: " + invalidPadding, exception.getMessage()); + } + + private static Stream provideValidVectors() { + return Stream.of( + Vector.floatVector(new float[]{1.1f, 2.2f, 3.3f}), + Vector.int8Vector(new byte[]{10, 20, 30, 40}), + Vector.packedBitVector(new byte[]{(byte) 0b10101010, (byte) 0b01010101}, (byte) 3) + ); + } + + @ParameterizedTest + @MethodSource("provideValidVectors") + void shouldStoreAndRetrieveValidVector(final Vector expectedVector) { + // Given + Document documentToInsert = new Document(FIELD_VECTOR, expectedVector) + .append("otherField", 1); // to test that the next field is not affected + documentCollection.insertOne(documentToInsert); + + // when & then + Vector actualVector = findExactlyOne(documentCollection) + .get(FIELD_VECTOR, Vector.class); + + assertEquals(expectedVector, actualVector); + } + + @ParameterizedTest + @MethodSource("provideValidVectors") + void shouldStoreAndRetrieveValidVectorWithBsonBinary(final Vector expectedVector) { + // Given + Document documentToInsert = new Document(FIELD_VECTOR, new BsonBinary(expectedVector)); + documentCollection.insertOne(documentToInsert); + + // when & then + Vector actualVector = findExactlyOne(documentCollection) + .get(FIELD_VECTOR, Vector.class); + + assertEquals(actualVector, actualVector); + } + + @Test + void shouldStoreAndRetrieveValidVectorWithFloatVectorPojo() { + // given + MongoCollection floatVectorPojoMongoCollection = mongoClient + .getDatabase(getDatabaseName()) + .getCollection(getCollectionName()).withDocumentClass(FloatVectorPojo.class); + Float32Vector vector = Vector.floatVector(new float[]{1.1f, 2.2f, 3.3f}); + + // whe + floatVectorPojoMongoCollection.insertOne(new FloatVectorPojo(vector)); + FloatVectorPojo floatVectorPojo = floatVectorPojoMongoCollection.find().first(); + + // then + Assertions.assertNotNull(floatVectorPojo); + assertEquals(vector, floatVectorPojo.getVector()); + } + + @Test + void shouldStoreAndRetrieveValidVectorWithInt8VectorPojo() { + // given + MongoCollection floatVectorPojoMongoCollection = mongoClient + .getDatabase(getDatabaseName()) + .getCollection(getCollectionName()).withDocumentClass(Int8VectorPojo.class); + Int8Vector vector = Vector.int8Vector(new byte[]{10, 20, 30, 40}); + + // when + floatVectorPojoMongoCollection.insertOne(new Int8VectorPojo(vector)); + Int8VectorPojo int8VectorPojo = floatVectorPojoMongoCollection.find().first(); + + // then + Assertions.assertNotNull(int8VectorPojo); + assertEquals(vector, int8VectorPojo.getVector()); + } + + @Test + void shouldStoreAndRetrieveValidVectorWithPackedBitVectorPojo() { + // given + MongoCollection floatVectorPojoMongoCollection = mongoClient + .getDatabase(getDatabaseName()) + .getCollection(getCollectionName()).withDocumentClass(PackedBitVectorPojo.class); + + PackedBitVector vector = Vector.packedBitVector(new byte[]{(byte) 0b10101010, (byte) 0b01010101}, (byte) 3); + + // when + floatVectorPojoMongoCollection.insertOne(new PackedBitVectorPojo(vector)); + PackedBitVectorPojo packedBitVectorPojo = floatVectorPojoMongoCollection.find().first(); + + // then + Assertions.assertNotNull(packedBitVectorPojo); + assertEquals(vector, packedBitVectorPojo.getVector()); + } + + @ParameterizedTest + @MethodSource("provideValidVectors") + void shouldStoreAndRetrieveValidVectorWithGenericVectorPojo(final Vector actualVector) { + // given + MongoCollection floatVectorPojoMongoCollection = mongoClient + .getDatabase(getDatabaseName()) + .getCollection(getCollectionName()).withDocumentClass(VectorPojo.class); + + // when + floatVectorPojoMongoCollection.insertOne(new VectorPojo(actualVector)); + VectorPojo vectorPojo = floatVectorPojoMongoCollection.find().first(); + + //then + Assertions.assertNotNull(vectorPojo); + assertEquals(actualVector, vectorPojo.getVector()); + } + + private Document findExactlyOne(final MongoCollection collection) { + List documents = new ArrayList<>(); + collection.find().into(documents); + assertEquals(1, documents.size(), "Expected exactly one document, but found: " + documents.size()); + return documents.get(0); + } + + public static class VectorPojo { + private Vector vector; + + public VectorPojo() { + } + + public VectorPojo(final Vector vector) { + this.vector = vector; + } + + public Vector getVector() { + return vector; + } + + public void setVector(final Vector vector) { + this.vector = vector; + } + } + + public static class Int8VectorPojo { + private Int8Vector vector; + + public Int8VectorPojo() { + } + + public Int8VectorPojo(final Int8Vector vector) { + this.vector = vector; + } + + public Vector getVector() { + return vector; + } + + public void setVector(final Int8Vector vector) { + this.vector = vector; + } + } + + public static class PackedBitVectorPojo { + private PackedBitVector vector; + + public PackedBitVectorPojo() { + } + + public PackedBitVectorPojo(final PackedBitVector vector) { + this.vector = vector; + } + + public Vector getVector() { + return vector; + } + + public void setVector(final PackedBitVector vector) { + this.vector = vector; + } + } + + public static class FloatVectorPojo { + private Float32Vector vector; + + public FloatVectorPojo() { + } + + public FloatVectorPojo(final Float32Vector vector) { + this.vector = vector; + } + + public Vector getVector() { + return vector; + } + + public void setVector(final Float32Vector vector) { + this.vector = vector; + } + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/client/vector/VectorFunctionalTest.java b/driver-sync/src/test/functional/com/mongodb/client/vector/VectorFunctionalTest.java new file mode 100644 index 00000000000..63d756a8f35 --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/vector/VectorFunctionalTest.java @@ -0,0 +1,28 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client.vector; + +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; + +public class VectorFunctionalTest extends AbstractVectorFunctionalTest { + @Override + protected MongoClient getMongoClient(final MongoClientSettings settings) { + return MongoClients.create(settings); + } +} diff --git a/graalvm-native-image-app/build.gradle b/graalvm-native-image-app/build.gradle index 713b8c29a1a..b3d7335f9d9 100644 --- a/graalvm-native-image-app/build.gradle +++ b/graalvm-native-image-app/build.gradle @@ -14,6 +14,8 @@ * limitations under the License. */ +// Note requires a Gradle project flag `-PincludeGraalvm` (see settings.gradle). + plugins { id 'application' id 'org.graalvm.buildtools.native' version '0.9.23' diff --git a/graalvm-native-image-app/readme.md b/graalvm-native-image-app/readme.md index a659b7d1c07..c47a9829851 100644 --- a/graalvm-native-image-app/readme.md +++ b/graalvm-native-image-app/readme.md @@ -47,12 +47,12 @@ you need to inform Gradle about that location as specified in https://docs.gradl Assuming that your MongoDB deployment is accessible at `mongodb://localhost:27017`, run from the driver project root directory: -| # | Command | Description | -|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| -| 0 | `env JAVA_HOME="${JDK17}" ./gradlew -PjavaVersion=21 :graalvm-native-image-app:nativeCompile` | Build the application relying on the reachability metadata stored in `graalvm-native-image-app/src/main/resources/META-INF/native-image`. | -| 1 | `env JAVA_HOME="${JDK17}" ./gradlew clean && env JAVA_HOME=${JDK21_GRAALVM} ./gradlew -PjavaVersion=21 -Pagent :graalvm-native-image-app:run && env JAVA_HOME=${JDK21_GRAALVM} ./gradlew :graalvm-native-image-app:metadataCopy` | Collect the reachability metadata and update the files storing it. Do this before building the application only if building fails otherwise. | -| 2 | `./graalvm-native-image-app/build/native/nativeCompile/NativeImageApp` | Run the application that has been built. | -| 3 | `env JAVA_HOME="${JDK17}" ./gradlew -PjavaVersion=21 :graalvm-native-image-app:nativeRun` | Run the application using Gradle, build it if necessary relying on the stored reachability metadata. | +| # | Command | Description | +|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| 0 | `env JAVA_HOME="${JDK17}" ./gradlew -PincludeGraalvm -PjavaVersion=21 :graalvm-native-image-app:nativeCompile` | Build the application relying on the reachability metadata stored in `graalvm-native-image-app/src/main/resources/META-INF/native-image`. | +| 1 | `env JAVA_HOME="${JDK17}" ./gradlew clean && env JAVA_HOME=${JDK21_GRAALVM} ./gradlew -PincludeGraalvm -PjavaVersion=21 -Pagent :graalvm-native-image-app:run && env JAVA_HOME=${JDK21_GRAALVM} ./gradlew -PincludeGraalvm :graalvm-native-image-app:metadataCopy` | Collect the reachability metadata and update the files storing it. Do this before building the application only if building fails otherwise. | +| 2 | `./graalvm-native-image-app/build/native/nativeCompile/NativeImageApp` | Run the application that has been built. | +| 3 | `env JAVA_HOME="${JDK17}" ./gradlew -PincludeGraalvm -PjavaVersion=21 :graalvm-native-image-app:nativeRun` | Run the application using Gradle, build it if necessary relying on the stored reachability metadata. | #### Specifying a custom connection string diff --git a/gradle.properties b/gradle.properties index e31c63fbd62..12f1750c442 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,8 +16,8 @@ org.gradle.daemon=true org.gradle.jvmargs=-Duser.country=US -Duser.language=en -scalaVersions=2.11.12,2.12.15,2.13.6 -defaultScalaVersions=2.13.6 +scalaVersions=2.11.12,2.12.20,2.13.15 +defaultScalaVersions=2.13.15 runOnceTasks=clean,release org.gradle.java.installations.auto-download=false org.gradle.java.installations.fromEnv=JDK8,JDK11,JDK17,JDK21,JDK21_GRAALVM diff --git a/gradle/javaToolchain.gradle b/gradle/javaToolchain.gradle index 187b143eea6..f1c779dab33 100644 --- a/gradle/javaToolchain.gradle +++ b/gradle/javaToolchain.gradle @@ -80,7 +80,7 @@ allprojects { options.encoding = "UTF-8" options.release.set(17) } - } else if (project == project(':graalvm-native-image-app')) { + } else if (project.name == 'graalvm-native-image-app') { tasks.withType(JavaCompile) { options.encoding = 'UTF-8' options.release.set(DEFAULT_JDK_VERSION) diff --git a/gradle/javadoc.gradle b/gradle/javadoc.gradle index 8d425b693b8..b986747c647 100644 --- a/gradle/javadoc.gradle +++ b/gradle/javadoc.gradle @@ -17,8 +17,9 @@ import static org.gradle.util.CollectionUtils.single */ -def projectsThatDoNotPublishJavaDocs = project(":util").allprojects + project(":driver-benchmarks") + project("driver-workload-executor") + project("driver-lambda") + project(":graalvm-native-image-app") -def javaMainProjects = subprojects - projectsThatDoNotPublishJavaDocs +def projectNamesThatDoNotPublishJavaDocs =["driver-benchmarks", "driver-lambda", "driver-workload-executor", "graalvm-native-image-app", "util", + "spock", "taglets"] +def javaMainProjects = subprojects.findAll { !projectNamesThatDoNotPublishJavaDocs.contains(it.name) } task docs { dependsOn javaMainProjects.collect { it.tasks.withType(Javadoc) + it.tasks.withType(ScalaDoc) } diff --git a/gradle/publish.gradle b/gradle/publish.gradle index f72773c5ad7..9add25f9261 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -72,8 +72,10 @@ ext { } } -def projectsNotPublishedToMaven = project(":util").allprojects + project(":driver-benchmarks") + project("driver-workload-executor") + project("driver-lambda") + project(":graalvm-native-image-app") -def publishedProjects = subprojects - projectsNotPublishedToMaven + +def projectNamesNotToBePublished = ["driver-benchmarks", "driver-lambda", "driver-workload-executor", "graalvm-native-image-app", "util", + "spock", "taglets"] +def publishedProjects = subprojects.findAll { !projectNamesNotToBePublished.contains(it.name) } def scalaProjects = publishedProjects.findAll { it.name.contains('scala') } def javaProjects = publishedProjects - scalaProjects def projectsWithManifest = publishedProjects.findAll {it.name != 'driver-legacy' } diff --git a/settings.gradle b/settings.gradle index b1c5e185d37..4ebbb10c4e0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -32,4 +32,7 @@ include ':driver-scala' include ':mongodb-crypt' include 'util:spock' include 'util:taglets' -include ':graalvm-native-image-app' + +if(hasProperty("includeGraalvm")) { + include ':graalvm-native-image-app' +} pFad - Phonifier reborn

    Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

    Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


    Alternative Proxies:

    Alternative Proxy

    pFad Proxy

    pFad v3 Proxy

    pFad v4 Proxy