GITHUB-426: run firstTimeOnly/lastTimeOnly configs as a barrier around the invocation pool#3279
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (9)
✅ Files skipped from review due to trivial changes (2)
🚧 Files skipped from review as they are similar to previous changes (7)
📝 WalkthroughWalkthroughTestNG now enforces ChangesFirst/Last time-only barrier around parallel invocation pools
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
testng-core/src/test/java/test/invocationcount/FirstAndLastTimeTest.java (1)
150-162:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winPreserve strict ordering for the non-thread-pooled fixture
Using
containsExactlyInAnyOrderfor both classes removes validation of the sequential ordering contract forSampleTestClassWithNoThreadPoolSizeDefined. Keep order-insensitive matching only for the pooled fixture.Suggested adjustment
`@Test`(dataProvider = "classNames", description = "GITHUB-426") public void verifyFirstTimeOnly(Class<?> clazz) { List<String> invokedMethodNames = run(clazz); String[] expected = new String[] {"beforeMethod", "testMethod", "testMethod"}; - // One of the sample classes uses a thread pool, so its test method invocations - // run in parallel. firstTimeOnly guarantees only that the `@BeforeMethod` runs - // *exactly once* - it does NOT add a barrier that makes it complete before the - // parallel test invocations start. The single worker that runs the - // configuration does not block the others (see ConfigInvoker, which gates on - // m_executedConfigMethods only), so a testMethod can be recorded before - // beforeMethod. We therefore assert on occurrences, not on ordering. - assertThat(invokedMethodNames).containsExactlyInAnyOrder(expected); + if (clazz == SampleTestClassWithThreadPoolSizeDefined.class) { + // Pooled invocations are parallel; ordering is non-deterministic. + assertThat(invokedMethodNames).containsExactlyInAnyOrder(expected); + } else { + // Non-pooled invocations should remain sequential and deterministic. + assertThat(invokedMethodNames).containsExactly(expected); + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@testng-core/src/test/java/test/invocationcount/FirstAndLastTimeTest.java` around lines 150 - 162, The test method verifyFirstTimeOnly currently uses containsExactlyInAnyOrder for all test fixtures, but this removes validation of sequential ordering for the non-thread-pooled fixture SampleTestClassWithNoThreadPoolSizeDefined which should maintain strict ordering. Refactor the test to apply conditional assertions based on whether the class uses a thread pool: use containsExactly for the non-thread-pooled fixture to preserve strict ordering validation, and use containsExactlyInAnyOrder only for the thread-pooled fixtures. You can either split this into two separate test methods with different dataProviders or add conditional logic within verifyFirstTimeOnly to check the clazz parameter and apply the appropriate assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@testng-core/src/test/java/test/invocationcount/FirstAndLastTimeTest.java`:
- Around line 150-162: The test method verifyFirstTimeOnly currently uses
containsExactlyInAnyOrder for all test fixtures, but this removes validation of
sequential ordering for the non-thread-pooled fixture
SampleTestClassWithNoThreadPoolSizeDefined which should maintain strict
ordering. Refactor the test to apply conditional assertions based on whether the
class uses a thread pool: use containsExactly for the non-thread-pooled fixture
to preserve strict ordering validation, and use containsExactlyInAnyOrder only
for the thread-pooled fixtures. You can either split this into two separate test
methods with different dataProviders or add conditional logic within
verifyFirstTimeOnly to check the clazz parameter and apply the appropriate
assertion.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: de07ef7f-2bc0-43ec-8e3c-c2565b7bfd36
📒 Files selected for processing (3)
testng-core/src/test/java/test/invocationcount/FirstAndLastTimeTest.javatestng-core/src/test/java/test/invocationcount/issue426/SampleTestClassWithNoThreadPoolSizeDefined.javatestng-core/src/test/java/test/invocationcount/issue426/SampleTestClassWithThreadPoolSizeDefined.java
f501fc7 to
d7f1d62
Compare
d7f1d62 to
9a738f7
Compare
…d the invocation pool When a @test has invocationCount > 1 and threadPoolSize > 1, TestNG runs each invocation in its own pool thread (TestInvoker#invokePooledTestMethods). The firstTimeOnly @BeforeMethod and lastTimeOnly @AfterMethod were invoked from inside those parallel workers, which broke their documented contract: - firstTimeOnly ran exactly once (deduped via ConfigInvoker#m_executedConfigMethods) but without a barrier, so a test invocation could start - and its body run - before the @BeforeMethod completed. This surfaced as a flaky FirstAndLastTimeTest.verifyFirstTimeOnly ([testMethod, beforeMethod, testMethod]). - lastTimeOnly had no dedup at all and ran once per invocation (e.g. 4x for invocationCount=4) instead of once, and could run while invocations were still in flight. Fix: hoist both out of the pool. invokePooledTestMethods now runs the firstTimeOnly setup once before the workers start and the lastTimeOnly teardown once after every worker completes, mirroring the existing @BeforeGroups/ @AfterGroups barrier in runWorkers. The per-invocation clones are flagged (BaseTestMethod#skipFirstAndLastTimeOnlyConfigs) so the in-pool filtering skips those configs. Tests: - restore the strict order assertion in verifyFirstTimeOnly; - add verifyFirstAndLastTimeOnlyWithThreadPool (lastTimeOnly + thread pool, a previously untested combination); - add verifyFailingFirstTimeOnlySkipsAllParallelInvocations to guard that a failing shared firstTimeOnly config skips every parallel invocation; - fix the two issue426 sample class names, which were swapped relative to their content.
9a738f7 to
c770aea
Compare
Problem
FirstAndLastTimeTest.verifyFirstTimeOnlywas flaky, occasionally failing with:Investigation showed this is not a test-only issue — it surfaces a real contract violation in the engine.
Root cause
When a
@TesthasinvocationCount > 1andthreadPoolSize > 1, each invocation runs in its own pool thread (TestInvoker#invokePooledTestMethods). ThefirstTimeOnly @BeforeMethod/lastTimeOnly @AfterMethodwere invoked inside those parallel workers:firstTimeOnlyran exactly once (deduped viaConfigInvoker#m_executedConfigMethods) but with no barrier — a losing worker proceeded straight to its test, so a test body could run before the@BeforeMethodcompleted. The Javadoc says it runs "before the first test invocation", so this is a contract violation, not just a listener-ordering artifact.lastTimeOnlyhad no dedup at all: it ran once per invocation (e.g. 4× forinvocationCount=4) instead of once, and could run while invocations were still in flight.Measured on real execution (method bodies, not listeners),
invocationCount=4, threadPoolSize=4:firstTimeOnly @BeforeMethodlastTimeOnly @AfterMethodAfter the fix, both run exactly once and as a barrier (0/30 violations).
Fix
Hoist both configs out of the pool, mirroring the existing
@BeforeGroups/@AfterGroupsbarrier already present inrunWorkers:invokePooledTestMethodsruns thefirstTimeOnlysetup once before the workers start, and thelastTimeOnlyteardown once after every worker completes.BaseTestMethod#skipFirstAndLastTimeOnlyConfigs) so the in-pool config filtering (TestNgMethodUtils) skips those two configs.A failing
firstTimeOnlyin the pooled path still skips all invocations (verified).Also
issue426sample class names, which were swapped relative to their content (...WithNoThreadPoolSizeDefinedactually hadthreadPoolSize=5, and vice versa).lastTimeOnly+ thread pool case, and a regression test asserting that a failing sharedfirstTimeOnlyconfig skips every parallel invocation (none executed).CHANGES.txtentry.Validation
:testng-core:testsuite: 12480 passed, 0 failed.FirstAndLastTimeTestdeterministic across repeated--rerun-tasksruns.autostyleCheckpasses.Summary by CodeRabbit
@BeforeMethod(firstTimeOnly=true)and@AfterMethod(lastTimeOnly=true)to execute correctly as a single barrier around parallel test invocations when usingthreadPoolSize, ensuring proper synchronization and preventing concurrency issues.