-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Retry flaky unified tests #1565
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,27 +21,27 @@ | |
import com.mongodb.MongoNamespace; | ||
import com.mongodb.ReadPreference; | ||
import com.mongodb.UnixServerAddress; | ||
import com.mongodb.client.unified.UnifiedTestModifications.TestDef; | ||
import com.mongodb.event.TestServerMonitorListener; | ||
import com.mongodb.internal.logging.LogMessage; | ||
import com.mongodb.logging.TestLoggingInterceptor; | ||
import com.mongodb.WriteConcern; | ||
import com.mongodb.client.ClientSession; | ||
import com.mongodb.client.MongoClient; | ||
import com.mongodb.client.MongoDatabase; | ||
import com.mongodb.client.gridfs.GridFSBucket; | ||
import com.mongodb.client.model.Filters; | ||
import com.mongodb.client.test.CollectionHelper; | ||
import com.mongodb.client.unified.UnifiedTestModifications.TestDef; | ||
import com.mongodb.client.vault.ClientEncryption; | ||
import com.mongodb.connection.ClusterDescription; | ||
import com.mongodb.connection.ClusterType; | ||
import com.mongodb.connection.ServerDescription; | ||
import com.mongodb.event.CommandEvent; | ||
import com.mongodb.event.CommandStartedEvent; | ||
import com.mongodb.event.TestServerMonitorListener; | ||
import com.mongodb.internal.connection.TestCommandListener; | ||
import com.mongodb.internal.connection.TestConnectionPoolListener; | ||
import com.mongodb.internal.logging.LogMessage; | ||
import com.mongodb.lang.NonNull; | ||
import com.mongodb.lang.Nullable; | ||
import com.mongodb.logging.TestLoggingInterceptor; | ||
import com.mongodb.test.AfterBeforeParameterResolver; | ||
import org.bson.BsonArray; | ||
import org.bson.BsonBoolean; | ||
|
@@ -62,9 +62,11 @@ | |
import java.io.File; | ||
import java.io.IOException; | ||
import java.net.URISyntaxException; | ||
import java.text.MessageFormat; | ||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Set; | ||
import java.util.concurrent.ExecutionException; | ||
|
@@ -81,6 +83,8 @@ | |
import static com.mongodb.client.test.CollectionHelper.getCurrentClusterTime; | ||
import static com.mongodb.client.test.CollectionHelper.killAllSessions; | ||
import static com.mongodb.client.unified.RunOnRequirementsMatcher.runOnRequirementsMet; | ||
import static com.mongodb.client.unified.UnifiedTestModifications.Modifier; | ||
import static com.mongodb.client.unified.UnifiedTestModifications.applyCustomizations; | ||
import static com.mongodb.client.unified.UnifiedTestModifications.testDef; | ||
import static java.util.Collections.singletonList; | ||
import static java.util.stream.Collectors.toList; | ||
|
@@ -91,6 +95,7 @@ | |
import static org.junit.jupiter.api.Assertions.assertNull; | ||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
import static org.junit.jupiter.api.Assertions.fail; | ||
import static org.junit.jupiter.api.Assumptions.abort; | ||
import static org.junit.jupiter.api.Assumptions.assumeFalse; | ||
import static org.junit.jupiter.api.Assumptions.assumeTrue; | ||
import static util.JsonPoweredTestHelper.getTestDocument; | ||
|
@@ -101,6 +106,9 @@ public abstract class UnifiedTest { | |
private static final Set<String> PRESTART_POOL_ASYNC_WORK_MANAGER_FILE_DESCRIPTIONS = Collections.singleton( | ||
"wait queue timeout errors include details about checked out connections"); | ||
|
||
public static final int ATTEMPTS = 3; | ||
private static Set<String> ignoreRemaining = new HashSet<>(); | ||
|
||
@Nullable | ||
private String fileDescription; | ||
private String schemaVersion; | ||
|
@@ -155,32 +163,47 @@ public Entities getEntities() { | |
} | ||
|
||
@NonNull | ||
protected static Collection<Arguments> getTestData(final String directory) throws URISyntaxException, IOException { | ||
protected static Collection<Arguments> getTestData(final String directory, final boolean isReactive) | ||
throws URISyntaxException, IOException { | ||
List<Arguments> data = new ArrayList<>(); | ||
for (File file : getTestFiles("/" + directory + "/")) { | ||
BsonDocument fileDocument = getTestDocument(file); | ||
|
||
for (BsonValue cur : fileDocument.getArray("tests")) { | ||
data.add(UnifiedTest.createTestData(directory, fileDocument, cur.asDocument())); | ||
|
||
final BsonDocument testDocument = cur.asDocument(); | ||
String testDescription = testDocument.getString("description").getValue(); | ||
String fileDescription = fileDocument.getString("description").getValue(); | ||
TestDef testDef = testDef(directory, fileDescription, testDescription, isReactive); | ||
applyCustomizations(testDef); | ||
|
||
boolean forceFlaky = testDef.wasAssignedModifier(Modifier.FORCE_FLAKY); | ||
boolean retry = forceFlaky || testDef.wasAssignedModifier(Modifier.RETRY); | ||
|
||
int attempts = retry ? ATTEMPTS : 1; | ||
if (forceFlaky) { | ||
attempts = 10; | ||
} | ||
|
||
for (int attempt = 1; attempt <= attempts; attempt++) { | ||
String testName = MessageFormat.format("{0}: {1}", fileDescription, testDescription); | ||
data.add(Arguments.of( | ||
testName, | ||
fileDescription, | ||
testDescription, | ||
directory, | ||
attempt, | ||
attempts * (forceFlaky ? -1 : 1), | ||
fileDocument.getString("schemaVersion").getValue(), | ||
fileDocument.getArray("runOnRequirements", null), | ||
fileDocument.getArray("createEntities", new BsonArray()), | ||
fileDocument.getArray("initialData", new BsonArray()), | ||
testDocument)); | ||
} | ||
} | ||
} | ||
return data; | ||
} | ||
|
||
@NonNull | ||
private static Arguments createTestData( | ||
final String directory, final BsonDocument fileDocument, final BsonDocument testDocument) { | ||
return Arguments.of( | ||
fileDocument.getString("description").getValue(), | ||
testDocument.getString("description").getValue(), | ||
directory, | ||
fileDocument.getString("schemaVersion").getValue(), | ||
fileDocument.getArray("runOnRequirements", null), | ||
fileDocument.getArray("createEntities", new BsonArray()), | ||
fileDocument.getArray("initialData", new BsonArray()), | ||
testDocument); | ||
} | ||
|
||
protected BsonDocument getDefinition() { | ||
return definition; | ||
} | ||
|
@@ -193,9 +216,12 @@ protected BsonDocument getDefinition() { | |
|
||
@BeforeEach | ||
public void setUp( | ||
final String testName, | ||
@Nullable final String fileDescription, | ||
@Nullable final String testDescription, | ||
@Nullable final String directoryName, | ||
final int attemptNumber, | ||
final int totalAttempts, | ||
final String schemaVersion, | ||
@Nullable final BsonArray runOnRequirements, | ||
final BsonArray entitiesArray, | ||
|
@@ -216,9 +242,9 @@ public void setUp( | |
rootContext.getAssertionContext().push(ContextElement.ofTest(definition)); | ||
ignoreExtraEvents = false; | ||
testDef = testDef(directoryName, fileDescription, testDescription, isReactive()); | ||
UnifiedTestModifications.doSkips(testDef); | ||
applyCustomizations(testDef); | ||
|
||
boolean skip = testDef.wasAssignedModifier(UnifiedTestModifications.Modifier.SKIP); | ||
boolean skip = testDef.wasAssignedModifier(Modifier.SKIP); | ||
assumeFalse(skip, "Skipping test"); | ||
skips(fileDescription, testDescription); | ||
|
||
|
@@ -288,8 +314,9 @@ protected void postCleanUp(final TestDef testDef) { | |
} | ||
|
||
/** | ||
* This method is called once per {@link #setUp(String, String, String, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)}, | ||
* unless {@link #setUp(String, String, String, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)} fails unexpectedly. | ||
* This method is called once per | ||
* {@link #setUp(String, String, String, String, int, int, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)}, unless | ||
* {@link #setUp(String, String, String, String, int, int, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)} fails unexpectedly. | ||
*/ | ||
protected void skips(final String fileDescription, final String testDescription) { | ||
} | ||
|
@@ -298,40 +325,66 @@ protected boolean isReactive() { | |
return false; | ||
} | ||
|
||
@ParameterizedTest(name = "{0}: {1}") | ||
@ParameterizedTest(name = "{0}") | ||
@MethodSource("data") | ||
public void shouldPassAllOutcomes( | ||
final String testName, | ||
@Nullable final String fileDescription, | ||
@Nullable final String testDescription, | ||
@Nullable final String directoryName, | ||
final int attemptNumber, | ||
final int totalAttempts, | ||
final String schemaVersion, | ||
@Nullable final BsonArray runOnRequirements, | ||
final BsonArray entitiesArray, | ||
final BsonArray initialData, | ||
final BsonDocument definition) { | ||
BsonArray operations = definition.getArray("operations"); | ||
for (int i = 0; i < operations.size(); i++) { | ||
BsonValue cur = operations.get(i); | ||
assertOperation(rootContext, cur.asDocument(), i); | ||
boolean forceFlaky = totalAttempts < 0; | ||
if (!forceFlaky) { | ||
boolean ignoreThisTest = ignoreRemaining.contains(testName); | ||
assumeFalse(ignoreThisTest, "Skipping a retryable test that already succeeded"); | ||
ignoreRemaining.add(testName); | ||
} | ||
try { | ||
BsonArray operations = definition.getArray("operations"); | ||
for (int i = 0; i < operations.size(); i++) { | ||
BsonValue cur = operations.get(i); | ||
assertOperation(rootContext, cur.asDocument(), i); | ||
} | ||
|
||
if (definition.containsKey("outcome")) { | ||
assertOutcome(rootContext); | ||
} | ||
if (definition.containsKey("outcome")) { | ||
assertOutcome(rootContext); | ||
} | ||
|
||
if (definition.containsKey("expectEvents")) { | ||
compareEvents(rootContext, definition); | ||
} | ||
if (definition.containsKey("expectEvents")) { | ||
compareEvents(rootContext, definition); | ||
} | ||
|
||
if (definition.containsKey("expectLogMessages")) { | ||
ArrayList<LogMatcher.Tweak> tweaks = new ArrayList<>(singletonList( | ||
// `LogMessage.Entry.Name.OPERATION` is not supported, therefore we skip matching its value | ||
LogMatcher.Tweak.skip(LogMessage.Entry.Name.OPERATION))); | ||
if (getMongoClientSettings().getClusterSettings() | ||
.getHosts().stream().anyMatch(serverAddress -> serverAddress instanceof UnixServerAddress)) { | ||
tweaks.add(LogMatcher.Tweak.skip(LogMessage.Entry.Name.SERVER_PORT)); | ||
if (definition.containsKey("expectLogMessages")) { | ||
ArrayList<LogMatcher.Tweak> tweaks = new ArrayList<>(singletonList( | ||
// `LogMessage.Entry.Name.OPERATION` is not supported, therefore we skip matching its value | ||
LogMatcher.Tweak.skip(LogMessage.Entry.Name.OPERATION))); | ||
if (getMongoClientSettings().getClusterSettings() | ||
.getHosts().stream().anyMatch(serverAddress -> serverAddress instanceof UnixServerAddress)) { | ||
tweaks.add(LogMatcher.Tweak.skip(LogMessage.Entry.Name.SERVER_PORT)); | ||
} | ||
compareLogMessages(rootContext, definition, tweaks); | ||
} | ||
} catch (Throwable e) { | ||
if (forceFlaky) { | ||
throw e; | ||
} | ||
compareLogMessages(rootContext, definition, tweaks); | ||
if (!testDef.matchesThrowable(e)) { | ||
// if the throwable is not matched, test definitions were not intended to apply; rethrow it | ||
throw e; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tested this by failing an assertion within the try of |
||
} | ||
boolean isLastAttempt = attemptNumber == Math.abs(totalAttempts); | ||
if (isLastAttempt) { | ||
throw e; | ||
} | ||
|
||
ignoreRemaining.remove(testName); | ||
abort("Ignoring failure and retrying attempt " + attemptNumber); | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method contains the try-catch with the new retry logic.