62
62
import java .io .File ;
63
63
import java .io .IOException ;
64
64
import java .net .URISyntaxException ;
65
+ import java .text .MessageFormat ;
65
66
import java .util .ArrayList ;
66
67
import java .util .Collection ;
67
68
import java .util .Collections ;
69
+ import java .util .HashSet ;
68
70
import java .util .List ;
69
71
import java .util .Set ;
70
72
import java .util .concurrent .ExecutionException ;
81
83
import static com .mongodb .client .test .CollectionHelper .getCurrentClusterTime ;
82
84
import static com .mongodb .client .test .CollectionHelper .killAllSessions ;
83
85
import static com .mongodb .client .unified .RunOnRequirementsMatcher .runOnRequirementsMet ;
86
+ import static com .mongodb .client .unified .UnifiedTestModifications .Modifier ;
87
+ import static com .mongodb .client .unified .UnifiedTestModifications .applyCustomizations ;
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 ;
91
95
import static org .junit .jupiter .api .Assertions .assertNull ;
92
96
import static org .junit .jupiter .api .Assertions .assertTrue ;
93
97
import static org .junit .jupiter .api .Assertions .fail ;
98
+ import static org .junit .jupiter .api .Assumptions .abort ;
94
99
import static org .junit .jupiter .api .Assumptions .assumeFalse ;
95
100
import static org .junit .jupiter .api .Assumptions .assumeTrue ;
96
101
import static util .JsonPoweredTestHelper .getTestDocument ;
@@ -101,6 +106,10 @@ public abstract class UnifiedTest {
101
106
private static final Set <String > PRESTART_POOL_ASYNC_WORK_MANAGER_FILE_DESCRIPTIONS = Collections .singleton (
102
107
"wait queue timeout errors include details about checked out connections" );
103
108
109
+ public static final int RETRY_ATTEMPTS = 3 ;
110
+ public static final int FORCE_FLAKY_ATTEMPTS = 10 ;
111
+ private static final Set <String > ATTEMPTED_TESTS_TO_HENCEFORTH_IGNORE = new HashSet <>();
112
+
104
113
@ Nullable
105
114
private String fileDescription ;
106
115
private String schemaVersion ;
@@ -156,32 +165,49 @@ public Entities getEntities() {
156
165
}
157
166
158
167
@ NonNull
159
- protected static Collection <Arguments > getTestData (final String directory ) throws URISyntaxException , IOException {
168
+ protected static Collection <Arguments > getTestData (final String directory , final boolean isReactive )
169
+ throws URISyntaxException , IOException {
160
170
List <Arguments > data = new ArrayList <>();
161
171
for (File file : getTestFiles ("/" + directory + "/" )) {
162
172
BsonDocument fileDocument = getTestDocument (file );
163
-
164
173
for (BsonValue cur : fileDocument .getArray ("tests" )) {
165
- data .add (UnifiedTest .createTestData (directory , fileDocument , cur .asDocument ()));
174
+
175
+ final BsonDocument testDocument = cur .asDocument ();
176
+ String testDescription = testDocument .getString ("description" ).getValue ();
177
+ String fileDescription = fileDocument .getString ("description" ).getValue ();
178
+ TestDef testDef = testDef (directory , fileDescription , testDescription , isReactive );
179
+ applyCustomizations (testDef );
180
+
181
+ boolean forceFlaky = testDef .wasAssignedModifier (Modifier .FORCE_FLAKY );
182
+ boolean retry = forceFlaky || testDef .wasAssignedModifier (Modifier .RETRY );
183
+
184
+ int attempts ;
185
+ if (retry ) {
186
+ attempts = forceFlaky ? FORCE_FLAKY_ATTEMPTS : RETRY_ATTEMPTS ;
187
+ } else {
188
+ attempts = 1 ;
189
+ }
190
+
191
+ for (int attempt = 1 ; attempt <= attempts ; attempt ++) {
192
+ String testName = MessageFormat .format ("{0}: {1}" , fileDescription , testDescription );
193
+ data .add (Arguments .of (
194
+ testName ,
195
+ fileDescription ,
196
+ testDescription ,
197
+ directory ,
198
+ attempt ,
199
+ attempts ,
200
+ fileDocument .getString ("schemaVersion" ).getValue (),
201
+ fileDocument .getArray ("runOnRequirements" , null ),
202
+ fileDocument .getArray ("createEntities" , new BsonArray ()),
203
+ fileDocument .getArray ("initialData" , new BsonArray ()),
204
+ testDocument .clone ()));
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 ,
@@ -218,9 +247,9 @@ public void setUp(
218
247
ignoreExtraEvents = false ;
219
248
if (directoryName != null && fileDescription != null && testDescription != null ) {
220
249
testDef = testDef (directoryName , fileDescription , testDescription , isReactive ());
221
- UnifiedTestModifications . doSkips (testDef );
250
+ applyCustomizations (testDef );
222
251
223
- boolean skip = testDef .wasAssignedModifier (UnifiedTestModifications . Modifier .SKIP );
252
+ boolean skip = testDef .wasAssignedModifier (Modifier .SKIP );
224
253
assumeFalse (skip , "Skipping test" );
225
254
}
226
255
skips (fileDescription , testDescription );
@@ -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,72 @@ 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 = testDef .wasAssignedModifier (Modifier .FORCE_FLAKY );
353
+ if (!forceFlaky ) {
354
+ boolean ignoreThisTest = ATTEMPTED_TESTS_TO_HENCEFORTH_IGNORE .contains (testName );
355
+ assumeFalse (ignoreThisTest , "Skipping a retryable test that already succeeded" );
356
+ // The attempt is what counts, since a test may fail with
357
+ // something like "ignored", and would not be retried.
358
+ // Only failures should trigger another attempt.
359
+ ATTEMPTED_TESTS_TO_HENCEFORTH_IGNORE .add (testName );
323
360
}
361
+ try {
362
+ BsonArray operations = definition .getArray ("operations" );
363
+ for (int i = 0 ; i < operations .size (); i ++) {
364
+ BsonValue cur = operations .get (i );
365
+ assertOperation (rootContext , cur .asDocument (), i );
366
+ }
324
367
325
- if (definition .containsKey ("outcome" )) {
326
- assertOutcome (rootContext );
327
- }
368
+ if (definition .containsKey ("outcome" )) {
369
+ assertOutcome (rootContext );
370
+ }
328
371
329
- if (definition .containsKey ("expectEvents" )) {
330
- compareEvents (rootContext , definition );
331
- }
372
+ if (definition .containsKey ("expectEvents" )) {
373
+ compareEvents (rootContext , definition );
374
+ }
332
375
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 ));
376
+ if (definition .containsKey ("expectLogMessages" )) {
377
+ ArrayList <LogMatcher .Tweak > tweaks = new ArrayList <>(singletonList (
378
+ // `LogMessage.Entry.Name.OPERATION` is not supported, therefore we skip matching its value
379
+ LogMatcher .Tweak .skip (LogMessage .Entry .Name .OPERATION )));
380
+ if (getMongoClientSettings ().getClusterSettings ()
381
+ .getHosts ().stream ().anyMatch (serverAddress -> serverAddress instanceof UnixServerAddress )) {
382
+ tweaks .add (LogMatcher .Tweak .skip (LogMessage .Entry .Name .SERVER_PORT ));
383
+ }
384
+ compareLogMessages (rootContext , definition , tweaks );
385
+ }
386
+ } catch (TestAbortedException e ) {
387
+ // if a test is ignored, we do not retry
388
+ throw e ;
389
+ } catch (Throwable e ) {
390
+ if (forceFlaky ) {
391
+ throw e ;
392
+ }
393
+ if (testDef != null && !testDef .matchesThrowable (e )) {
394
+ // if the throwable is not matched, test definitions were not intended to apply; rethrow it
395
+ throw e ;
340
396
}
341
- compareLogMessages (rootContext , definition , tweaks );
397
+ boolean isLastAttempt = attemptNumber == totalAttempts ;
398
+ if (isLastAttempt ) {
399
+ throw e ;
400
+ }
401
+
402
+ ATTEMPTED_TESTS_TO_HENCEFORTH_IGNORE .remove (testName );
403
+ abort ("Ignoring failure and retrying attempt " + attemptNumber );
342
404
}
343
405
}
344
406
0 commit comments