2121import com .mongodb .MongoNamespace ;
2222import com .mongodb .ReadPreference ;
2323import com .mongodb .UnixServerAddress ;
24- import com .mongodb .client .unified .UnifiedTestModifications .TestDef ;
25- import com .mongodb .event .TestServerMonitorListener ;
26- import com .mongodb .internal .logging .LogMessage ;
27- import com .mongodb .logging .TestLoggingInterceptor ;
2824import com .mongodb .WriteConcern ;
2925import com .mongodb .client .ClientSession ;
3026import com .mongodb .client .MongoClient ;
3127import com .mongodb .client .MongoDatabase ;
3228import com .mongodb .client .gridfs .GridFSBucket ;
3329import com .mongodb .client .model .Filters ;
3430import com .mongodb .client .test .CollectionHelper ;
31+ import com .mongodb .client .unified .UnifiedTestModifications .TestDef ;
3532import com .mongodb .client .vault .ClientEncryption ;
3633import com .mongodb .connection .ClusterDescription ;
3734import com .mongodb .connection .ClusterType ;
3835import com .mongodb .connection .ServerDescription ;
3936import com .mongodb .event .CommandEvent ;
4037import com .mongodb .event .CommandStartedEvent ;
38+ import com .mongodb .event .TestServerMonitorListener ;
4139import com .mongodb .internal .connection .TestCommandListener ;
4240import com .mongodb .internal .connection .TestConnectionPoolListener ;
41+ import com .mongodb .internal .logging .LogMessage ;
4342import com .mongodb .lang .NonNull ;
4443import com .mongodb .lang .Nullable ;
44+ import com .mongodb .logging .TestLoggingInterceptor ;
4545import com .mongodb .test .AfterBeforeParameterResolver ;
4646import org .bson .BsonArray ;
4747import org .bson .BsonBoolean ;
5757import org .junit .jupiter .params .ParameterizedTest ;
5858import org .junit .jupiter .params .provider .Arguments ;
5959import org .junit .jupiter .params .provider .MethodSource ;
60+ import org .opentest4j .AssertionFailedError ;
6061import org .opentest4j .TestAbortedException ;
6162
6263import java .io .File ;
6364import java .io .IOException ;
6465import java .net .URISyntaxException ;
66+ import java .text .MessageFormat ;
6567import java .util .ArrayList ;
6668import java .util .Collection ;
6769import java .util .Collections ;
70+ import java .util .HashSet ;
6871import java .util .List ;
6972import java .util .Set ;
7073import java .util .concurrent .ExecutionException ;
8184import static com .mongodb .client .test .CollectionHelper .getCurrentClusterTime ;
8285import static com .mongodb .client .test .CollectionHelper .killAllSessions ;
8386import static com .mongodb .client .unified .RunOnRequirementsMatcher .runOnRequirementsMet ;
87+ import static com .mongodb .client .unified .UnifiedTestModifications .doSkips ;
8488import static com .mongodb .client .unified .UnifiedTestModifications .testDef ;
8589import static java .util .Collections .singletonList ;
8690import static java .util .stream .Collectors .toList ;
9195import static org .junit .jupiter .api .Assertions .assertNull ;
9296import static org .junit .jupiter .api .Assertions .assertTrue ;
9397import static org .junit .jupiter .api .Assertions .fail ;
98+ import static org .junit .jupiter .api .Assumptions .assumeFalse ;
9499import static org .junit .jupiter .api .Assumptions .assumeTrue ;
95100import static util .JsonPoweredTestHelper .getTestDocument ;
96101import static util .JsonPoweredTestHelper .getTestFiles ;
@@ -100,6 +105,9 @@ public abstract class UnifiedTest {
100105 private static final Set <String > PRESTART_POOL_ASYNC_WORK_MANAGER_FILE_DESCRIPTIONS = Collections .singleton (
101106 "wait queue timeout errors include details about checked out connections" );
102107
108+ public static final int ATTEMPTS = 3 ;
109+ private static Set <String > completed = new HashSet <>();
110+
103111 @ Nullable
104112 private String fileDescription ;
105113 private String schemaVersion ;
@@ -154,32 +162,51 @@ public Entities getEntities() {
154162 }
155163
156164 @ NonNull
157- protected static Collection <Arguments > getTestData (final String directory ) throws URISyntaxException , IOException {
165+ protected static Collection <Arguments > getTestData (final String directory , final boolean isReactive )
166+ throws URISyntaxException , IOException {
158167 List <Arguments > data = new ArrayList <>();
159168 for (File file : getTestFiles ("/" + directory + "/" )) {
160169 BsonDocument fileDocument = getTestDocument (file );
161-
162170 for (BsonValue cur : fileDocument .getArray ("tests" )) {
163- data .add (UnifiedTest .createTestData (directory , fileDocument , cur .asDocument ()));
171+
172+ final BsonDocument testDocument = cur .asDocument ();
173+ String testDescription = testDocument .getString ("description" ).getValue ();
174+ String fileDescription = fileDocument .getString ("description" ).getValue ();
175+ TestDef testDef = testDef (directory , fileDescription , testDescription , isReactive );
176+ doSkips (testDef );
177+
178+ boolean forceFlaky = testDef .wasAssignedModifier (UnifiedTestModifications .Modifier .FORCE_FLAKY );
179+ boolean retry = forceFlaky || testDef .wasAssignedModifier (UnifiedTestModifications .Modifier .RETRY );
180+
181+ int attempts = retry ? ATTEMPTS : 1 ;
182+ if (forceFlaky ) {
183+ attempts = 10 ;
184+ }
185+
186+ for (int attempt = 1 ; attempt <= attempts ; attempt ++) {
187+ String testName = !retry
188+ ? MessageFormat .format ("{0}: {1}" , fileDescription , testDescription )
189+ : MessageFormat .format (
190+ "{0}: {1} ({2} of {3})" ,
191+ fileDescription , testDescription , attempt , attempts );
192+ data .add (Arguments .of (
193+ testName ,
194+ fileDescription ,
195+ testDescription ,
196+ directory ,
197+ attempt ,
198+ attempts * (forceFlaky ? -1 : 1 ),
199+ fileDocument .getString ("schemaVersion" ).getValue (),
200+ fileDocument .getArray ("runOnRequirements" , null ),
201+ fileDocument .getArray ("createEntities" , new BsonArray ()),
202+ fileDocument .getArray ("initialData" , new BsonArray ()),
203+ testDocument ));
204+ }
164205 }
165206 }
166207 return data ;
167208 }
168209
169- @ NonNull
170- private static Arguments createTestData (
171- final String directory , final BsonDocument fileDocument , final BsonDocument testDocument ) {
172- return Arguments .of (
173- fileDocument .getString ("description" ).getValue (),
174- testDocument .getString ("description" ).getValue (),
175- directory ,
176- fileDocument .getString ("schemaVersion" ).getValue (),
177- fileDocument .getArray ("runOnRequirements" , null ),
178- fileDocument .getArray ("createEntities" , new BsonArray ()),
179- fileDocument .getArray ("initialData" , new BsonArray ()),
180- testDocument );
181- }
182-
183210 protected BsonDocument getDefinition () {
184211 return definition ;
185212 }
@@ -192,9 +219,12 @@ protected BsonDocument getDefinition() {
192219
193220 @ BeforeEach
194221 public void setUp (
222+ final String testName ,
195223 @ Nullable final String fileDescription ,
196224 @ Nullable final String testDescription ,
197225 @ Nullable final String directoryName ,
226+ final int attemptNumber ,
227+ final int totalAttempts ,
198228 final String schemaVersion ,
199229 @ Nullable final BsonArray runOnRequirements ,
200230 final BsonArray entitiesArray ,
@@ -287,8 +317,9 @@ protected void postCleanUp(final TestDef testDef) {
287317 }
288318
289319 /**
290- * This method is called once per {@link #setUp(String, String, String, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)},
291- * unless {@link #setUp(String, String, String, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)} fails unexpectedly.
320+ * This method is called once per
321+ * {@link #setUp(String, String, String, String, int, int, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)}, unless
322+ * {@link #setUp(String, String, String, String, int, int, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)} fails unexpectedly.
292323 */
293324 protected void skips (final String fileDescription , final String testDescription ) {
294325 }
@@ -297,40 +328,58 @@ protected boolean isReactive() {
297328 return false ;
298329 }
299330
300- @ ParameterizedTest (name = "{0}: {1} " )
331+ @ ParameterizedTest (name = "{0}" )
301332 @ MethodSource ("data" )
302333 public void shouldPassAllOutcomes (
334+ final String testName ,
303335 @ Nullable final String fileDescription ,
304336 @ Nullable final String testDescription ,
305337 @ Nullable final String directoryName ,
338+ final int attemptNumber ,
339+ final int totalAttempts ,
306340 final String schemaVersion ,
307341 @ Nullable final BsonArray runOnRequirements ,
308342 final BsonArray entitiesArray ,
309343 final BsonArray initialData ,
310344 final BsonDocument definition ) {
311- BsonArray operations = definition . getArray ( "operations" ) ;
312- for ( int i = 0 ; i < operations . size (); i ++ ) {
313- BsonValue cur = operations . get ( i );
314- assertOperation ( rootContext , cur . asDocument (), i );
345+ boolean forceFlaky = totalAttempts < 0 ;
346+ if (! forceFlaky ) {
347+ assumeFalse ( completed . contains ( testName ), "Skipping retryable test that succeeded" );
348+ completed . add ( testName );
315349 }
350+ try {
351+ BsonArray operations = definition .getArray ("operations" );
352+ for (int i = 0 ; i < operations .size (); i ++) {
353+ BsonValue cur = operations .get (i );
354+ assertOperation (rootContext , cur .asDocument (), i );
355+ }
316356
317- if (definition .containsKey ("outcome" )) {
318- assertOutcome (rootContext );
319- }
357+ if (definition .containsKey ("outcome" )) {
358+ assertOutcome (rootContext );
359+ }
320360
321- if (definition .containsKey ("expectEvents" )) {
322- compareEvents (rootContext , definition );
323- }
361+ if (definition .containsKey ("expectEvents" )) {
362+ compareEvents (rootContext , definition );
363+ }
324364
325- if (definition .containsKey ("expectLogMessages" )) {
326- ArrayList <LogMatcher .Tweak > tweaks = new ArrayList <>(singletonList (
327- // `LogMessage.Entry.Name.OPERATION` is not supported, therefore we skip matching its value
328- LogMatcher .Tweak .skip (LogMessage .Entry .Name .OPERATION )));
329- if (getMongoClientSettings ().getClusterSettings ()
330- .getHosts ().stream ().anyMatch (serverAddress -> serverAddress instanceof UnixServerAddress )) {
331- tweaks .add (LogMatcher .Tweak .skip (LogMessage .Entry .Name .SERVER_PORT ));
365+ if (definition .containsKey ("expectLogMessages" )) {
366+ ArrayList <LogMatcher .Tweak > tweaks = new ArrayList <>(singletonList (
367+ // `LogMessage.Entry.Name.OPERATION` is not supported, therefore we skip matching its value
368+ LogMatcher .Tweak .skip (LogMessage .Entry .Name .OPERATION )));
369+ if (getMongoClientSettings ().getClusterSettings ()
370+ .getHosts ().stream ().anyMatch (serverAddress -> serverAddress instanceof UnixServerAddress )) {
371+ tweaks .add (LogMatcher .Tweak .skip (LogMessage .Entry .Name .SERVER_PORT ));
372+ }
373+ compareLogMessages (rootContext , definition , tweaks );
374+ }
375+ } catch (AssertionFailedError e ) {
376+ completed .remove (testName );
377+ boolean lastAttempt = attemptNumber == Math .abs (totalAttempts );
378+ if (forceFlaky || lastAttempt ) {
379+ throw e ;
380+ } else {
381+ assumeFalse (completed .contains (testName ), "Ignoring failure and retrying attempt " + attemptNumber );
332382 }
333- compareLogMessages (rootContext , definition , tweaks );
334383 }
335384 }
336385
0 commit comments