diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java index 35189aef45..beea019b86 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java @@ -267,6 +267,11 @@ public void setUp( boolean skip = testDef.wasAssignedModifier(Modifier.SKIP); assumeFalse(skip, "Skipping test"); + + if (testDef.hasTransformations()) { + this.entitiesArray = entitiesArray.clone(); + testDef.applyTransformations(this.entitiesArray, definition); + } } skips(fileDescription, testDescription); @@ -289,7 +294,7 @@ public void setUp( startingClusterTime = addInitialDataAndGetClusterTime(); - entities.init(entitiesArray, startingClusterTime, + entities.init(this.entitiesArray, startingClusterTime, fileDescription != null && PRESTART_POOL_ASYNC_WORK_MANAGER_FILE_DESCRIPTIONS.contains(fileDescription), this::createMongoClient, this::createGridFSBucket, diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java index 286e6f525a..d1f1a194f8 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java @@ -17,10 +17,18 @@ package com.mongodb.client.unified; import com.mongodb.ClusterFixture; +import com.mongodb.lang.Nullable; +import org.bson.BsonArray; +import org.bson.BsonDocument; +import org.bson.BsonInt32; +import org.bson.BsonValue; +import org.bson.diagnostics.Logger; +import org.bson.diagnostics.Loggers; import org.opentest4j.AssertionFailedError; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.function.Function; import java.util.function.Supplier; @@ -39,6 +47,8 @@ import static java.lang.String.format; public final class UnifiedTestModifications { + private static final Logger LOGGER = Loggers.getLogger("UnifiedTestModifications"); + public static void applyCustomizations(final TestDef def) { // change-streams @@ -104,10 +114,6 @@ public static void applyCustomizations(final TestDef def) { .test("client-side-operations-timeout", "timeoutMS behaves correctly for tailable awaitData cursors", "apply maxAwaitTimeMS if less than remaining timeout"); - def.skipJira("https://jira.mongodb.org/browse/JAVA-5839") - .test("client-side-operations-timeout", "timeoutMS behaves correctly for GridFS download operations", - "timeoutMS applied to entire download, not individual parts"); - def.skipJira("https://jira.mongodb.org/browse/JAVA-5491") .when(() -> !serverVersionLessThan(8, 3)) .test("client-side-operations-timeout", "operations ignore deprecated timeout options if timeoutMS is set", @@ -320,6 +326,15 @@ public static void applyCustomizations(final TestDef def) { def.skipJira("https://jira.mongodb.org/browse/JAVA-5689") .file("gridfs", "gridfs-deleteByName") .file("gridfs", "gridfs-renameByName"); + def.transform("JAVA-5839: Bump blocking/timeout to avoid CI latency failures", + (entitiesArray, definition) -> { + findAndSetInt(entitiesArray, "client.uriOptions.timeoutMS", 250); + findAndSetInt(definition.getArray("operations"), + "arguments.failPoint.data.blockTimeMS", 200); + }) + .test("client-side-operations-timeout", + "timeoutMS behaves correctly for GridFS download operations", + "timeoutMS applied to entire download, not individual parts"); // Skip all rawData based tests def.skipJira("https://jira.mongodb.org/browse/JAVA-5830 rawData support only added to Go and Node") @@ -505,30 +520,69 @@ public static void applyCustomizations(final TestDef def) { .file("unified-test-format/tests/valid-fail", "operator-matchAsDocument"); } + /** + * Searches each document in {@code array} for a nested int field specified + * by a dot-separated {@code path}, and replaces it with {@code newValue}. + * Logs each replacement. Silently skips documents where the path does not + * exist or the intermediate keys are absent. + * + *

Example: {@code findAndSetInt(entitiesArray, "client.uriOptions.timeoutMS", 250)} + * walks each element looking for {@code element.client.uriOptions.timeoutMS}.

+ * + * @param array the array to search + * @param path dot-separated path to an int field + * @param newValue the replacement value + */ + static void findAndSetInt(final BsonArray array, final String path, final int newValue) { + String[] segments = path.split("\\."); + for (BsonValue element : array) { + if (!element.isDocument()) { + continue; + } + BsonDocument current = element.asDocument(); + boolean found = true; + for (int i = 0; i < segments.length - 1; i++) { + if (current.containsKey(segments[i]) && current.get(segments[i]).isDocument()) { + current = current.getDocument(segments[i]); + } else { + found = false; + break; + } + } + String leafKey = segments[segments.length - 1]; + if (found && current.containsKey(leafKey) && current.get(leafKey).isInt32()) { + int oldValue = current.getInt32(leafKey).getValue(); + LOGGER.info(format(" %s: %d -> %d", leafKey, oldValue, newValue)); + current.put(leafKey, new BsonInt32(newValue)); + } + } + } + private UnifiedTestModifications() { } - public static TestDef testDef(final String dir, final String file, final String test, final boolean reactive, - final UnifiedTest.Language language) { - return new TestDef(dir, file, test, reactive, language); + public static TestDef testDef(final String directory, final String fileDescription, final String testDescription, + final boolean reactive, final UnifiedTest.Language language) { + return new TestDef(directory, fileDescription, testDescription, reactive, language); } public static final class TestDef { - private final String dir; - private final String file; - private final String test; + private final String directory; + private final String fileDescription; + private final String testDescription; private final boolean reactive; private final UnifiedTest.Language language; private final List modifiers = new ArrayList<>(); + private final List transformers = new ArrayList<>(); private Function matchesThrowable; - private TestDef(final String dir, final String file, final String test, final boolean reactive, - final UnifiedTest.Language language) { - this.dir = assertNotNull(dir); - this.file = assertNotNull(file); - this.test = assertNotNull(test); + private TestDef(final String directory, final String fileDescription, final String testDescription, + final boolean reactive, final UnifiedTest.Language language) { + this.directory = assertNotNull(directory); + this.fileDescription = assertNotNull(fileDescription); + this.testDescription = assertNotNull(testDescription); this.reactive = reactive; this.language = assertNotNull(language); } @@ -538,9 +592,9 @@ public String toString() { return "TestDef{" + "modifiers=" + modifiers + ", reactive=" + reactive - + ", test='" + test + '\'' - + ", file='" + file + '\'' - + ", dir='" + dir + '\'' + + ", testDescription='" + testDescription + '\'' + + ", fileDescription='" + fileDescription + '\'' + + ", directory='" + directory + '\'' + '}'; } @@ -614,6 +668,34 @@ public TestApplicator retryReactive(final String reason) { .when(this::isReactive); } + /** + * Registers a transformation that mutates the test's entity and + * definition data before execution. The reason is logged when the + * transformation is registered for a matching test. + * + * @param reason why the transformation is needed + * @param transformer the transformation to apply + */ + public TestApplicator transform(final String reason, final TestTransformer transformer) { + return new TestApplicator(this, reason, transformer); + } + + /** + * Applies all registered transformations to the test data. + */ + public void applyTransformations(final BsonArray entitiesArray, final BsonDocument definition) { + for (TestTransformer transformer : transformers) { + transformer.transform(entitiesArray, definition); + } + } + + /** + * Returns true if any transformations have been registered. + */ + public boolean hasTransformations() { + return !transformers.isEmpty(); + } + public TestApplicator modify(final Modifier... modifiers) { return new TestApplicator(this, null, modifiers); } @@ -648,18 +730,34 @@ public static final class TestApplicator { private final List modifiersToApply; private Function matchesThrowable; + @Nullable + private final TestTransformer transformer; + @Nullable + private final String reason; private TestApplicator( final TestDef testDef, - final String reason, + @Nullable final String reason, final Modifier... modifiersToApply) { this.testDef = testDef; + this.reason = reason; this.modifiersToApply = Arrays.asList(modifiersToApply); + this.transformer = null; if (this.modifiersToApply.contains(SKIP) || this.modifiersToApply.contains(RETRY)) { assertNotNull(reason); } } + private TestApplicator( + final TestDef testDef, + final String reason, + final TestTransformer transformer) { + this.testDef = testDef; + this.reason = assertNotNull(reason); + this.modifiersToApply = Collections.emptyList(); + this.transformer = assertNotNull(transformer); + } + private TestApplicator onMatch(final boolean match) { matchWasPerformed = true; if (precondition != null && !precondition.get()) { @@ -668,6 +766,11 @@ private TestApplicator onMatch(final boolean match) { if (match) { this.testDef.modifiers.addAll(this.modifiersToApply); this.testDef.matchesThrowable = this.matchesThrowable; + if (this.transformer != null) { + LOGGER.info("Registered transformation for test [" + + testDef.testDescription + "]: " + reason); + this.testDef.transformers.add(this.transformer); + } } return this; } @@ -675,59 +778,59 @@ private TestApplicator onMatch(final boolean match) { /** * Applies to all tests in directory. * - * @param dir the directory name + * @param directory the directory name * @return this */ - public TestApplicator directory(final String dir) { - boolean match = (dir).equals(testDef.dir); + public TestApplicator directory(final String directory) { + boolean match = (directory).equals(testDef.directory); return onMatch(match); } /** * Applies to all tests in file under the directory. * - * @param dir the directory name - * @param file the test file's "description" field + * @param directory the directory name + * @param fileDescription the test file's "description" field * @return this */ - public TestApplicator file(final String dir, final String file) { - boolean match = (dir).equals(testDef.dir) - && file.equals(testDef.file); + public TestApplicator file(final String directory, final String fileDescription) { + boolean match = (directory).equals(testDef.directory) + && fileDescription.equals(testDef.fileDescription); return onMatch(match); } /** - * Applies to the test where dir, file, and test match. + * Applies to the test where directory, fileDescription, and testDescription match. * - * @param dir the directory name - * @param file the test file's "description" field - * @param test the individual test's "description" field + * @param directory the directory name + * @param fileDescription the test file's "description" field + * @param testDescription the individual test's "description" field * @return this */ - public TestApplicator test(final String dir, final String file, final String test) { - boolean match = testDef.dir.equals(dir) - && testDef.file.equals(file) - && testDef.test.equals(test); + public TestApplicator test(final String directory, final String fileDescription, final String testDescription) { + boolean match = testDef.directory.equals(directory) + && testDef.fileDescription.equals(fileDescription) + && testDef.testDescription.equals(testDescription); return onMatch(match); } /** * Utility method: emit replacement to standard out. * - * @param dir the directory name - * @param fragment the substring to check in the test "description" field + * @param directory the directory name + * @param fragment the substring to check in the test "description" field * @return this */ - public TestApplicator testContains(final String dir, final String fragment) { - boolean match = (dir).equals(testDef.dir) - && testDef.test.contains(fragment); + public TestApplicator testContains(final String directory, final String fragment) { + boolean match = (directory).equals(testDef.directory) + && testDef.testDescription.contains(fragment); if (match) { System.out.printf( "!!! REPLACE %s WITH: .test(\"%s\", \"%s\", \"%s\")%n", fragment, - testDef.dir, - testDef.file, - testDef.test); + testDef.directory, + testDef.fileDescription, + testDef.testDescription); } return this; } @@ -735,16 +838,16 @@ public TestApplicator testContains(final String dir, final String fragment) { /** * Utility method: emit file info to standard out * - * @param dir the directory name - * @param test the individual test's "description" field + * @param directory the directory name + * @param testDescription the individual test's "description" field * @return this */ - public TestApplicator debug(final String dir, final String test) { - boolean match = testDef.test.equals(test); + public TestApplicator debug(final String directory, final String testDescription) { + boolean match = testDef.testDescription.equals(testDescription); if (match) { System.out.printf( "!!! ADD: \"%s\", \"%s\", \"%s\"%n", - testDef.dir, testDef.file, test); + testDef.directory, testDef.fileDescription, testDescription); } return this; } @@ -788,6 +891,16 @@ public TestApplicator whenFailureContains(final String messageFragment) { } + /** + * A transformation that mutates the test's entity array and/or definition + * before execution. Used to adjust spec test values (e.g., timeouts) that + * are too tight for CI environments. + */ + @FunctionalInterface + public interface TestTransformer { + void transform(BsonArray entitiesArray, BsonDocument definition); + } + public enum Modifier { /** * Reactive only.