diff --git a/.claude/docs/ci.md b/.claude/docs/ci.md index 1a64037a1c5..4a48added9a 100644 --- a/.claude/docs/ci.md +++ b/.claude/docs/ci.md @@ -21,6 +21,8 @@ The full Selenide UI suite takes ~1.5h per DB, so it does **not** run on every P **A hang costs one class, not the shard.** `IntegrationTest` carries a class-level **`@Timeout(15, MINUTES)`** (inherited; per test *and* per lifecycle method - the Spring context start is an extension callback and is not counted), so a method that never returns fails its own class and the run continues and reports. The slowest measured method in the suite is 456 s (`CreateNewFileIT`, one `@Test`), so 15 minutes is roughly twice the worst legitimate case; a class needing a different bound declares its own `@Timeout`, which overrides the inherited one (`JavaLspIT`, `JavaDebugIT`, `SchemaTemplateForeignKeyIT` each declare a tighter one). There is deliberately **no `forkedProcessTimeoutInSeconds`**: it bounds the whole fork, and the jobs sharing this configuration range from 13 to 91 minutes, so one value would either be useless or kill the longest job. +**A background race fails nothing (#7581).** Awaitility catches uncaught exceptions from EVERY thread while an `await()` runs and rethrows them on the test thread, and the framework polls through `await()` almost everywhere (`RestAssuredExecutor`, `AwaitilityExecutor`, `IntegrationTest` itself). So a Flowable async-executor thread that could not lock a timer job - a contention outcome Flowable recovers from on the next acquire cycle, with no test frame anywhere in its stack - failed whichever IT happened to be polling at that moment. `IntegrationTest` now registers one global ignore predicate (`Awaitility.ignoreExceptionsByDefaultMatching`) matching exactly that: a `FlowableOptimisticLockingException` raised from `ExecuteAsyncRunnable`, matched by class NAME so the framework keeps no Flowable dependency. Everything else still fails the test, from any thread, and so does anything thrown inside an await's own condition. + **Diagnostics survive a cancel.** `TestStallWatchdog` (registered on `IntegrationTest`) schedules a full `ThreadMXBean.dumpAllThreads(true, true)` - plus `findDeadlockedThreads()` - for **12 minutes** into every test/lifecycle method invocation and cancels it when the invocation returns. It fires *before* the `@Timeout` on purpose: the stalled stack is still live, and the interrupt `@Timeout` uses cannot land on a blocking socket read or a monitor deadlock (where the timeout itself never completes either). The dump is written to `target/failsafe-reports/.-threaddump-.txt` **before** it is logged, because a stall in the logging path would eat the log copy. Every IT job then uploads `tests/tests-integrations/target/{failsafe,surefire}-reports` in an `if: always()` step placed **before** the `Assert integration tests ran` guard - `always()` covers a cancellation, and the guard never runs on one. **A JUnit 5 tag expression only selects if `junit:junit` is off the classpath (#7215/#7218).** Surefire and failsafe 3.6.0 unified the five providers, and the unified provider decides what `` / `` MEAN from the classpath: when the project's dependencies contain `junit:junit`, the mojo auto-adds the Vintage engine and sets the provider property `junit.vintage.engine.detected`, after which the values are read as JUnit 4 `@Category` **class names** rather than a JUnit 5 tag expression. No IT carries a `@Category`, so on 3.6.0 every `-Dit.groups` expression selected nothing - `!no-such-tag` included, which is why it looked like broken tag filtering rather than a different dialect - and each IT job answered `Tests run: 0` and passed: for three days no integration test ran anywhere (PR gate, master push or nightly), the only symptom being the jobs getting faster (3 min against the 13-62 min measured above). `tests/tests-integrations` was the only module affected because it is the only one that SELECTS by tag; the layout (`*IT.java` in `src/main/java` behind the root pom's `` override) had nothing to do with it, and an *exclude*-only filter degrades harmlessly - a category that resolves to nothing excludes nothing - which is why the unit-test jobs and the `testcontainers` container-test job were never dark although `modules/**` carries JUnit 4. The `junit:junit` that triggered it came in transitively from **greenmail**, whose JUnit 4 rule no test here uses; `dependencies/pom.xml` excludes it, and it must stay excluded - putting JUnit 4 back on that classpath silently re-darkens every tagged IT run. Every IT job also carries an **Assert integration tests ran** step that counts `target/failsafe-reports/TEST-*.xml`, because failsafe's own `failIfNoTests` cannot catch this class of failure: scanning does find the IT classes and the filter drops them afterwards. To count what a tag expression selects without running the suite, add `-Djunit.platform.execution.dryRun.enabled=true` - discovery and filtering run in full, every selected test is reported as skipped, and the whole set costs about a minute. diff --git a/tests/tests-framework/src/main/java/org/eclipse/dirigible/tests/base/IntegrationTest.java b/tests/tests-framework/src/main/java/org/eclipse/dirigible/tests/base/IntegrationTest.java index 5fb5a0c8d5b..df7dd6180b7 100644 --- a/tests/tests-framework/src/main/java/org/eclipse/dirigible/tests/base/IntegrationTest.java +++ b/tests/tests-framework/src/main/java/org/eclipse/dirigible/tests/base/IntegrationTest.java @@ -68,6 +68,42 @@ static void useIsolatedDependenciesFolders() { Configuration.set("DIRIGIBLE_MAVEN_LOCAL_REPO", "target/dirigible/m2"); } + /** + * A race on a Flowable async-executor thread must not fail whichever test happens to be inside an + * {@code await()} (issue #7581). + * + *

+ * Awaitility catches uncaught exceptions from EVERY thread while an await runs and rethrows them on + * the test thread, so a job the executor could not lock - which Flowable recovers from on the next + * acquire cycle - failed an IT whose only connection to it was the clock. An assertion should fail + * a test; a background retry should not. The whitelist is narrow on purpose: the one exception + * type, raised from the one class that acquires jobs. Everything else still fails the test, from + * any thread, and so does anything thrown inside an await's own condition. + */ + @BeforeAll + static void toleratePollingJobAcquisitionRaces() { + Awaitility.ignoreExceptionsByDefaultMatching(IntegrationTest::isJobAcquisitionRace); + } + + private static boolean isJobAcquisitionRace(Throwable throwable) { + for (Throwable cause = throwable; cause != null; cause = cause.getCause() == cause ? null : cause.getCause()) { + // matched by NAME: the test framework does not depend on Flowable, and this is the only + // thing it ever needs to know about it + if (!cause.getClass() + .getName() + .endsWith("FlowableOptimisticLockingException")) { + continue; + } + for (StackTraceElement frame : cause.getStackTrace()) { + if (frame.getClassName() + .endsWith("ExecuteAsyncRunnable")) { + return true; + } + } + } + return false; + } + @BeforeAll static void cleanBeforeTestClassExecution() { DirigibleCleaner.deleteDirigibleFolder();