fix: re-enable / clean up excluded search test cases (fixes #55) (#264)

A single surefire pattern in root pom.xml (org.airsonic.player.service
.search.*TestCase.java) blanket-excluded the entire search-domain test
surface (7 classes, 33 tests) since 2024-04-28, introduced alongside
upstream's JDK 21 / byte-buddy-agent work. This PR removes the exclusion
and brings the search subsystem back into the CI matrix for the first
time since.

Initial run with the exclusion removed: 26 pass / 6 fail / 1 error.
Per-class disposition:

- 4 classes (20 tests) were stale-skipped — re-enabled cleanly with no
  code changes (AnalyzerFactoryTestCase, SearchServiceTestCase,
  SearchServiceSpecialGenreTestCase, SearchServiceStartWithStopwardsTestCase).
- QueryFactoryTestCase (11 tests, 5 failing) had stale expectations.
  createMultiFieldWildQuery was refactored to emit boost-aware output --
  WildcardQuery + a 0.5-boosted TermQuery per token per field, with
  field-multipliers (1.1) on boosted fields -- but the test expectations
  were pinned to the pre-refactor output. Updated 5 expected strings to
  match the actual boost math (1.1 wildcard, 1.1 x 0.5 = 0.55 boosted-
  field term, 0.5 no-boost term).
- IndexManagerTestCase had a copy-paste at line 216 asserting on
  candidates.size() instead of albums.size(). One-line fix.
- SearchServiceSpecialPathTestCase had three test-infra issues:
  @SpringBootTest(webEnvironment=NONE) incompatible with the security
  config's MvcRequestMatcher (which requires a servlet context); switched
  to default MOCK. Hardcoded MusicFolder IDs (1, 2, 3) collided with
  Liquibase-seeded rows producing ObjectOptimisticLockingFailure; switched
  to the no-id constructor so JPA's IDENTITY generator assigns unique IDs
  at persist time. Copy-paste at line 132 asserting on folder03.size()
  (always 1) instead of randomAlbums.size() -- a no-op assertion for
  years.

Every fix is test-side. Production code (QueryFactory, IndexManager,
SearchService) was untouched. Three consecutive Postgres runs of the
affected classes confirm no flakiness.

Test floor: 1092 -> 1125 (+33).

fixes #55.

Co-authored-by: litebito <litebito@users.noreply.github.com>
This commit is contained in:
litebito 2026-06-06 22:50:32 +02:00 committed by GitHub
parent c72025829f
commit e1106699fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 57 additions and 35 deletions

View file

@ -213,7 +213,7 @@ public class IndexManagerTestCase {
});
albums = albumRepository.findByPresentFalse();
assertEquals(4, candidates.size());
assertEquals(4, albums.size());
/* Does not scan, only expunges the index. */
indexManager.startIndexing();

View file

