57
57
import org .junit .jupiter .params .ParameterizedTest ;
58
58
import org .junit .jupiter .params .provider .Arguments ;
59
59
import org .junit .jupiter .params .provider .MethodSource ;
60
+ import org .opentest4j .AssertionFailedError ;
60
61
import org .opentest4j .TestAbortedException ;
61
62
62
63
import java .io .File ;
63
64
import java .io .IOException ;
64
65
import java .net .URISyntaxException ;
66
+ import java .text .MessageFormat ;
65
67
import java .util .ArrayList ;
66
68
import java .util .Collection ;
67
69
import java .util .Collections ;
70
+ import java .util .HashSet ;
68
71
import java .util .List ;
69
72
import java .util .Set ;
70
73
import java .util .concurrent .ExecutionException ;
81
84
import static com .mongodb .client .test .CollectionHelper .getCurrentClusterTime ;
82
85
import static com .mongodb .client .test .CollectionHelper .killAllSessions ;
83
86
import static com .mongodb .client .unified .RunOnRequirementsMatcher .runOnRequirementsMet ;
87
+ import static com .mongodb .client .unified .UnifiedTestModifications .doSkips ;
84
88
import static com .mongodb .client .unified .UnifiedTestModifications .testDef ;
85
89
import static java .util .Collections .singletonList ;
86
90
import static java .util .stream .Collectors .toList ;
@@ -101,6 +105,9 @@ public abstract class UnifiedTest {
101
105
private static final Set <String > PRESTART_POOL_ASYNC_WORK_MANAGER_FILE_DESCRIPTIONS = Collections .singleton (
102
106
"wait queue timeout errors include details about checked out connections" );
103
107
108
+ public static final int ATTEMPTS = 3 ;
109
+ private static Set <String > completed = new HashSet <>();
110
+
104
111
@ Nullable
105
112
private String fileDescription ;
106
113
private String schemaVersion ;
@@ -156,32 +163,51 @@ public Entities getEntities() {
156
163
}
157
164
158
165
@ NonNull
159
- protected static Collection <Arguments > getTestData (final String directory ) throws URISyntaxException , IOException {
166
+ protected static Collection <Arguments > getTestData (final String directory , final boolean isReactive )
167
+ throws URISyntaxException , IOException {
160
168
List <Arguments > data = new ArrayList <>();
161
169
for (File file : getTestFiles ("/" + directory + "/" )) {
162
170
BsonDocument fileDocument = getTestDocument (file );
163
-
164
171
for (BsonValue cur : fileDocument .getArray ("tests" )) {
165
- data .add (UnifiedTest .createTestData (directory , fileDocument , cur .asDocument ()));
172
+
173
+ final BsonDocument testDocument = cur .asDocument ();
174
+ String testDescription = testDocument .getString ("description" ).getValue ();
175
+ String fileDescription = fileDocument .getString ("description" ).getValue ();
176
+ TestDef testDef = testDef (directory , fileDescription , testDescription , isReactive );
177
+ doSkips (testDef );
178
+
179
+ boolean forceFlaky = testDef .wasAssignedModifier (UnifiedTestModifications .Modifier .FORCE_FLAKY );
180
+ boolean retry = forceFlaky || testDef .wasAssignedModifier (UnifiedTestModifications .Modifier .RETRY );
181
+
182
+ int attempts = retry ? ATTEMPTS : 1 ;
183
+ if (forceFlaky ) {
184
+ attempts = 10 ;
185
+ }
186
+
187
+ for (int attempt = 1 ; attempt <= attempts ; attempt ++) {
188
+ String testName = !retry
189
+ ? MessageFormat .format ("{0}: {1}" , fileDescription , testDescription )
190
+ : MessageFormat .format (
191
+ "{0}: {1} ({2} of {3})" ,
192
+ fileDescription , testDescription , attempt , attempts );
193
+ data .add (Arguments .of (
194
+ testName ,
195
+ fileDescription ,
196
+ testDescription ,
197
+ directory ,
198
+ attempt ,
199
+ attempts * (forceFlaky ? -1 : 1 ),
200
+ fileDocument .getString ("schemaVersion" ).getValue (),
201
+ fileDocument .getArray ("runOnRequirements" , null ),
202
+ fileDocument .getArray ("createEntities" , new BsonArray ()),
203
+ fileDocument .getArray ("initialData" , new BsonArray ()),
204
+ testDocument ));
205
+ }
166
206
}
167
207
}
168
208
return data ;
169
209
}
170
210
171
- @ NonNull
172
- private static Arguments createTestData (
173
- final String directory , final BsonDocument fileDocument , final BsonDocument testDocument ) {
174
- return Arguments .of (
175
- fileDocument .getString ("description" ).getValue (),
176
- testDocument .getString ("description" ).getValue (),
177
- directory ,
178
- fileDocument .getString ("schemaVersion" ).getValue (),
179
- fileDocument .getArray ("runOnRequirements" , null ),
180
- fileDocument .getArray ("createEntities" , new BsonArray ()),
181
- fileDocument .getArray ("initialData" , new BsonArray ()),
182
- testDocument );
183
- }
184
-
185
211
protected BsonDocument getDefinition () {
186
212
return definition ;
187
213
}
@@ -194,9 +220,12 @@ protected BsonDocument getDefinition() {
194
220
195
221
@ BeforeEach
196
222
public void setUp (
223
+ final String testName ,
197
224
@ Nullable final String fileDescription ,
198
225
@ Nullable final String testDescription ,
199
226
@ Nullable final String directoryName ,
227
+ final int attemptNumber ,
228
+ final int totalAttempts ,
200
229
final String schemaVersion ,
201
230
@ Nullable final BsonArray runOnRequirements ,
202
231
final BsonArray entitiesArray ,
@@ -295,8 +324,9 @@ protected void postCleanUp(final TestDef testDef) {
295
324
}
296
325
297
326
/**
298
- * This method is called once per {@link #setUp(String, String, String, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)},
299
- * unless {@link #setUp(String, String, String, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)} fails unexpectedly.
327
+ * This method is called once per
328
+ * {@link #setUp(String, String, String, String, int, int, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)}, unless
329
+ * {@link #setUp(String, String, String, String, int, int, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)} fails unexpectedly.
300
330
*/
301
331
protected void skips (final String fileDescription , final String testDescription ) {
302
332
}
@@ -305,40 +335,58 @@ protected boolean isReactive() {
305
335
return false ;
306
336
}
307
337
308
- @ ParameterizedTest (name = "{0}: {1} " )
338
+ @ ParameterizedTest (name = "{0}" )
309
339
@ MethodSource ("data" )
310
340
public void shouldPassAllOutcomes (
341
+ final String testName ,
311
342
@ Nullable final String fileDescription ,
312
343
@ Nullable final String testDescription ,
313
344
@ Nullable final String directoryName ,
345
+ final int attemptNumber ,
346
+ final int totalAttempts ,
314
347
final String schemaVersion ,
315
348
@ Nullable final BsonArray runOnRequirements ,
316
349
final BsonArray entitiesArray ,
317
350
final BsonArray initialData ,
318
351
final BsonDocument definition ) {
319
- BsonArray operations = definition . getArray ( "operations" ) ;
320
- for ( int i = 0 ; i < operations . size (); i ++ ) {
321
- BsonValue cur = operations . get ( i );
322
- assertOperation ( rootContext , cur . asDocument (), i );
352
+ boolean forceFlaky = totalAttempts < 0 ;
353
+ if (! forceFlaky ) {
354
+ assumeFalse ( completed . contains ( testName ), "Skipping retryable test that succeeded" );
355
+ completed . add ( testName );
323
356
}
357
+ try {
358
+ BsonArray operations = definition .getArray ("operations" );
359
+ for (int i = 0 ; i < operations .size (); i ++) {
360
+ BsonValue cur = operations .get (i );
361
+ assertOperation (rootContext , cur .asDocument (), i );
362
+ }
324
363
325
- if (definition .containsKey ("outcome" )) {
326
- assertOutcome (rootContext );
327
- }
364
+ if (definition .containsKey ("outcome" )) {
365
+ assertOutcome (rootContext );
366
+ }
328
367
329
- if (definition .containsKey ("expectEvents" )) {
330
- compareEvents (rootContext , definition );
331
- }
368
+ if (definition .containsKey ("expectEvents" )) {
369
+ compareEvents (rootContext , definition );
370
+ }
332
371
333
- if (definition .containsKey ("expectLogMessages" )) {
334
- ArrayList <LogMatcher .Tweak > tweaks = new ArrayList <>(singletonList (
335
- // `LogMessage.Entry.Name.OPERATION` is not supported, therefore we skip matching its value
336
- LogMatcher .Tweak .skip (LogMessage .Entry .Name .OPERATION )));
337
- if (getMongoClientSettings ().getClusterSettings ()
338
- .getHosts ().stream ().anyMatch (serverAddress -> serverAddress instanceof UnixServerAddress )) {
339
- tweaks .add (LogMatcher .Tweak .skip (LogMessage .Entry .Name .SERVER_PORT ));
372
+ if (definition .containsKey ("expectLogMessages" )) {
373
+ ArrayList <LogMatcher .Tweak > tweaks = new ArrayList <>(singletonList (
374
+ // `LogMessage.Entry.Name.OPERATION` is not supported, therefore we skip matching its value
375
+ LogMatcher .Tweak .skip (LogMessage .Entry .Name .OPERATION )));
376
+ if (getMongoClientSettings ().getClusterSettings ()
377
+ .getHosts ().stream ().anyMatch (serverAddress -> serverAddress instanceof UnixServerAddress )) {
378
+ tweaks .add (LogMatcher .Tweak .skip (LogMessage .Entry .Name .SERVER_PORT ));
379
+ }
380
+ compareLogMessages (rootContext , definition , tweaks );
381
+ }
382
+ } catch (AssertionFailedError e ) {
383
+ completed .remove (testName );
384
+ boolean lastAttempt = attemptNumber == Math .abs (totalAttempts );
385
+ if (forceFlaky || lastAttempt ) {
386
+ throw e ;
387
+ } else {
388
+ assumeFalse (completed .contains (testName ), "Ignoring failure and retrying attempt " + attemptNumber );
340
389
}
341
- compareLogMessages (rootContext , definition , tweaks );
342
390
}
343
391
}
344
392
0 commit comments