Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<Class>.<method>-threaddump-<ts>.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 `<groups>` / `<excludedGroups>` 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 `<testClassesDirectory>` 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*
* <p>
* 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();
Expand Down
Loading