Repository navigation
Make openam-core-rest tests independent of class order for InjectorHolder - #1186
Conversation
…e the suite InjectorHolder builds its injector once per JVM, from the module loader in place at that moment. The modules on the openam-core-rest test classpath cannot be built (no NotificationBroker binding), so the first test that touches InjectorHolder with the default loader leaves the class unusable for the rest of the fork. SessionResourceV2Test.setUp constructs a real AMIdentity, whose constructor asks InjectorHolder for a RepoAuditorFactory and swallows the failure. IdentityResourceNotificationTest and SessionResourceTest installed an empty loader in @BeforeClass, which only helped when they ran before SessionResourceV2Test. Surefire runs the classes in file-system order, and on the ubuntu-26.04 runner image that order puts SessionResourceV2Test first: IdentityResourceNotificationTest.setupMocks fails with NoClassDefFoundError for InjectorHolder, and four more classes fail because its Realm mock was never torn down. Install the empty loader from @BeforeSuite(alwaysRun = true) in the three classes, so it is in place before the first test of the suite, whatever order the classes run in.
…the reason for @BeforeSuite The javadocs of SessionResourceTest and IdentityResourceNotificationTest gave SessionResourceV2Test as the class that makes a @BeforeClass come too late. SessionResourceV2Test now installs the empty loader from its own @BeforeSuite hook, so that reason no longer holds. State the general one instead: any class that touches InjectorHolder without such a hook and runs first, with SessionResourceV2Test as the past example.
vharseko
left a comment
There was a problem hiding this comment.
The fix is right. openam-core-rest inherits forkCount=1 and reuseForks=true, so surefire runs all of its classes in one TestNG suite, and the three @BeforeSuite hooks run before the first test whatever the class order. I also ran the module at this head with -DreuseForks=false, which gives each class its own JVM and suite. All 53 classes and 446 tests pass, so no class without the hook relies on another class's hook today.
A few non-blocking notes, for this PR or a follow-up:
-
The session tests could stop touching
InjectorHolderaltogether.SessionResourceTest.setUp(line 156) andSessionResourceV2Test.setUp(line 134) build a realAMIdentityonly to return it from the stubbedgetIdentity(). The resource code reads justgetName()andgetRealm()from it (SessionResourceUtil.java:135-136). Amock(AMIdentity.class)with those two stubs never runs the constructor that asksInjectorHolderfor aRepoAuditorFactory. The hooks in both session classes would then not be needed. -
The empty loader is now copied into a third class. The same anonymous
GuiceModuleLoaderblock is inIdentityResourceNotificationTest,SessionResourceTestandSessionResourceV2Test. One shared helper would leave a single place to change, for example next toRealmTestHelperin theopenam-coretest-jar that this module already uses. -
The javadoc on
IdentityResourceNotificationTest.setupGuiceModuleLoaderclaims more than the build guarantees. It says "What every test class in the same fork gets is therefore an empty injector". That holds only while the module keeps the inheritedforkCount=1andreuseForks=true, and while one of the three hooked classes is in the run. Nothing pins either:- With
reuseForks=falseorforkCount>1, surefire's TestNG provider runs each class in a suite of its own. - A
-Dtest=<other class>run or an IDE single-class run has no hook at all.
It is worth saying so in the comment, or pinning the settings in
openam-core-rest/pom.xml. - With
-
The same hazard remains in other modules. These tests install their loader before the class or the method, not before the suite:
IdRepoTestBase(openam-datastore) andAuditContextFilterTest(openam-audit-context) use@BeforeClass.GroovyValidatorTest,JavaScriptValidatorTestandGroovyAstTransformEscapeTest(openam-scripting) use@BeforeMethod.
They are green on
ubuntu-26.04in this PR's run, but only because of the order their classes come in, and that order is what changed underopenam-core-rest. A follow-up issue would keep this from surfacing the same way. -
A caveat dropped from the
SessionResourceTestjavadoc still applies. The old text ended with "this is a no-op when another class got there first". With@BeforeSuitethat is still true in one case: TestNG creates an instance of every test class before it runs any@BeforeSuitemethod. So a constructor, field initializer or static initializer of a test class that reachesInjectorHolderstill builds the injector from the default loader first. One example is a fieldmock(IdentityServicesImpl.class), whose static fields callInjectorHolder.getInstance. One sentence on that in the comment would save the next person a search.
… loader SessionResourceTest and SessionResourceV2Test built a real AMIdentity only to return it from the stubbed SessionResourceUtil.getIdentity(), and the session code reads just getName() and getRealm() from it. The AMIdentity constructor asks InjectorHolder for a RepoAuditorFactory, which is why both classes had to install the empty module loader. Return a mock with those two getters stubbed instead, and drop both hooks: neither class touches InjectorHolder any more. IdentityResourceNotificationTest keeps its @BeforeSuite hook. Its javadoc now also says that TestNG creates an instance of every test class before it runs any @BeforeSuite method, so a constructor or initializer that reaches InjectorHolder still comes first, and that the hook covers the other classes only while the module runs as one TestNG suite in one JVM, as it does with the inherited forkCount=1 and reuseForks=true.
|
Pushed 0c41e0a.
At 0c41e0a, locally: |
vharseko
left a comment
There was a problem hiding this comment.
Re-reviewed 0c41e0a against the five notes from the last round. All five are addressed, and my approval stands.
1. Mocked AMIdentity. The session code reads only getName() and getRealm() from the identity (SessionResourceUtil.java:135-136, ValidateActionHandler.java:120-121, AbstractSessionPropertiesActionHandler.java:65), so the two stubs cover it. getName() returns demo, as the real object did. The getRealm() value never reaches an assertion, because both classes override convertDNToRealm. It still has to be non-null in SessionResourceTest, where the validate tests stub convertDNToRealm(anyString()), and anyString() does not match null.
2. With the loader left only in IdentityResourceNotificationTest, there is nothing to share.
3 and 5. Javadoc. It now states the one-suite, one-JVM condition, names the runs that fall outside it, and covers the instantiation order. Agreed on not pinning reuseForks in the pom, for the reason you give.
4. #1187 records the latent cases in openam-scripting and openam-audit-context, with the measurements behind them.
I reran the module at 0c41e0a locally (macOS, JDK 27) and read the class order from TEST-TestSuite.xml:
reversealphabeticalas is: 446 passed, 0 failed.- With the
IdentityResourceNotificationTesthook put back on@BeforeClass,reversealphabeticalruns that class last, after all 51 others, includingSessionResourceV2TestandSessionResourceTest. It passes, 446/0. So no class of the module reachesInjectorHolderwith the default loader today, and the@BeforeSuitehook guards against the next one. - The same run with
SessionResourceV2Testbuilding a realAMIdentityagain fails inIdentityResourceNotificationTest.setupMockswithNoClassDefFoundError: Could not initialize class org.forgerock.guice.core.InjectorHolder, throughIdentityServicesImpl.<clinit>. That is the CI failure.
CI at 0c41e0a is green on every leg, including all five ubuntu-latest ones.
Two optional wording nits. Neither blocks the merge.
- The new comment in both
setUpmethods says theAMIdentityconstructor "leaves InjectorHolder unusable for every later test class in the JVM". That happens only when the constructor is the first call to reach the holder and no loader is installed yet. In a full-module run the@BeforeSuitehook comes first, and a realAMIdentitywould just get the empty injector. The comment could keep the condition, as the removed javadoc did ("If this class is the first in the fork to touch the injector holder"). - The javadoc says "the static fields of that class ask InjectorHolder for instances".
IdentityServicesImplhas one such static field,RESOURCE_MAPPING_HANDLER(line 108). Its otherInjectorHoldercall,idServicesErrorHandler, is an instance field, and creating a mock does not initialize it. The plural comes from my note in the last round.
The setUp comments in SessionResourceTest and SessionResourceV2Test said the AMIdentity constructor leaves InjectorHolder unusable for every later test class. That happens only when it is the first call to reach the holder and no loader is installed yet. The IdentityResourceNotificationTest javadoc spoke of the static fields of IdentityServicesImpl. RESOURCE_MAPPING_HANDLER is the only one; idServicesErrorHandler is an instance field.
|
Both wording nits are fixed in 31b6c89. The change touches comments only.
|
vharseko
left a comment
There was a problem hiding this comment.
Checked 31b6c89 against the two wording nits from the last round. Both are fixed, and the commit changes comment lines only.
- The
setUpcomment inSessionResourceTestandSessionResourceV2Teststates the condition again: theAMIdentityconstructor leavesInjectorHolderunusable only when it is the first call to reach the holder in the JVM, before any loader is installed. - The
IdentityResourceNotificationTestjavadoc now matches the code.RESOURCE_MAPPING_HANDLER(IdentityServicesImpl.java:108) is the only static field that asksInjectorHolderfor an instance.idServicesErrorHandler(line 105) is an instance field, and creating a mock does not initialize it.
CI at 31b6c89 is green on every leg, including all five ubuntu-latest ones. The branch is one commit behind master (#1176, which does not touch openam-core-rest) and merges cleanly. My approval stands.
…ripting and openam-audit-context tests (#1188) `InjectorHolder` builds the first injector of the JVM from the module loader `InjectorConfiguration` holds at that moment, and never reads the loader again. The Guice test classes in `openam-scripting` and `openam-audit-context` installed their loader from `@BeforeMethod` or `@BeforeClass`, so a class that reaches the holder before them decides the injector for the whole fork. As #1187 describes, the hazard is latent: no order of today's test classes fails, but a new unhooked class that runs first breaks both modules. Same cause as #1186 for `openam-core-rest`. Changes (test code only): - `openam-scripting`: a TestNG execution listener, `EmptyGuiceModuleLoaderListener`, installs the empty loader. It is registered in `src/test/resources/META-INF/services/org.testng.ITestNGListener`, so TestNG runs it before it creates any test instance, in every TestNG run of the module, whichever classes the run includes and however surefire forks it. - It replaces the `setupGuiceModules()` overrides in `GroovyValidatorTest`, `JavaScriptValidatorTest` and `GroovyAstTransformEscapeTest`, which installed the loader before each test. `GuiceTestCase.setupGuiceModules()` carries its own `@BeforeMethod` and still registers the per-test injector from `@GuiceModules`. - `GroovyAstTransformEscapeTest` no longer overrides `setupGuiceModules()` at all: `clearCanary()` now runs before and after each test. - `openam-audit-context`: `AuditContextFilterTest` installs its loader from `@BeforeSuite(alwaysRun = true)` instead of `@BeforeClass`, and resets `TransactionIdConfigurationImpl.trustHeader` to its default after each test. - Unlike the issue title says, this loader is not empty. `AuditContextFilterTest` is not a `GuiceTestCase`, so no per-test injector is registered, and `AuditContextFilter` reads `TransactionIdConfiguration` from the holder's own injector. The loader keeps returning `TestGuiceModule`, which binds it. With an empty loader both tests fail with `No implementation for TransactionIdConfiguration was bound`. - That binding now serves every class in the JVM, hence the reset. Limits of the `openam-audit-context` hook, stated in its javadoc: - TestNG creates an instance of every test class before it runs any `@BeforeSuite` method, so a constructor or field initializer that reaches `InjectorHolder` still comes first. No class in the module does that today. - The loader applies to the whole JVM. A second class there that installs a different loader from its own `@BeforeSuite` would make the result depend on class order again. Out of scope: `RestRouterIT` in `openam-rest` still installs an empty loader from a `@BeforeMethod`, but needs none, because the default loader builds the root injector on that module's test classpath (details in #1187). Tests, offline (scratch classes and TestNG suite XMLs not committed): - Before the change, a scratch class that touches `InjectorHolder` without a hook, run first: `openam-scripting` had 3 failures and 63 skipped (`NoClassDefFoundError: Could not initialize class org.forgerock.guice.core.InjectorHolder` in `setupGuiceModules` of all three classes), and `openam-audit-context` had 2 failures. - `openam-scripting` at 72fdecf: - a scratch class whose field initializer calls `InjectorHolder`, run first ahead of the three Guice classes: 20 of 20 pass. Without the service file the same run cannot start (`Cannot instantiate class ...AaaHolderCtorProbeTest`); - that scratch class alone with `-Dtest`: 1 of 1; - each Guice class alone with `-Dtest=<Class>`: 5, 5 and 9 pass; - `mvn -o -pl openam-scripting test` in the default order, with `-Dsurefire.runOrder=reversealphabetical` and with `-DreuseForks=false`: 196 tests, 0 failures each. - `openam-audit-context` at 72fdecf: - an unhooked scratch class that touches the holder first, then `AuditContextFilterTest` with `shouldIgnoreTransactionIdHttpHeaderIfConfiguredToDoSo` last, then a scratch class that asserts the holder's `TransactionIdConfiguration` trusts the header: 4 of 4. Without the reset the last one fails with `Expecting value to be true but was false`; - `mvn -o -pl openam-audit/openam-audit-context test`, default and with `-DreuseForks=false`: 2 tests, 0 failures. Fixes #1187
Since
ubuntu-latestmoved to theubuntu-26.04runner image,build-mavenfails inopenam-core-reston every Ubuntu leg (JDK 11, 17, 21, 25, 27). It was first seen in run 39217607024 on #1023, whose changes do not touch this module:openam-core-restthere is identical tomaster(d75bdb8). The same tests were green onubuntu-24.04three days earlier, and are green on macOS and Windows in the failing run.Cause
InjectorHolderbuilds its injector once per JVM, from the module loaderInjectorConfigurationholds at that moment. The modules on this test classpath cannot be built (No implementation for NotificationBroker was bound). The first test that touchesInjectorHolderwith the default loader therefore leaves the class unusable for the rest of the fork.SessionResourceV2Test.setUpconstructs a realAMIdentity. Its constructor asksInjectorHolderfor aRepoAuditorFactoryand swallows the failure (AMIdentity.java:190).IdentityResourceNotificationTestandSessionResourceTestinstalled an empty loader, but in@BeforeClass, which only helped when they ran beforeSessionResourceV2Test. Surefire runs the classes in file-system order, and onubuntu-26.04that order putsSessionResourceV2Testfirst:IdentityResourceNotificationTest.setupMocks:155fails withNoClassDefFoundError: Could not initialize class org.forgerock.guice.core.InjectorHolder, throughIdentityServicesImpl.<clinit>.@BeforeMethodfails afterRealmTestHelper.setupRealmClass(), so the@AfterMethodteardown never runs. As a result,OathDevicesResourceTest,PushDevicesResourceTest,TrustedDevicesResourceTestandSmsResourceProviderTestfail withPreviously setup Realm class - forgot to call teardown.Change
Test code only.
IdentityResourceNotificationTestinstalls the empty loader from@BeforeSuite(alwaysRun = true)instead of@BeforeClass.openam-core-restsets noforkCount,parallelorsuiteXmlFiles, so surefire runs all its classes in one TestNG suite, and every@BeforeSuitemethod runs before the first test, whatever order the classes are in. The tests get the same empty injector they got on the green runs. Only the moment the loader is installed changes.SessionResourceTestandSessionResourceV2Testno longer build a realAMIdentity, whose constructor reachedInjectorHolder. They return a mock withgetName()andgetRealm()stubbed, the only two getters the session code reads, and they no longer install a loader.IdentityResourceNotificationTestjavadoc states the limits of the hook: TestNG instantiates every test class before any@BeforeSuitemethod runs, and the hook covers the other classes only while the module runs as one TestNG suite in one JVM.Tests
mvn -o -pl openam-core-rest test -Dsurefire.runOrder=<order>, run locally on macOS:reversealphabeticalatmasterputssession.SessionResourceV2Testahead ofIdentityResourceNotificationTest, as onubuntu-26.04. Result:IdentityResourceNotificationTest.setupMocksfails with the CI error, caused atSessionResourceV2Test.setUp:112. The default order is green atmaster.@BeforeSuitehook in all three classes),reversealphabetical,alphabetical,filesystemandrandomwith seeds 1, 42 and 20261019 all pass: 446 passed, 0 failed, 0 skipped each, includingIdentityResourceNotificationTest8/8,SessionResourceTest33/33 andSessionResourceV2Test26/26.-Dtest=<class>) passes.reversealphabetical, the default order and-DreuseForks=false(53 classes, each in its own JVM) all pass, 446/0/0. With theIdentityResourceNotificationTesthook put back on@BeforeClass,reversealphabeticalstill passes (446/0), so neither session class reachesInjectorHolder. WithSessionResourceV2Testbuilding a realAMIdentityagain, the same run fails as onubuntu-26.04.Locally, the
ubuntu-26.04order is only approximated byreversealphabetical. This PR's own CI run checks the real image.