fix(reports): keep a passed configuration's snapshot for the reports that list it - #3421
fix(reports): keep a passed configuration's snapshot for the reports that list it#3421juherr wants to merge 1 commit into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughConfiguration parameter snapshots remain available through report generation when a late reader requests retention. Snapshot discard still removes values when no late reader exists. Tests verify invocation-time values, rendering counts, late reads, and cleanup. ChangesConfiguration snapshot retention
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to The change preserves passed configuration snapshots for late reports and includes targeted regression coverage; no actionable merge-blocking risk remains after normal checks and review. Sequence Diagram(s)sequenceDiagram
participant TestInvocation
participant ParameterSnapshotReader
participant ParameterSnapshots
participant XmlReporter
TestInvocation->>ParameterSnapshots: capture invocation parameters
ParameterSnapshotReader->>ParameterSnapshots: request capture held until reporting
XmlReporter->>ParameterSnapshots: read snapshot during generateReport
XmlReporter->>ParameterSnapshots: detachFrom after reporting
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 37.14% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 35 functions across 8 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…that list it ParameterSnapshotRecorder dropped the snapshot of a configuration method the moment it succeeded, on the premise that only failed and skipped configurations are ever listed again. That was true when it was written. XMLSuiteResultWriter writes getPassedConfigurations() and runs during IReporter#generateReport, long after the drop, so it fell back to re-rendering ITestResult#getParameters() -- the late read the snapshots exist to remove. A @BeforeMethod handed the row its test method will run with therefore reported what it left behind: VerboseReporter printed [before-configuration] and testng-results.xml said [mutated], for the same invocation of the same run. The store now records whether anything reads it once the invocations are over. That distinction already existed in the wiring and was simply not kept: requestCaptureIfAnyReads asks on behalf of reporters that only run at generateReport, while TextReporter and VerboseReporter ask for themselves from onStart. Discarding is correct for the second and wrong for the first, so discard consults the store rather than the caller -- the recorder knows the live reporters are done, only the store knows whether one that has not run yet still wants it. Two independent monotone flags rather than one ordered value: folding them would turn two race-free writes into a read-modify-write, and <suite parallel="tests"> makes those calls from two runners at once. The value is now rendered once instead of captured, dropped and rendered again by the fallback, on the most frequent invocations of a run. A run whose only readers sit in the invocation lifecycle still drops what they are finished with, so it retains nothing it has no use for. Also corrects two comments that claimed more than the code does. The one on onConfigurationSuccess is what made this wrong in the first place. The one on TestRunner.addInternalConfigurationListener repeats its ordering claim, which holds only while nothing reorders the listeners: ListenerComparator sorts the list before it is reversed, preferential listeners are merged in afterwards, and the invoker appends the listener that files the result into m_passedConfigurations after the reversal. The fix no longer depends on that order for the reports that read late.
6af5899 to
fa2d98b
Compare
Phase 7b of #3406.
#3416 made
XMLSuiteResultWriterread the invocation-time snapshots. That turned a true premiseinto a false one.
ParameterSnapshotRecorder.onConfigurationSuccessdiscards the snapshot of a configuration methodthat passed, and said why:
XMLSuiteResultWriterwritestestContext.getPassedConfigurations(), and it runs duringIReporter#generateReport, long after the discard. So it found no snapshot and fell back toITestResult#getParameters()— the late read the migration exists to remove.On the existing
PassingConfigurationParameterSample, a@BeforeMethodhanded the row its testmethod will run with, which mutates it and passes. One default run, before this change:
Contradictory answers from the same run, and the XML one is wrong.
The fix
The store now records whether anything will read it after the invocation lifecycle is over. That
distinction already existed in the wiring and was simply not kept:
ParameterSnapshotReader.requestCaptureIfAnyReadsasks on behalf of reporters that only run atgenerateReport;ParameterSnapshots.requestCaptureForis called byTextReporterandVerboseReporter, whichsit in the invocation lifecycle.
Discarding is correct for the second and wrong for the first, so
discardconsults the store ratherthan the caller. The recorder holds only half the information — it knows the live reporters have
been told; only the store knows whether one that has not run yet still wants it. Having the recorder
scan the run's reporters instead would put a run-wide walk on the single most frequent event in a
suite.
Two independent monotone flags rather than one ordered value. Both are writes of
trueand neitherclears the other, so a run with both kinds of reader gets the same answer whichever asks first.
Folding them into one field updated to a maximum would turn two race-free writes into a
read-modify-write, and
<suite parallel="tests">makes those calls from two runners at once.What a large suite now retains, and whether it is bounded
Bounded by what TestNG already holds, and dominated by it.
A configuration method that declares no parameter — the common
@BeforeMethod()— retainsnothing at all:
Parameters.createParametersForMethodreturnsnew Object[0],ParameterSnapshot.ofanswers
nullfor an empty array, andcaptureIfAbsentstores only non-null. That is the frequentcase, and it costs zero bytes.
For one that does declare a parameter, the retained graph is the rendered text plus roughly 200
bytes of map entry and wrappers — about 250 B for
@BeforeMethod(Method m), about 400 B for@BeforeMethod(ITestResult r). Injected values do land inresult.getParameters()for aconfiguration method, so those shapes are included. A 10,000-test suite with a parameterised
@BeforeMethodand@AfterMethodretains on the order of 5 MB, against the 4–8 MB ofTestResultobjects the same run already held for the same results.
It is bounded because those
ITestResults are already retained for the whole run inTestRunner'spassed-configurations
ResultMap— that is whereXMLSuiteResultWriterreads them — and nothingevicts it before reporting:
MethodHelper.clearclears a name cache, and memory-friendly mode dropsmethod maps, not result maps. The map is identity-keyed, so it holds one entry per invocation, the
same granularity as the snapshot store.
ParameterSnapshots.detachFromempties the store in afinallyimmediately aftergenerateReports.The one value whose size is user-unbounded is
@BeforeMethod(Object[] parameters), which retainsArrays.deepToString(row). The row objects themselves were already pinned by the same map; what isnew is their rendering. And the fallback allocated the identical
ParameterSnapshottransientlyanyway — the change converts allocation into retention rather than introducing it.
The default path also gets cheaper: it no longer allocates a
ResultKeyand runs aConcurrentHashMap.removeper passing configuration, and it renders the value once instead oftwice.
Shapes considered and rejected
away from the state it depends on; any future caller of
discardre-acquires the bug.discardentirely. It is not dead. Surefire and TestNG's own test kit setuseDefaultListeners=false; such a run has noXMLReporter, nothing reads late, and retainingevery passing
@BeforeMethodsnapshot would be pure waste.discardhas exactly one caller, sothe flag is the configuration-only narrowing. "Only if mutable" is undecidable, and
ParameterValuealready shares oneStringinstance for both forms unless the declared type isString.Comments this corrects
Both claimed more than the code does, and the first is what made this wrong in the first place.
ParameterSnapshotRecorder.onConfigurationSuccessasserted a property of what is listed that wasnever the recorder's to know. It now states what it does know and defers the decision to the store.
TestRunner.addInternalConfigurationListenerrepeats the claim that being registered first meansbeing told last. That holds only while nothing reorders the listeners:
ListenerComparatorsortsthe list before
reversedOrderreverses it, preferential listeners are merged in after the regularones, and
TestListenerHelperappends the invoker's own listener — the one that files the resultinto
m_passedConfigurations— after the reversal. So at the momentdiscardfires, the resulthas not yet been recorded as something the report will list. That is the bug in one sentence, and
for the reports that read late the fix no longer depends on the order at all.
Coverage
XmlReporterParametersTest— a passing configuration is reported with the values it was announcedwith; and the value is rendered exactly twice, once per invocation handed it (the configuration's
row and the test's own parameter), where capture-then-discard-then-fallback made it three.
PassedConfigurationSnapshotTest— a reader that reads once the invocations are over still findsthe passing configuration's snapshot; a run whose only readers are live ones still drops it; and a
store that reached
detachFromstill holding one is released all the same.ParameterSnapshotsTest— retention at the store level, and that the two requests do not canceleach other in either order.
VerboseReporterTestandTextReporterTestare unchanged and still pass: the console output,including the existing
PassingConfigurationParameterSampleexpectation, is untouched.CountedConfigurationParameterSamplecarries its own counter rather than reusingRenderingCountSample's: adding a configuration method there would move the exact counts fiveassertions across three test classes rely on.
What is left
Unchanged in this PR, as Phase 7 continues:
TestHTMLReporter, thejqpanels andEmailableReporter2still render for themselves.jq'sModelalso lists passed configurations,so when
Mainis migrated it must declare the reading — in a default runXMLReporterhas alreadyasked, which would hide the miss.
FailedReporteris untouched../gradlew buildpasses: 0 failures, 0 errors,testng-test-osgiincluded.Summary by CodeRabbit
Bug Fixes
Documentation
Changelog