Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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.
*
* <p>Example: {@code findAndSetInt(entitiesArray, "client.uriOptions.timeoutMS", 250)}
* walks each element looking for {@code element.client.uriOptions.timeoutMS}.</p>
*
* @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) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a value in asserting the expected original value? this will make sure if the upstream test changes to a more favourable value maybe the transformation is no longer needed ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we generalise this method for all numbers? and use isNumber below?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we refactor this to include the reason ? this will make the log statement below (LOGGER.info) more highly informative

Ex:

 String reason = "JAVA-5839: Bump blocking/timeout to avoid CI latency failures";
def.transform(reason,
         (entitiesArray, definition) -> {
             findAndSetInt(reason, entitiesArray, "client.uriOptions.timeoutMS", 250);
             findAndSetInt(reason, definition.getArray("operations"), "arguments.failPoint.data.blockTimeMS", 200);
         })

Or infer it from TestApplicator

// TestDef ...
public void applyTransformations(final BsonArray entitiesArray, final BsonDocument definition) {
         for (Transformation t : transformations) {
             t.transformer.transform(t.reason, entitiesArray, definition);
         }
     }

//  lambda

def.transform("JAVA-5839: Bump blocking/timeout to avoid CI latency failures",
         (reason, entitiesArray, definition) -> {
             findAndSetInt(reason, entitiesArray, "client.uriOptions.timeoutMS", 250);
             findAndSetInt(reason, definition.getArray("operations"), "arguments.failPoint.data.blockTimeMS", 200);
         })

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()) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transformation is skipped if the value is BsonInt64 ? (maybe related to https://github.com/mongodb/mongo-java-driver/pull/1991/changes#r3356666027)

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<Modifier> modifiers = new ArrayList<>();
private final List<TestTransformer> transformers = new ArrayList<>();
private Function<Throwable, Boolean> 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);
}
Expand All @@ -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 + '\''
+ '}';
}

Expand Down Expand Up @@ -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.
*
Comment thread
rozza marked this conversation as resolved.
* @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);
}
Expand Down Expand Up @@ -648,18 +730,34 @@ public static final class TestApplicator {

private final List<Modifier> modifiersToApply;
private Function<Throwable, Boolean> 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()) {
Expand All @@ -668,83 +766,88 @@ 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;
}

/**
* 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;
}

/**
* 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;
}
Expand Down Expand Up @@ -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.
Expand Down