fix(grails-gsp): eliminate race condition in FormTagLib2Tests.testDatePickerTag#15612
fix(grails-gsp): eliminate race condition in FormTagLib2Tests.testDatePickerTag#15612jamesfredley wants to merge 1 commit into8.0.xfrom
Conversation
…ePickerTag
The testDatePickerTag helper used by every testDatePickerTagWith*Precision()
case rendered the date-picker HTML first, then constructed a fresh
GregorianCalendar afterwards to derive the expected select-field values.
When the two calls fell on opposite sides of a minute (or hour/day/year)
boundary the picker output and the calendar disagreed, producing a flaky
assertion such as:
FormTagLib2Tests > testDatePickerTagWithMinutePrecision()
expected: <true> but was: <false>
at assertSelectFieldPresentWithSelectedValue (FormTagLib2Tests:307)
at validateSelectedMinuteValue (FormTagLib2Tests:297)
at testDatePickerTag (FormTagLib2Tests:237)
Observed in CI on Build Grails-Core (windows-latest, 25) where the slower
runner crossed a minute boundary between the two calls.
Capture the calendar up-front and, when the test passes a null date,
forward calendar.getTime() to the picker so both sides agree on a single
instant. Behaviour for explicit Date arguments is unchanged.
Verified:
./gradlew :grails-gsp:test --tests org.grails.web.taglib.FormTagLib2Tests --rerun-tasks
-> 11 tests, all passing (including testDatePickerTagWithMinutePrecision)
Assisted-by: claude-code:claude-opus-4-7
There was a problem hiding this comment.
Pull request overview
Fixes a flaky FormTagLib2Tests.testDatePickerTagWithMinutePrecision() by ensuring the expected calendar values and the rendered datePicker output are derived from the same instant, avoiding minute/hour/day boundary races on slower CI runners.
Changes:
- Capture a single
Calendar“now” upfront intestDatePickerTag(...)to prevent time drift between rendering and assertions. - When the test passes
nullas the date, compute a resolved date from the captured calendar and render/assert against that same instant.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Calendar calendar = new GregorianCalendar() | ||
| Object resolvedDate = date | ||
| if (date == null) { | ||
| resolvedDate = calendar.getTime() | ||
| } else if (date instanceof Date) { | ||
| calendar.setTime(date) | ||
| } /*else if (date instanceof TemporalAccessor) { | ||
| ZonedDateTime zonedDateTime | ||
| if (date instanceof LocalDateTime) { | ||
| zonedDateTime = ZonedDateTime.of(date, ZoneId.systemDefault()) | ||
| } else if (date instanceof LocalDate) { | ||
| zonedDateTime = ZonedDateTime.of(date, LocalTime.MIN, ZoneId.systemDefault()) | ||
| } else { | ||
| zonedDateTime = ZonedDateTime.from(date) | ||
| } | ||
| calendar = GregorianCalendar.from(zonedDateTime) | ||
| }*/ | ||
|
|
||
| Document document = getDatePickerOutput(resolvedDate, precision, null) | ||
| assertNotNull(document) |
There was a problem hiding this comment.
When date == null, the test now passes an explicit value to datePicker (resolvedDate = calendar.getTime()), which changes the exercised code path: datePicker no longer runs its !value -> value = xdefault defaulting logic. To keep the original behavior ("no value provided" defaults to now) while still removing the race, consider leaving value unset and instead passing the captured instant via the tag's default attribute (i.e., call getDatePickerOutput(null, precision, calendar.getTime()) when date is null, and use calendar for assertions).
|
The suggestion modifies the test to pass null as the date value when date is null, and uses the captured calendar time as the default parameter in getDatePickerOutput. This preserves the original defaulting logic inside the datePicker tag (treating null as 'no value provided' that defaults to now) while eliminating the race condition by using a single captured instant for both the tag output and assertions. Applying it improves the test's reliability without altering the intended behavior. grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/FormTagLib2Tests.groovy grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/FormTagLib2Tests.groovy |
✅ All tests passed ✅🏷️ Commit: 61b7d3c Learn more about TestLens at testlens.app. |
jdaugherty
left a comment
There was a problem hiding this comment.
Lets shorten then comment
|
|
||
| private void testDatePickerTag(Object date, String precision) { | ||
| Document document = getDatePickerOutput(date, precision, null) | ||
| // Capture a single "now" instant up-front so that the picker output |
There was a problem hiding this comment.
Shorten this comment, its just captured upfront to prevent test pillution
Summary
Eliminates the race condition in
FormTagLib2Tests.testDatePickerTagthat produced an intermittenttestDatePickerTagWithMinutePrecision()failure on slow runners (most recently onBuild Grails-Core (windows-latest, 25)in PR #15467 / run 25170545020).What was wrong
testDatePickerTag(Object date, String precision)rendered the date-picker HTML first, then constructed a freshGregorianCalendarafterwards to derive the expected select-field values. When the two calls fell on opposite sides of a minute (or hour/day/year) boundary, the picker output and the calendar disagreed, producing a flaky assertion such as:Fix
Capture the calendar up-front and, when the test passes a
nulldate, forwardcalendar.getTime()to the picker so both sides agree on a single instant. Behaviour for explicitDatearguments is unchanged.getDatePickerOutputsimply forwards the supplied value to thedatePickertag, which itself defaults tonew Date()when no value is provided, so passing the capturedcalendar.getTime()exercises the same code path asnulldid before but without the race window.Verification
All 11 tests pass on Windows (the runner that exhibited the original flake), including the previously flaky
testDatePickerTagWithMinutePrecision().