Add test coverage and fix lint for GORM domain event listeners - #16151
Add test coverage and fix lint for GORM domain event listeners#16151borinquenkid wants to merge 10 commits into
Conversation
DomainEventListener, DefaultApplicationEventPublisher, and ConfigurableApplicationContextEventPublisher had zero test coverage. Adds mock-based Spock specs driving each class through its public constructor/method/onApplicationEvent surface only. Coverage: DomainEventListener 0% -> 92% lines / 91% branches / 96% methods; DefaultApplicationEventPublisher 0% -> 95% / 81% / 100%; ConfigurableApplicationContextEventPublisher 0% -> 100% / n/a / 100%. Three branches are documented as intentionally left uncovered in DomainEventListenerSpec: invokeEvent's ea==null path is unreachable via the public API, and its eventMethod.getParameterTypes().length==1 path is confirmed (via decompiling spring-core) structurally dead — findAndCacheEvent only ever caches zero-argument hook methods, so event-argument-accepting hooks can never be invoked.
The existing AutoTimestampEventListenerSpec only drove the suppression logic and beforeInsert/beforeUpdate directly via a test subclass that bypassed initForMappingContext entirely, leaving construction, real entity/property scanning (storeDateCreatedAndLastUpdatedInfo, including @CreatedDate/@LastModifiedDate/@CreatedBy/@LastModifiedBy annotation detection), the deferred-initialization path, setApplicationContext, supportsEventType, and onApplicationEvent dispatch untested. Coverage: 84% -> 98% lines, 61% -> 83% branches, 76% -> 96% methods.
invokeEvent's eventMethod.getParameterTypes().length == 1 branch could never be taken: findAndCacheEvent caches hooks via Spring's ReflectionUtils.findMethod(Class, String), which only ever matches zero-argument methods (confirmed by decompiling spring-core), so a cached eventMethod can never have one parameter. Dropped the branch and the now-unused ApplicationEvent parameter it required, along with the argument at all 8 call sites. No behavior change; full module suite, jacoco, and codeStyle all clean.
- entityEvents field made final (never reassigned) - Removed ZERO_PARAMS constant (zero references anywhere in the repo) - Parameterized raw ConnectionSourcesProvider/Class usages - supportsEventType now null-safe: Spring 7's SmartApplicationListener.supportsEventType declares its parameter @nullable, and this override would NPE via Class.isAssignableFrom(null) - invokeEvent's ea.refresh() call is now guarded by the same ea != null check already used earlier in the method, closing a latent NPE path - Removed the two switch branches in onPersistenceEvent (SaveOrUpdate, Validation) that were duplicates of the default branch - Reordered each before/after method pair so the 2-arg overload holds the real logic and is canonical; the 3-arg overload (whose event parameter has been unused since the dead 1-arg-hook branch was removed) is now @deprecated, forwards to the 2-arg overload, and is scheduled for removal in 9.0. onPersistenceEvent now calls the 2-arg overloads directly instead of the newly-deprecated 3-arg ones. Added tests for the deprecated overloads' delegation and the null eventType case. Full suite, jacoco, and codeStyle all clean.
Rather than defensively guarding against null, declare the contract explicitly via jspecify's @nonnull and let a null argument fail fast with an NPE. Updated the corresponding spec to assert the NPE instead of a graceful false return.
- supportsEventType is now null-safe, matching Spring 7's SmartApplicationListener.supportsEventType @nullable contract - Parameterized every raw Class/List<Class> usage across the withoutLastUpdated/withoutDateCreated/withoutTimestamps overloads and their shared runWithDisabled helper - Replaced disabled.entityNames.removeAll(added) (a HashSet.removeAll of a List, which can fall into an O(n*m) path depending on relative collection sizes) with a direct per-element remove, guaranteeing O(1) removals regardless of size getTimestampProvider() was also flagged as unused but left as-is: it's the getter half of a real getter/setter bean-property pair (the setter is the intended extension point for injecting a custom TimestampProvider), not dead code. Full suite, jacoco, and codeStyle all clean.
publishEvent(ApplicationEvent) and publishEvent(Object) both iterated applicationListeners and applied the same SmartApplicationListener event/source-type filtering before dispatching; only the event-wrapping step differed. Extracted the shared iterate-filter-dispatch logic into a private dispatch(ApplicationEvent) method both overloads now call. Also narrows ConfigurableApplicationEventPublisher.addApplicationListener's parameter to ApplicationListener<? extends ApplicationEvent>, matching ConfigurableApplicationContextEventPublisher's already-narrower signature. No behavior change; existing DefaultApplicationEventPublisherSpec coverage (95%/81%) verifies both overloads unchanged.
Renamed the local RxDatastoreClient variable in onApplicationEvent from datastoreClient to sourceClient, since it shadowed the class's own datastoreClient field (the source of the "might not be assigned" confusion) and made the two equals() calls read ambiguously. Both this.datastoreClient.equals(datastoreClient) calls are now datastoreClient == sourceClient, Groovy's idiomatic null-safe equals. No behavior change; full suite, jacoco, and codeStyle clean.
…tListener Same idiomatic Groovy null-safe equals fix already applied to MultiTenantEventListener. No behavior change; full suite, jacoco, and codeStyle clean.
There was a problem hiding this comment.
Pull request overview
This PR strengthens and modernizes the GORM domain event listener infrastructure by adding substantial Spock test coverage, removing a confirmed-dead reflective invocation path, and making small API and lint/IDE-warning cleanups across the org.grails.datastore.gorm.events package (plus corresponding Rx listener adjustments).
Changes:
- Adds new mock-based Spock specs covering
DomainEventListener,AutoTimestampEventListener, and the application event publisher implementations. - Simplifies domain hook invocation by removing a dead “event-argument” hook path and deprecating the now-vestigial 3-arg hook overloads (event parameter).
- Refactors event publishing/listener handling and updates Rx listeners to align with the core changes and reduce IDE/lint warnings.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| grails-datamapping-rx/src/main/groovy/org/grails/gorm/rx/events/MultiTenantEventListener.groovy | Replaces explicit .equals() usage and clarifies source client naming in event handling. |
| grails-datamapping-rx/src/main/groovy/org/grails/gorm/rx/events/DomainEventListener.groovy | Uses Groovy == for datastore client comparisons in source validation. |
| grails-datamapping-rx/src/main/groovy/org/grails/gorm/rx/events/AutoTimestampEventListener.groovy | Uses Groovy == for datastore client comparisons in source validation. |
| grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/DomainEventListenerSpec.groovy | Adds comprehensive unit coverage for domain event listener behavior and hook dispatch. |
| grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/DefaultApplicationEventPublisherSpec.groovy | Adds tests for listener dispatch and SmartApplicationListener filtering behavior. |
| grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/ConfigurableApplicationContextEventPublisherSpec.groovy | Adds delegation tests for the application-context-backed event publisher. |
| grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListenerConstructionSpec.groovy | Adds construction + scanning behavior tests, plus event dispatch coverage for AutoTimestamp listener. |
| grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/events/DomainEventListener.java | Removes dead reflective hook path, updates dispatch to call canonical 2-arg hooks, and deprecates 3-arg overloads. |
| grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/events/DefaultApplicationEventPublisher.groovy | Deduplicates publish paths into a shared dispatch method and tightens listener API generics. |
| grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/events/ConfigurableApplicationEventPublisher.groovy | Narrows listener API type bound to ApplicationEvent. |
| grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListener.java | Tightens generics, adjusts disabled-timestamps cleanup, and updates supportsEventType signature/formatting. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| public boolean supportsEventType(@NonNull Class<? extends ApplicationEvent> eventType) { | ||
| return AbstractPersistenceEvent.class.isAssignableFrom(eventType); | ||
| } |
There was a problem hiding this comment.
Checked this against Spring 7.0.8's actual SmartApplicationListener source: supportsEventType's eventType parameter is documented never null, not @Nullable — it's the sibling supportsSourceType(sourceType) method whose parameter is @Nullable. I'd mixed the two up in the PR description (now corrected). Since nothing in the real dispatch path ever calls supportsEventType(null) (Spring's multicaster and this PR's own DefaultApplicationEventPublisher.dispatch always pass event.getClass()), the @NonNull annotation + fail-fast NPE here is the correct contract, consistent with this PR's theme of removing defensive code for guarantees the framework already provides. Leaving as-is.
| public boolean supportsEventType(@NonNull Class<? extends ApplicationEvent> eventType) { | ||
| return PreInsertEvent.class.isAssignableFrom(eventType) || PreUpdateEvent.class.isAssignableFrom(eventType); | ||
| } |
There was a problem hiding this comment.
Same finding as on the DomainEventListener copy of this method: Spring 7's SmartApplicationListener.supportsEventType's eventType is documented never null (it's supportsSourceType's sourceType that's @Nullable, not this one). The PR description was wrong about which parameter is nullable — fixed now. @NonNull + fail-fast here is intentional and correct; no code change needed.
| void "supportsEventType throws on a null event type, per its @NonNull contract"() { | ||
| given: | ||
| DomainEventListener listener = new DomainEventListener(plainDatastore(Stub(MappingContext) { getPersistentEntities() >> [] })) | ||
|
|
||
| when: | ||
| listener.supportsEventType(null) | ||
|
|
||
| then: | ||
| thrown(NullPointerException) | ||
| } |
There was a problem hiding this comment.
Same root cause as the two supportsEventType comments above: I mischaracterized the Spring contract in the PR description. SmartApplicationListener.supportsEventType's eventType parameter is documented never null in Spring 7.0.8 — it's supportsSourceType's sourceType param that's actually @Nullable. So this spec correctly asserts the fail-fast NPE, matching the real (non-nullable) contract rather than a nullable one. Description corrected; test left as-is.
| if (listener instanceof SmartApplicationListener) { | ||
| SmartApplicationListener smartApplicationListener = (SmartApplicationListener) listener | ||
| if (!smartApplicationListener.supportsEventType((Class<ApplicationEvent>) eventObject.getClass())) { | ||
| if (!smartApplicationListener.supportsEventType((Class<ApplicationEvent>) event.getClass())) { | ||
| continue |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 8.1.x #16151 +/- ##
==================================================
+ Coverage 52.8254% 53.2015% +0.3760%
- Complexity 18871 19402 +531
==================================================
Files 2079 2080 +1
Lines 97207 98986 +1779
Branches 16873 17358 +485
==================================================
+ Hits 51350 52662 +1312
- Misses 38446 38777 +331
- Partials 7411 7547 +136
🚀 New features to boost your workflow:
|
event.getClass() already statically types as Class<? extends ApplicationEvent> here, matching supportsEventType's parameter type, so the explicit cast was redundant. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (1)
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/events/DefaultApplicationEventPublisher.groovy:48
- DefaultApplicationEventPublisher.publishEvent(Object) always wraps the argument in a PayloadApplicationEvent, even when the caller passes an ApplicationEvent instance. This diverges from Spring’s ApplicationEventPublisher behavior (publish ApplicationEvent as-is, otherwise wrap as a payload) and can change listener filtering/dispatch if Java code calls the Object overload with an ApplicationEvent-typed-as-Object.
@Override
void publishEvent(Object event) {
dispatch(new PayloadApplicationEvent<Object>(this, event))
}
🚨 TestLens detected 1 failed test 🚨Here is what you can do:
Test SummaryCI / Functional Tests (Java 21, indy=false, shard 1) > :grails-test-examples-gsp-sitemesh3:integrationTest
🏷️ Commit: dcdb614 Test FailuresEndToEndSpec > async multiple levels of layouts (:grails-test-examples-gsp-sitemesh3:integrationTest in CI / Functional Tests (Java 21, indy=false, shard 1))
Muted TestsSelect tests to mute in this pull request:
Reuse successful test results:
Click the checkbox to trigger a rerun:
Learn more about TestLens at testlens.app. |
Summary
org.grails.datastore.gorm.events(core), which had little to no coverage:DomainEventListener0%→93% lines/93% branches,AutoTimestampEventListener84%/61%→98%/83%,DefaultApplicationEventPublisher0%→95%/81%,ConfigurableApplicationContextEventPublisher0%→100%DomainEventListener.invokeEvent: the event-argument-accepting hook branch could never execute sincefindAndCacheEventonly ever caches zero-argument methods (confirmed by decompiling spring-core'sReflectionUtils.findMethod)eventparameter on the 8 public 3-argbefore*/after*overloads (scheduled for removal in 9.0); the 2-arg overloads are now canonical andonPersistenceEventcalls them directly.equals()→==, aSet.removeAll(List)performance smell, duplicatedpublishEventoverload bodies deduplicated into a shared method, and an explicit@NonNullcontract onsupportsEventType'seventTypeparameter, matchingSmartApplicationListener's javadoc ("never null"); only its siblingsupportsSourceType'ssourceTypeparameter is@Nullablein Spring 7 —supportsEventTypewas previously guarding against a null that the framework never actually passes, so the guard is now removed in favor of a documented, fail-fast contractTest plan
:grails-datamapping-core:testfull suite green:grails-datamapping-rx:testfull suite green (verifies the rx subclasses still compile/pass against the changed core signatures):grails-datamapping-core:codeStyle/:grails-datamapping-rx:codeStyle— zero Checkstyle/CodeNarc violations