Skip to content

Commit ad3c136

Browse files
Port #1413 to dtq-dev-9-base: test: de-flake scheduled ITs (test-planted handle collisions, leaked bitstream formats, stale Solr searcher) (#1413) (#1435)
Source: 23f3786 (dtq-dev PR #1413) Test-only. All six flake sources the source commit removed exist unchanged on this branch, so the port is a straight carry-over: - ItemHandleCheckerIT / RequiredMetadataIT planted a random numeric handle (`123456789/` + 1000..1999) that `handle_seq` reaches late in the module run; the sequence mint path does no existence check, so whichever test was minting at that moment died on the handle unique index. Replaced by deterministic non-numeric suffixes (`handle-checker-test`, `required-metadata-test`), and the now-unused `randomString()` helper and `java.util.Random` import go away. - XmlWorkflowFactoryTest used the numeric handle `123456789/999` for the non-mapped collection; same collision class, now `123456789/workflow-test-2`. - PreviewContentServiceImplIT deleted its custom bitstream format through the shared test context after earlier builder deletes had detached it from the thread-bound session, so the delete was silently lost -- one leaked `bitstreamformatregistry` row per test, which broke BitstreamFormatRestRepositoryIT's counts whenever class order put Preview first. The id is now captured before `super.destroy()` and the row deleted afterwards through `BitstreamFormatBuilder.deleteBitstreamFormat`, which uses its own Context. - BitstreamFormatRestRepositoryIT no longer asserts against the hardcoded `DEFAULT_AMOUNT_FORMATS = 95`; each affected test reads the live baseline via `currentTotalFormats()`. Short descriptions are unique in the format registry, so `createMockBitstreamRest()` derives them from the JUnit `@Rule TestName` instead of a random number, and `createAdminAccess` captures the created id in the `andDo` attached to the status check so cleanup still runs if a later assertion fails. - StatisticsRestRepositoryIT#totalVisitsReport_Item_Visited queried the usage report before the posted view event became visible to a new Solr searcher; it now commits, exactly as topCountriesReport_Community_Visited already did. One adaptation vs the source commit, in BitstreamFormatRestRepositoryIT only: this branch already carries an unrelated refactor that replaced the six per-test `ObjectMapper mapper = new ObjectMapper();` locals with a single `@Autowired private ObjectMapper mapper` field. That field is kept and the source hunks were applied on top of it, so no `new ObjectMapper()` returns. Every added and removed line of the source patch for this file is otherwise byte-identical (44 added / 25 removed, verified by diffing the two patches). (cherry picked from commit 23f3786) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 1a8e844 commit ad3c136

6 files changed

Lines changed: 65 additions & 48 deletions

File tree

dspace-api/src/test/java/org/dspace/curate/ItemHandleCheckerIT.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.io.IOException;
2020
import java.sql.SQLException;
2121
import java.util.List;
22-
import java.util.Random;
2322

2423
import okhttp3.mockwebserver.Dispatcher;
2524
import okhttp3.mockwebserver.MockResponse;
@@ -58,7 +57,9 @@
5857
public class ItemHandleCheckerIT extends AbstractIntegrationTestWithDatabase {
5958
private static final String TASK_NAME = "checkhandles";
6059

61-
private static final String HANDLE_COLLECTION = "123456789/" + randomString();
60+
// Non-numeric suffix the handle sequence can never mint, unique to this class so it cannot
61+
// clash with leftover handle rows of other tests either.
62+
private static final String HANDLE_COLLECTION = "123456789/handle-checker-test";
6263

6364
private static final String HANDLE_ITEM1 = HANDLE_COLLECTION + "-1";
6465
private static final String HANDLE_ITEM2 = HANDLE_COLLECTION + "-2";
@@ -280,10 +281,4 @@ private String getIdentifierUri(Item item) {
280281
private List<MetadataValue> getIdentifierUris(Item item) {
281282
return itemService.getMetadata(item, "dc", "identifier", "uri", Item.ANY);
282283
}
283-
284-
private static String randomString() {
285-
Random r = new Random();
286-
// Generate random integers in range 1000 to 1999
287-
return String.valueOf(1000 + r.nextInt(1000));
288-
}
289284
}

dspace-api/src/test/java/org/dspace/curate/RequiredMetadataIT.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
import java.io.IOException;
1717
import java.sql.SQLException;
18-
import java.util.Random;
1918

2019
import org.dspace.AbstractIntegrationTestWithDatabase;
2120
import org.dspace.authorize.AuthorizeException;
@@ -44,7 +43,9 @@
4443
public class RequiredMetadataIT extends AbstractIntegrationTestWithDatabase {
4544
private static final String TASK_NAME = "requiredmetadata";
4645

47-
private static final String HANDLE_COLLECTION = "123456789/" + randomString();
46+
// Non-numeric suffix the handle sequence can never mint, unique to this class so it cannot
47+
// clash with leftover handle rows of other tests either.
48+
private static final String HANDLE_COLLECTION = "123456789/required-metadata-test";
4849
private static final String HANDLE_ITEM1 = HANDLE_COLLECTION + "-1";
4950
private static final String HANDLE_ITEM2 = HANDLE_COLLECTION + "-2";
5051
private static final String HANDLE_ITEM3 = HANDLE_COLLECTION + "-3";
@@ -154,10 +155,4 @@ private static String successResultForItem(Item item) {
154155
return "Item: " + item.getHandle() + " has all required fields";
155156
}
156157

157-
private static String randomString() {
158-
Random r = new Random();
159-
// Generate random integers in range 1000 to 1999
160-
return String.valueOf(1000 + r.nextInt(1000));
161-
}
162-
163158
}

dspace-api/src/test/java/org/dspace/xmlworkflow/XmlWorkflowFactoryTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ public void init() {
7070
this.owningCommunity = communityService.create(null, context);
7171
this.mappedCollection =
7272
this.collectionService.create(context, owningCommunity, "123456789/workflow-test-1");
73-
this.nonMappedCollection = this.collectionService.create(context, owningCommunity, "123456789/999");
73+
// Non-numeric suffix so the handle sequence can never mint a colliding handle
74+
this.nonMappedCollection =
75+
this.collectionService.create(context, owningCommunity, "123456789/workflow-test-2");
7476
//we need to commit the changes so we don't block the table for testing
7577
context.restoreAuthSystemState();
7678
} catch (SQLException e) {

dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamFormatRestRepositoryIT.java

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
import java.util.Arrays;
2323
import java.util.Map;
24-
import java.util.Random;
2524
import java.util.concurrent.atomic.AtomicReference;
2625

2726
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -39,7 +38,9 @@
3938
import org.dspace.eperson.EPerson;
4039
import org.hamcrest.Matchers;
4140
import org.junit.Ignore;
41+
import org.junit.Rule;
4242
import org.junit.Test;
43+
import org.junit.rules.TestName;
4344
import org.springframework.beans.factory.annotation.Autowired;
4445
import org.springframework.test.web.servlet.MvcResult;
4546

@@ -59,7 +60,8 @@ public class BitstreamFormatRestRepositoryIT extends AbstractControllerIntegrati
5960
@Autowired
6061
private ObjectMapper mapper;
6162

62-
private final int DEFAULT_AMOUNT_FORMATS = 95;
63+
@Rule
64+
public TestName testName = new TestName();
6365

6466
@Test
6567
public void findAllPaginationTest() throws Exception {
@@ -138,7 +140,7 @@ public void findOneNonExistentIDInURL() throws Exception {
138140

139141
@Test
140142
public void createAdminAccess() throws Exception {
141-
BitstreamFormatRest bitstreamFormatRest = this.createRandomMockBitstreamRest(false);
143+
BitstreamFormatRest bitstreamFormatRest = this.createMockBitstreamRest();
142144

143145
//Create bitstream format
144146
String token = getAuthToken(admin.getEmail(), password);
@@ -147,10 +149,15 @@ public void createAdminAccess() throws Exception {
147149
AtomicReference<Integer> idRef = new AtomicReference<>();
148150
try {
149151

152+
// Capture the id right after the status check, so the format is cleaned up even when
153+
// a later assertion in this test fails.
150154
MvcResult mvcResult = getClient(token).perform(post("/api/core/bitstreamformats/")
151155
.content(mapper.writeValueAsBytes(bitstreamFormatRest))
152156
.contentType(contentType))
153157
.andExpect(status().isCreated())
158+
.andDo(result -> idRef
159+
.set(read(result.getResponse().getContentAsString(),
160+
"$.id")))
154161
.andExpect(jsonPath("$", HalMatcher.matchNoEmbeds()))
155162
.andReturn();
156163

@@ -170,22 +177,23 @@ public void createAdminAccess() throws Exception {
170177
bitstreamFormatRest.getMimetype(),
171178
bitstreamFormatRest.getDescription(),
172179
bitstreamFormatRest.getShortDescription())
173-
)))
174-
.andDo(result -> idRef
175-
.set(read(result.getResponse().getContentAsString(), "$.id")));
180+
)));
176181

177182
} finally {
178-
// Delete the created community (cleanup after ourselves!)
179-
BitstreamFormatBuilder.deleteBitstreamFormat(idRef.get());
183+
// Delete the created bitstream format (cleanup after ourselves!)
184+
if (idRef.get() != null) {
185+
BitstreamFormatBuilder.deleteBitstreamFormat(idRef.get());
186+
}
180187
}
181188

182189

183190
}
184191