@ -80,6 +80,14 @@ public class QueryFactoryTestCase {
* This is suitable for 8.x.
*/
/*
* The expected query strings below reflect the boost-aware output of
* createMultiFieldWildQuery in QueryFactory: each token contributes BOTH a wildcard
* (token*) and a 0.5×-boosted bare term, with field-specific multipliers applied when
* IndexType declares a boost (TITLE 1.1, ALBUM 1.1). For ARTIST/ARTIST_ID3 (no
* field-boost) the wildcard appears unboosted and the term carries (artist:abc)^0.5.
*/
@Test
public void testSearchArtist() throws IOException {
SearchCriteria criteria = new SearchCriteria();
@ -89,12 +97,14 @@ public class QueryFactoryTestCase {
Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.ARTIST);
assertEquals(
"+((artist:abc*) (artist:def*)) +(folder:" + PATH1 + ")",
"+((artist:abc*) ((artist:abc)^0.5) (artist:def*) ((artist:def)^0.5)) +(folder:" + PATH1 + ")",
query.toString(), "SearchArtist");
query = queryFactory.search(criteria, MULTI_FOLDERS, IndexType.ARTIST);
assertEquals("+((artist:abc*) (artist:def*)) +(folder:" + PATH1
+ " folder:" + PATH2 + ")", query.toString(), "SearchArtist");
assertEquals(
"+((artist:abc*) ((artist:abc)^0.5) (artist:def*) ((artist:def)^0.5)) +(folder:" + PATH1
+ " folder:" + PATH2 + ")",
query.toString(), "SearchArtist");
}
@Test
@ -106,14 +116,16 @@ public class QueryFactoryTestCase {
Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.ALBUM);
assertEquals(
"+(((album:abc*)^1.1 artist:abc*) ((album:def*)^1.1 artist:def*)) +(folder:" + PATH1
+ ")",
"+(((album:abc)^0.55 artist:abc*) ((album:abc*)^1.1 (artist:abc)^0.5) "
+ "((album:def)^0.55 artist:def*) ((album:def*)^1.1 (artist:def)^0.5)) "
+ "+(folder:" + PATH1 + ")",
query.toString(), "SearchAlbum");
query = queryFactory.search(criteria, MULTI_FOLDERS, IndexType.ALBUM);
assertEquals(
"+(((album:abc*)^1.1 artist:abc*) ((album:def*)^1.1 artist:def*)) +(folder:" + PATH1
+ " folder:" + PATH2 + ")",
"+(((album:abc)^0.55 artist:abc*) ((album:abc*)^1.1 (artist:abc)^0.5) "
+ "((album:def)^0.55 artist:def*) ((album:def*)^1.1 (artist:def)^0.5)) "
+ "+(folder:" + PATH1 + " folder:" + PATH2 + ")",
query.toString(), "SearchAlbum");
}
@ -126,12 +138,17 @@ public class QueryFactoryTestCase {
Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.SONG);
assertEquals(
"+(((title:abc*)^1.1 artist:abc*) ((title:def*)^1.1 artist:def*)) +(folder:" + PATH1 + ")",
"+(((title:abc)^0.55 artist:abc*) ((title:abc*)^1.1 (artist:abc)^0.5) "
+ "((title:def)^0.55 artist:def*) ((title:def*)^1.1 (artist:def)^0.5)) "
+ "+(folder:" + PATH1 + ")",
query.toString(), "SearchSong");
query = queryFactory.search(criteria, MULTI_FOLDERS, IndexType.SONG);
assertEquals("+(((title:abc*)^1.1 artist:abc*) ((title:def*)^1.1 artist:def*)) +(folder:" + PATH1
+ " folder:" + PATH2 + ")", query.toString(), "SearchSong");
assertEquals(
"+(((title:abc)^0.55 artist:abc*) ((title:abc*)^1.1 (artist:abc)^0.5) "
+ "((title:def)^0.55 artist:def*) ((title:def*)^1.1 (artist:def)^0.5)) "
+ "+(folder:" + PATH1 + " folder:" + PATH2 + ")",
query.toString(), "SearchSong");
}
@Test
@ -142,14 +159,15 @@ public class QueryFactoryTestCase {
criteria.setQuery(QUERY_ENG_ONLY);
Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.ARTIST_ID3);
assertEquals("+((artist:abc*) (artist:def*)) +(folderId:"
+ FID1 + ")", query.toString(), "SearchSong");
assertEquals(
"+((artist:abc*) ((artist:abc)^0.5) (artist:def*) ((artist:def)^0.5)) +(folderId:" + FID1 + ")",
query.toString(), "SearchArtistId3");
query = queryFactory.search(criteria, MULTI_FOLDERS, IndexType.ARTIST_ID3);
assertEquals(
"+((artist:abc*) (artist:def*)) +(folderId:" + FID1
"+((artist:abc*) ((artist:abc)^0.5) (artist:def*) ((artist:def)^0.5)) +(folderId:" + FID1
+ " folderId:" + FID2 + ")",
query.toString(), "SearchSong");
query.toString(), "SearchArtistId3");
}
@Test
@ -161,15 +179,16 @@ public class QueryFactoryTestCase {
Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.ALBUM_ID3);
assertEquals(
"+(((album:abc*)^1.1 artist:abc*) ((album:def*)^1.1 artist:def*)) "
"+(((album:abc)^0.55 artist:abc*) ((album:abc*)^1.1 (artist:abc)^0.5) "
+ "((album:def)^0.55 artist:def*) ((album:def*)^1.1 (artist:def)^0.5)) "
+ "+(folderId:" + FID1 + ")",
query.toString(), "SearchAlbumId3");
query = queryFactory.search(criteria, MULTI_FOLDERS, IndexType.ALBUM_ID3);
assertEquals(
"+(((album:abc*)^1.1 artist:abc*) ((album:def*)^1.1 artist:def*)) +(folderId:"
+ FID1 + " folderId:"
+ FID2 + ")",
"+(((album:abc)^0.55 artist:abc*) ((album:abc*)^1.1 (artist:abc)^0.5) "
+ "((album:def)^0.55 artist:def*) ((album:def*)^1.1 (artist:def)^0.5)) "
+ "+(folderId:" + FID1 + " folderId:" + FID2 + ")",
query.toString(), "SearchAlbumId3");
}

