Repository navigation
[refactor] Replace XSuite with a JUnit Platform engine, remove JUnit 4, move test support to the test-jar - #6791
Merged
line-o merged 13 commits intoOct 6, 2026
Conversation
duncdrum
added this pull request to stack #6792
October 1, 2026 10:20
duncdrum
force-pushed
the
refactor/exist-core-test-scope
branch
from
October 1, 2026 10:35
8dfbf1a to
3c2102e
Compare
Contributor
📊 XQTS result comparisonComparison of this run against
Relative to Runtime: 439.7s (-60.66s vs |
duncdrum
force-pushed
the
refactor/exist-core-test-scope
branch
from
October 1, 2026 13:11
6bbf1d7 to
41cbbc8
Compare
duncdrum
marked this pull request as ready for review
October 1, 2026 20:26
duncdrum
force-pushed
the
refactor/exist-core-test-scope
branch
from
October 1, 2026 20:45
c55bb6c to
5b8b541
Compare
6 of 7 tasks
duncdrum
force-pushed
the
refactor/exist-core-test-scope
branch
from
October 5, 2026 12:14
2d8ddb2 to
723f422
Compare
5 tasks done
dizzzz
approved these changes
Oct 5, 2026
org.exist.test (server fixtures, assertion helpers, the XQSuite runner) lives in exist-core's main source tree, so exist-core declared junit, hamcrest, junit-jupiter-api, opentest4j and xmlunit-core at compile scope. That leaked them transitively to every consumer and into the server distribution. Declare them provided instead: exist-core still compiles against them, but they are no longer transitive. No production code outside org.exist.test uses them, and modules that run tests already declare what they use (the reactor build and dependency analyzer confirm it). xmlunit-core stays in the distribution only via exist-xmldiff, which declares it directly. The distribution no longer ships hamcrest, junit-jupiter-api, junit-platform-commons or opentest4j. exist-xqts is the one consumer that relied on the transitive libraries at runtime: the external exist-xqts-runner loads ExistEmbeddedServer (which implements the Jupiter extension callbacks) and JUnit classes at startup without declaring them, so exist-xqts now declares junit-jupiter-api and junit explicitly. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KqkMrHZx1uvZFuwvMrc3gj
The XQuery side of an XQSuite or XML test run reports each test's outcome by calling six small Java functions (started, finished, ignored, failure, error, assumption-failed). They talked directly to a JUnit 4 RunNotifier and built JUnit 4 Description and Failure objects. Introduce a TestEvents sink and have the functions report to it instead. RunNotifierTestEvents adapts it back to the RunNotifier, so XSuite behaves exactly as before. This is the seam a JUnit Platform engine plugs into, and removes the last JUnit 4 types from the callback functions. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KqkMrHZx1uvZFuwvMrc3gj
Add XQSuiteTestEngine, a JUnit Platform TestEngine that runs XQSuite and XML test files, as the replacement for the JUnit 4 XSuite runner. Classes are marked with @XQSuite(paths...) instead of @RunWith(XSuite.class) plus @XSuiteFiles. The engine reuses the existing XQuery runners through the TestEvents sink, so test execution itself is unchanged. The tree is suite class -> test file -> test. Each test function is its own test, including functions that share a name within a file (the first version collapsed them and reported 67 instead of 91 tests for XQSuiteTests; the old runner and the engine now report identical test names). Anything that would otherwise vanish is reported: a suite whose files cannot be discovered fails instead of disappearing, and tests the XQuery side never reports are failed rather than silently dropped. XQSuiteTests is the first suite converted. XQSuiteTestEngineTest drives the engine with EngineTestKit for a pass, assertion and error failures, node results in failure messages, an empty file and a discovery failure. The engine does not yet support parallel suites, the hang watcher, or JUnit 4 style class rules; the remaining suites still use XSuite. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KqkMrHZx1uvZFuwvMrc3gj
Convert the 37 suite classes in exist-core and 14 extension modules from @RunWith(XSuite.class) to @XQSuite, and give the extension modules the exist-core test-jar that carries the annotation and the engine. HttpClientXqueryTests manages its WireMock server in @BeforeAll/@afterall instead of a JUnit 4 class rule. Comparing the engine with the old runner suite by suite found two places where the old runner's discovery disagreed with the names the XQSuite runtime reports. JUnit 4 hid this because it only counts tests that start; the engine fails any test it discovered but never saw reported: - A test without %test:name keeps its module prefix unless the prefix is purely word characters (xqsuite.xql, test:get-test-name), so a hyphenated prefix gives "prefix:name". Discovery now applies the same rule, in the compile path and in xquery-discovery.xq (which also ignored %test:name). - XML tests are named from the raw text of the task element; discovery trimmed it. Discovery now keeps the raw text so test IDs are unchanged. Discovery also runs through an embedded database (started only when a file is not yet cached), as before, because some modules cannot be compiled without one. Result: exist-core 3023 tests, 39 skipped; the 14 extension modules 1471 tests, 61 skipped; zero failures. These are identical, module by module, to the totals of the old runner. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KqkMrHZx1uvZFuwvMrc3gj
All suites run on the JUnit Platform engine, so delete XSuite and the adapter that bridged the XQuery callbacks to a JUnit 4 RunNotifier. AbstractTestRunner and its XQuery and XML implementations are now plain classes that report to TestEvents; InitializationError is replaced by TestInitializationException, and TestRunners creates the runner for a file. XSuite's parallel mode and hang watcher are not carried over: no suite used @XSuiteParallel, only the runner's own test fixtures did. The runner's self-tests move to the new API: suite and test names, discovery through the database and by compiling giving identical names (including a hyphenated module prefix), raw task text for XML test names, and the engine tests now also cover navigating from a failure to the XQuery file or the Java code. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KqkMrHZx1uvZFuwvMrc3gj
Nothing runs on JUnit 4 any more, so remove it from the build and make sure it cannot come back unnoticed. With the Vintage engine gone a new JUnit 4 test would compile but silently never run. - Remove the junit:junit test dependencies, the Vintage engine from the surefire plugin, and the 52 blanket junit exclusions that the migration recipe had spread over 42 poms. Exclusions remain only where a library really brings JUnit 4: ws-commons-util (compile scope, would otherwise ship in the distribution), greenmail and xmlunit-legacy. - Add a ban-junit4 enforcer rule: junit:junit at compile, provided or test scope, the Vintage engine and junit-toolbox fail the build. Two documented exceptions override it per module: exist-xqts (the external XQTS runner brings junit through ant-junit and needs it at runtime) and exist-core, lucene and ngram, whose tests use xmlunit-legacy, whose XMLAssert and XMLTestCase extend junit.framework.Assert. Migrating those tests to XMLUnit 2 changes comparison semantics and is left for its own change. - exist-ant: replace Ant's BuildFileRule (a JUnit 4 ExternalResource) with a small extension that configures the project, executes targets and runs the tearDown target as the rule did, and drop ant-testutil. - Declare junit-platform-commons for the engine and suppress the supertype-only junit:junit in the dependency analyzer. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KqkMrHZx1uvZFuwvMrc3gj
Bring back the two features of the old XSuite runner, redesigned. @XQSuite(parallel = true) runs the test files of a suite concurrently against the one embedded database, at most exist.xqsuite.parallelism at a time (default: the processor count, capped by the broker pool, between 2 and 32). The tests within a file stay sequential. It is opt-in because the files of a suite share the database: XQuery3Tests, whose files drop and recreate the same collection, fails with the option set and is not faster, so no suite uses it yet. Hang detection now covers every suite, sequential or not. Each file runs on a thread of its own and a watcher fails a file that has reported nothing for exist.xqsuite.hang.threshold.minutes (default 5). The failure names the file and the tests that were running, the file's remaining tests are failed, its query is killed and its thread interrupted, and the other files carry on (the old runner cancelled everything). Whatever the abandoned thread reports later is ignored. Giving up on a file is reliable but stopping a spinning query is not: eXist only stops queries where they check for being killed, and some loops, also inside the test runner, never do. Such a thread keeps the database from shutting down, so the engine bounds the shutdown, fails the suite loudly and skips the suites after it instead of hanging. The old runner's test for this never made anything hang. The new tests read the engine's event timeline: files overlap if and only if run in parallel, the parallelism setting caps them, a hung file is failed while the others still run, in both modes, and it finishes exactly once. The bounded shutdown and the settings are tested on their own. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KqkMrHZx1uvZFuwvMrc3gj
The first real suite to use @XQSuite(parallel = true). Its 12 files only evaluate expressions on numbers: none touches the database or uses a function with side effects, so they are independent by construction, not just by chance. Measured: 40 consecutive runs through the engine gave 72 of 72 passing every time, and files ran concurrently in every run (up to 10 at once). Other suites were tried and are not safe to run in parallel because their files share database state: CoreTests (147 to 150 failures), RangeTests (63 to 67), LuceneTests (2 to 5 failures and errors), XQuery3Tests (files drop and recreate the same collection, and 150 of its 157 seconds are one file, so it could not be faster anyway). DateTests, NumberTests and UtilTests are also stable but too small to matter. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KqkMrHZx1uvZFuwvMrc3gj
org.exist.test (the embedded and web server fixtures, assertion helpers and the XQuery test runners) lived in exist-core's main tree, so it was part of the production jar. Move it, and the three XQuery files the runners load, to src/test; consumers get it from the exist-core test-jar. With nothing in main using them, hamcrest, junit-jupiter-api, opentest4j and xmlunit-core become plain test dependencies of exist-core. The 15 modules that used the fixtures without the test-jar now depend on it (the two JMH modules at compile scope, because their benchmarks are main code), and exist-xqts has it at runtime for the external XQTS runner. Three javadoc links from main code to the moved classes become code references, and the license plugin's paths for DiffMatcher follow it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KqkMrHZx1uvZFuwvMrc3gj
Move RUNNERS up, throw IllegalStateException instead of the raw
RuntimeException, and spell out the NaN check that !(value > 0) was
doing: Double.parseDouble("NaN") succeeds, so a NaN threshold had to
stay rejected rather than become a 0ms threshold.
The four assert suppressions are false positives - those tests assert
through assertStatistics, which Codacy's PMD does not recognise.
… tidy static imports Apply the JUnit5 AssertTrueInstanceofToAssertInstanceOf cleanup to the affected test sources: rewrite assertTrue(x instanceof Y) to assertInstanceOf(Y.class, x) and sort the JUnit Jupiter static import block alphabetically, replacing any collapsed wildcard import with the explicit set of Assertions methods actually used. 36 test files. exist-core test suite (7379 tests) and the mail/lucene/ restxq modules compile and test clean.
exist-core's test-jar already provides the identical file; with their own copy these modules logged "Discovered 2 'junit-platform.properties'". Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… classpath exist-core-jmh and exist-indexes-jmh did not compile with -Pperf-tests: the benchmarks (main code) use ExistEmbeddedServer, which since the test support code moved into the exist-core test-jar implements the Jupiter extension callbacks, so javac needs the Jupiter API to resolve its supertypes. exist-core-jmh declared it with test scope and exist-indexes-jmh not at all. CI does not build the perf-tests profile, so nothing noticed. Both modules now declare junit-jupiter-api at compile scope. The dependency analyzer sees it neither used by the main bytecode nor used by anything but tests, so it is listed in the ignores for unused declared and for non-test scoped dependencies, with the reason. Checked with: mvn compile dependency:analyze-only@analyze -pl exist-core-jmh,exist-indexes-jmh -am -Pperf-tests Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
duncdrum
force-pushed
the
refactor/exist-core-test-scope
branch
from
October 6, 2026 06:58
723f422 to
cea85e3
Compare
Open
4 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Part of #6037. After #6790 the only thing keeping JUnit 4 alive was
XSuite, the runner for the XQSuite and XML tests (a JUnit 4ParentRunner). This PR replaces it with a JUnit Platform engine, converts all 37 suites, then removes JUnit 4 and the Vintage engine from the build and bans them. It also moves the test support code (org.exist.test.*) out ofexist-core's production jar into its test-jar, so test libraries stop leaking to consumers and into the server distribution.What changed
providedinexist-core, so the distribution stops shipping hamcrest, jupiter-api, platform-commons and opentest4j .TestEventssink. The six XQuery callbacks report to a framework-neutral interface instead of a JUnit 4RunNotifier.XQSuiteTestEngineand@XQSuite. One test per test function. A suite that cannot be discovered fails instead of vanishing; a test that was discovered but never reported fails instead of being silently dropped.exist-core, 14 extension modules);XSuitedeleted.exist-antgets a small helper instead of Ant's JUnit 4BuildFileRule.exist-coretest-jar. The server fixtures, assertion helpers and test runners (26 files, moved with history) leave the production jar; the four test libraries become plaintestscope. 15 modules gain the test-jar dependency (the two JMH modules at compile scope, since their benchmarks are main code), andexist-xqtsgets it at runtime for the external XQTS runner, which also needsjunit-jupiter-apiandjunitdeclared.assertTrue(x instanceof y)converted toassertInstanceOfwith the static imports tidied, and the identicaljunit-platform.propertiesremoved fromluceneandrestxq(their copy and the one from theexist-coretest-jar made surefire warn that only the first of two files is used).-Pperf-tests.exist-core-jmhdeclaredjunit-jupiter-apiwith test scope andexist-indexes-jmhnot at all, but the benchmarks (main code) useExistEmbeddedServer, which now implements the Jupiter extension callbacks. CI does not build that profile, so it went unnoticed. Both modules now declare the API at compile scope, with the dependency analyzer ignores and the reason; checked withmvn compile dependency:analyze-only@analyze -pl exist-core-jmh,exist-indexes-jmh -am -Pperf-tests.Same tests as the old runner
By console totals:
exist-core3,023 tests (39 skipped); the 14 extension modules 1,471 (61 skipped); 0 failures; identical module by module. Comparing the two found mismatches JUnit 4 had hidden, because it only counts tests that start: same-named test functions collapsed into one, hyphenated module prefixes, raw<task>text in XML test names, and modules that cannot be compiled without a database. All fixed.Parallelism: what we found
Update: #6796 (stacked on this PR) addresses these findings: it gives test files their own collections and makes
CoreTestsandXQuery3Testsparallel, and records what still blocks the other suites.@XQSuite(parallel = true)runs a suite's files concurrently (tests within a file stay sequential), capped byexist.xqsuite.parallelism. It is opt-in, because parallel is only safe for suites whose files do not share database state, and almost none are:parallel = trueNumericOpTests(12 files, no database access)CoreTestsRangeTestsLuceneTestsXQuery3TestsXQuery3Tests' 157 seconds are one file (transform/catalog.xql); the other 100 files take about 7 seconds together, so per-file parallelism cannot speed it up. Why that one file is slow is a separate question.NumericOpTestsis enabled to keep the feature exercised on a real suite in CI, not for speed.Hang detection
Applies to every suite. A file that reports nothing for
exist.xqsuite.hang.threshold.minutes(default 5) is failed with its name and the tests that were running; the others carry on (the old runner cancelled everything). The old runner's test never actually hung anything; the new ones use fixtures that really do.Limit: giving up on a hung file is reliable, stopping its query is not. eXist only stops a query where it checks for being killed, and some loops never do. Such a thread keeps the database from shutting down, so the engine bounds the shutdown, fails the suite and skips the ones after it, instead of hanging as before.
Behaviour changes
XSuiteand@XSuiteFilesare gone; use@XQSuitefrom theexist-coretest-jar.org.exist.test.*(ExistEmbeddedServer,ExistWebServer, ...) is no longer in theexist-corejar; external users need theexist-coretest-jar (<type>test-jar</type>). The only external user found, the XQTS runner, is covered throughexist-xqts.XQSuiteTests) instead ofxqts.org.exist-db.... Test names are unchanged.exist.xqsuite.parallelism,.hang.threshold.minutes,.hang.watcher.interval.seconds,.hang.grace.seconds.Not in this PR
xmlunit-legacy: tests inexist-core, lucene and ngram still use XMLUnit 1 assertions, which extend JUnit 3'sAssert, so those three modules keepjunit:junitat test scope (the one documented exception to the ban). Migrating about 100 assertions changes comparison semantics and deserves its own review.Test plan
verifypasses (compile, license, dependency analyzer, JUnit 4 ban)exist-coreintegration tests and the XQTS runner's start-up checked on the moved code (isolated build)developat9c639a0ce6on 2026-10-05 (no conflicts;VectorSearchTestskeeps thequery-vector-k.xqmthat Fix ft:query-vector k being ignored / throwing past 341 docs #6741 added); reactortest-compile, and the Lucene module (951 tests) and the engine, runner,CoreTestsandXQuery3Testsruns (2106 tests), all with 0 failures, run at the top of the stack ([test] Isolate XQSuite test files, run CoreTests and XQuery3Tests in parallel #6796)mvn compile dependency:analyze-only@analyze -pl exist-core-jmh,exist-indexes-jmh -am -Pperf-testspassesclose #6037
🤖 Generated with Claude Code
https://claude.ai/code/session_01KqkMrHZx1uvZFuwvMrc3gj