185192
@Test
186193
public void createNonValidSupportLevel() throws Exception {
187-
BitstreamFormatRest bitstreamFormatRest = this.createRandomMockBitstreamRest(false);
194+
BitstreamFormatRest bitstreamFormatRest = this.createMockBitstreamRest();
188195
bitstreamFormatRest.setSupportLevel("NONVALID SUPPORT LVL");
196+
int totalFormatsBefore = currentTotalFormats();
189197
//Attempt to create bitstream with a non-valid support lvl
190198
String token = getAuthToken(admin.getEmail(), password);
191199
getClient(token).perform(post("/api/core/bitstreamformats/")
@@ -195,12 +203,13 @@ public void createNonValidSupportLevel() throws Exception {
195203
// Check that no new bitstreamformat was created
196204
getClient().perform(get("/api/core/bitstreamformats/"))
197205
.andExpect(status().isOk())
198-
.andExpect(jsonPath("$.page.totalElements", is(DEFAULT_AMOUNT_FORMATS)));
206+
.andExpect(jsonPath("$.page.totalElements", is(totalFormatsBefore)));
199207
}
200208

201209
@Test
202210
public void createNoAccess() throws Exception {
203-
BitstreamFormatRest bitstreamFormatRest = this.createRandomMockBitstreamRest(false);
211+
BitstreamFormatRest bitstreamFormatRest = this.createMockBitstreamRest();
212+
int totalFormatsBefore = currentTotalFormats();
204213

205214
//Try to create bitstreamFormat without auth token
206215
getClient(null).perform(post("/api/core/bitstreamformats/")
@@ -210,12 +219,13 @@ public void createNoAccess() throws Exception {
210219
// Check that no new bitstreamformat was created
211220
getClient().perform(get("/api/core/bitstreamformats/"))
212221
.andExpect(status().isOk())
213-
.andExpect(jsonPath("$.page.totalElements", is(DEFAULT_AMOUNT_FORMATS)));
222+
.andExpect(jsonPath("$.page.totalElements", is(totalFormatsBefore)));
214223
}
215224

216225
@Test
217226
public void createNonAdminAccess() throws Exception {
218-
BitstreamFormatRest bitstreamFormatRest = this.createRandomMockBitstreamRest(false);
227+
BitstreamFormatRest bitstreamFormatRest = this.createMockBitstreamRest();
228+
int totalFormatsBefore = currentTotalFormats();
219229
context.turnOffAuthorisationSystem();
220230
EPerson user = EPersonBuilder.createEPerson(context)
221231
.withNameInMetadata("first", "last")
@@ -233,12 +243,13 @@ public void createNonAdminAccess() throws Exception {
233243
// Check that no new bitstreamformat was created
234244
getClient().perform(get("/api/core/bitstreamformats/"))
235245
.andExpect(status().isOk())
236-
.andExpect(jsonPath("$.page.totalElements", is(DEFAULT_AMOUNT_FORMATS)));
246+
.andExpect(jsonPath("$.page.totalElements", is(totalFormatsBefore)));
237247
}
238248

239249
@Test
240250
public void createAlreadyExisting() throws Exception {
241-
BitstreamFormatRest bitstreamFormatRest = this.createRandomMockBitstreamRest(true);
251+
BitstreamFormatRest bitstreamFormatRest = this.createMockBitstreamRest();
252+
int totalFormatsBefore = currentTotalFormats();
242253

243254
// Capture the Id of the created BitstreamFormat (see andDo() below)
244255
AtomicReference<Integer> idRef = new AtomicReference<>();
@@ -262,12 +273,14 @@ public void createAlreadyExisting() throws Exception {
262273
// Check that the new bitstreamformat was created only once
263274
getClient().perform(get("/api/core/bitstreamformats/"))
264275
.andExpect(status().isOk())
265-
.andExpect(jsonPath("$.page.totalElements", is(DEFAULT_AMOUNT_FORMATS + 1)));
276+
.andExpect(jsonPath("$.page.totalElements", is(totalFormatsBefore + 1)));
266277

267278

268279
} finally {
269-
// Delete the created community (cleanup after ourselves!)
270-
BitstreamFormatBuilder.deleteBitstreamFormat(idRef.get());
280+
// Delete the created bitstream format (cleanup after ourselves!)
281+
if (idRef.get() != null) {
282+
BitstreamFormatBuilder.deleteBitstreamFormat(idRef.get());
283+
}
271284
}
272285
}
273286

@@ -631,19 +644,25 @@ public void deleteNonAdminAccess() throws Exception {
631644
)));
632645
}
633646

634-
private BitstreamFormatRest createRandomMockBitstreamRest(boolean withRand) {
647+
/**
648+
* Short descriptions are unique in the format registry, so derive them from the test method
649+
* name: deterministic and collision-free, unlike the random numbers used before.
650+
*/
651+
private BitstreamFormatRest createMockBitstreamRest() {
635652
BitstreamFormatRest bitstreamFormatRest = new BitstreamFormatRest();
636-
String random = null;
637-
if (withRand) {
638-
Random rand = new Random();
639-
random = String.valueOf(rand.nextInt(100) + 1);
640-
}
641-
bitstreamFormatRest.setShortDescription("Test short" + random);
653+
bitstreamFormatRest.setShortDescription("Test short " + testName.getMethodName());
642654
bitstreamFormatRest.setDescription("Full description of Test short");
643655
bitstreamFormatRest.setMimetype("text/plain");
644656
bitstreamFormatRest.setSupportLevel("KNOWN");
645657
bitstreamFormatRest.setInternal(false);
646658
bitstreamFormatRest.setExtensions(Arrays.asList("txt", "asc"));
647659
return bitstreamFormatRest;
648660
}
661+
662+
private int currentTotalFormats() throws Exception {
663+
String response = getClient().perform(get("/api/core/bitstreamformats/"))
664+
.andExpect(status().isOk())
665+
.andReturn().getResponse().getContentAsString();
666+
return read(response, "$.page.totalElements");
667+
}
649668
}

dspace-server-webapp/src/test/java/org/dspace/app/rest/PreviewContentServiceImplIT.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
2222
import org.dspace.authorize.AuthorizeException;
2323
import org.dspace.builder.BitstreamBuilder;
24+
import org.dspace.builder.BitstreamFormatBuilder;
2425
import org.dspace.builder.BundleBuilder;
2526
import org.dspace.builder.CollectionBuilder;
2627
import org.dspace.builder.CommunityBuilder;
@@ -33,7 +34,6 @@
3334
import org.dspace.content.Community;
3435
import org.dspace.content.Item;
3536
import org.dspace.content.PreviewContent;
36-
import org.dspace.content.service.BitstreamFormatService;
3737
import org.dspace.content.service.PreviewContentService;
3838
import org.dspace.util.FileInfo;
3939
import org.junit.After;
@@ -46,8 +46,6 @@ public class PreviewContentServiceImplIT extends AbstractControllerIntegrationTe
4646

4747
@Autowired
4848
PreviewContentService previewContentService;
49-
@Autowired
50-
BitstreamFormatService bitstreamFormatService;
5149

5250
PreviewContent previewContent0;
5351
PreviewContent previewContent1;
@@ -256,11 +254,15 @@ public void destroy() throws Exception {
256254
BitstreamBuilder.deleteBitstream(tarXzFileWithIncorrectMimeType.getID());
257255

258256
// removing custom mime type format created for tarXGzipFile and tgzFileWithGzipMimeType files
259-
if (customMimeTypeFormat != null) {
260-
bitstreamFormatService.delete(context, customMimeTypeFormat);
261-
}
257+
Integer customMimeTypeFormatId = customMimeTypeFormat == null ? null : customMimeTypeFormat.getID();
262258

263259
super.destroy();
260+
261+
// Must run in its own Context after super.destroy(): a delete through the shared test
262+
// context is never committed here, which leaked one format row per test.
263+
if (customMimeTypeFormatId != null) {
264+
BitstreamFormatBuilder.deleteBitstreamFormat(customMimeTypeFormatId);
265+
}
264266
}
265267

266268
@Test

dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,10 @@ public void totalVisitsReport_Item_Visited() throws Exception {
403403
.contentType(contentType))
404404
.andExpect(status().isCreated());
405405

406+
// Commit the view event waiting for a new searcher so it is visible to the report query
407+
// below (same race and fix as in topCountriesReport_Community_Visited).
408+
StatisticsServiceFactory.getInstance().getSolrLoggerService().commit();
409+
406410
// And request that collection's TotalVisits stat report
407411
getClient(adminToken).perform(
408412
get("/api/statistics/usagereports/" + itemVisited.getID() + "_" + TOTAL_VISITS_REPORT_ID))

0 commit comments

Comments
 (0)