View file

@ -38,7 +38,13 @@ import static org.springframework.util.ObjectUtils.isEmpty;
* This test case is a FalsePattern for search,
* but there may be problems with the data flow prior to creating the search index.
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
// Default web environment (MOCK) is required: GlobalSecurityConfig.extSecurityFilterChain
// uses MvcRequestMatcher, which needs mvcHandlerMappingIntrospector only available when a
// servlet context is bootstrapped. WebEnvironment.NONE fails ApplicationContext load with
// "No bean named '... mvcHandlerMappingIntrospector ...' available". The siblings
// IndexManagerTestCase and SearchServiceTestCase already rely on default-MOCK for the
// same reason.
@SpringBootTest
@EnableConfigurationProperties
public class SearchServiceSpecialPathTestCase {
@ -49,16 +55,17 @@ public class SearchServiceSpecialPathTestCase {
private List<MusicFolder> getMusicFolders() {
if (isEmpty(musicFolders)) {
// Use the no-id MusicFolder constructor so JPA's IDENTITY generator assigns
// unique IDs at persist time. The original hardcoded ids (1, 2, 3) collided
// with the music_folder rows seeded by Liquibase, producing an
// ObjectOptimisticLockingFailure when createMusicFolder tried to write back
// to an existing row.
musicFolders = new ArrayList<>();
Path musicDir = MusicFolderTestData.resolveBaseMediaPath().resolve("Search").resolve("SpecialPath").resolve("accessible");
musicFolders.add(new MusicFolder(1, musicDir, "accessible", Type.MEDIA, true, Instant.now().truncatedTo(ChronoUnit.MICROS)));
Path music2Dir = MusicFolderTestData.resolveBaseMediaPath().resolve("Search").resolve("SpecialPath").resolve("accessible's");
musicFolders.add(new MusicFolder(2, music2Dir, "accessible's", Type.MEDIA, true, Instant.now().truncatedTo(ChronoUnit.MICROS)));
Path music3Dir = MusicFolderTestData.resolveBaseMediaPath().resolve("Search").resolve("SpecialPath").resolve("accessible+s");
musicFolders.add(new MusicFolder(3, music3Dir, "accessible+s", Type.MEDIA, true, Instant.now().truncatedTo(ChronoUnit.MICROS)));
Path basePath = MusicFolderTestData.resolveBaseMediaPath().resolve("Search").resolve("SpecialPath");
Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
musicFolders.add(new MusicFolder(basePath.resolve("accessible"), "accessible", Type.MEDIA, true, now));
musicFolders.add(new MusicFolder(basePath.resolve("accessible's"), "accessible's", Type.MEDIA, true, now));
musicFolders.add(new MusicFolder(basePath.resolve("accessible+s"), "accessible+s", Type.MEDIA, true, now));
}
return musicFolders;
}
@ -122,7 +129,7 @@ public class SearchServiceSpecialPathTestCase {
.filter(m -> "accessible+s".equals(m.getName()))
.collect(Collectors.toList());
randomAlbums = searchService.getRandomAlbums(Integer.MAX_VALUE, folder03);
assertEquals(1, folder03.size(), "Albums in \"accessible+s\" ");
assertEquals(1, randomAlbums.size(), "Albums in \"accessible+s\" ");
}

View file

@ -335,12 +335,8 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.3</version>
<configuration>
<!-- #55 Temporarily skip a test. -->
<excludes>
<exclude>org.airsonic.player.service.search.*TestCase.java</exclude>
</excludes>
<!-- see https://github.com/mockito/mockito/issues/3037#issuecomment-1930132240-->
<argLine>-javaagent:${net.bytebuddy:byte-buddy-agent:jar}</argLine>
<argLine>-javaagent:${net.bytebuddy:byte-buddy-agent:jar}</argLine>
</configuration>
</plugin>
<plugin>