Repository navigation
[#1187] Install the Guice module loader before the suite in openam-scripting and openam-audit-context tests - #1188
Conversation
…module loader before the suite InjectorHolder builds the first injector of the JVM from the module loader in place at that moment and never reads the loader again. GroovyValidatorTest, JavaScriptValidatorTest and GroovyAstTransformEscapeTest set the empty loader from a @BeforeMethod, so any class that reached the holder earlier left it built from the default loader, which cannot build an injector on this classpath, and all three classes then failed in setupGuiceModules. Set the loader from @BeforeSuite(alwaysRun = true) in each of the three classes, so a single-class run is covered too, and drop the overrides that only set the loader before calling super.
…dule loader before the suite AuditContextFilter reads its TransactionIdConfiguration from InjectorHolder. AuditContextFilterTest is not a GuiceTestCase, so the binding has to be in the holder's own injector, the first injector of the JVM, which the holder builds from the module loader in place at that moment. The test set its loader from a @BeforeClass, which comes too late whenever another class reaches the holder first. Set the loader, still with TestGuiceModule, from @BeforeSuite(alwaysRun = true).
vharseko
left a comment
There was a problem hiding this comment.
The hooks do fix the class-order failure in the two modules they touch. openam-scripting and openam-audit-context inherit forkCount=1 and reuseForks=true, so each module runs as one TestNG suite, and the @BeforeSuite hooks install the loader before the first test whatever the class order. Both modules pass on every CI leg (see CI below).
I'm requesting changes for points 1–3. As it stands, the PR leaves the hazard #1187 targets in openam-rest, adds the hook as three copies that a single-class run of any other class bypasses, and turns a test-local binding with mutable static state into JVM-wide state.
Needs changing
-
openam-reststill has the old pattern.RestRouterIT.setupGuiceModules()(line 184) installs the empty loader from a@BeforeMethod(dependsOnMethods = "setupMocks")override, and neither #1187 nor this PR mentions it. It passes today only because it is the only*ITin the module, so failsafe runs it alone. If a second IT reachesInjectorHolderfirst,RestRouterITwill either fail insetupGuiceModuleswithNoClassDefFoundErroror silently get the default root injector instead of the empty one. I did not check which. Please harden it the same way, or note in #1187 why it is out of scope. -
Replace the three scripting copies with one hook. An abstract base class (e.g.
EmptyLoaderGuiceTestCase extends GuiceTestCase, likeCTSBasedGuiceTestCaseinopenam-core) with a single@BeforeSuitedoes the same job. TestNG 6.14.3 keys@BeforeSuitemethods byjava.lang.reflect.Method, so an inherited hook runs once per suite, and it still runs in a-Dtest=<Class>or IDE run of any subclass. A new validator test then gets the hook automatically, whereas today one written without it fails when run alone. This is the duplication from my note 2 on #1186, which was moot there only because a single copy was left.A stronger variant is a TestNG
IExecutionListenerregistered insrc/test/resources/META-INF/services/org.testng.ITestNGListener. TestNG runs execution listeners before it creates the test instances, so this would close both limits the javadoc lists (constructors and field initializers, and which classes are in the suite). Check that the surefire 3.1.2 TestNG provider picks up the service file before going this way. Either variant is fine. This is not a request to pinreuseForks; #1186 settled that.If you keep the copies, the javadoc has to name the runs they miss.
mvn -pl openam-scripting test -Dtest=SomeOtherTestruns none of the three hooks, so a class that touches the holder builds it from the default loader and fails. The #1186 javadoc names-Dtestand IDE runs explicitly, and this one only names the fork settings. -
Reset
trustHeadernow thatTestGuiceModuleis JVM-wide. As the new javadoc says, the module now feeds the root injector for every class in the fork. Each test setsTransactionIdConfigurationImpl.trustHeader(line 141, set at lines 90 and 111) and nothing resets it. Another class that asks the holder forTransactionIdConfigurationtherefore sees whatever the lastAuditContextFilterTestmethod left, e.g.falseaftershouldIgnoreTransactionIdHttpHeaderIfConfiguredToDoSo. That is the kind of order dependence this PR sets out to remove. Resetting the field in an@AfterMethod, or binding a per-test instance, closes it.
Non-blocking
-
GroovyAstTransformEscapeTeststill overridessetupGuiceModules()(line 66) only to clear the canary, while its two siblings drop their overrides. Putting@BeforeMethodonclearCanary()instead would stop overriding the library's lifecycle method and keep the three classes consistent. -
Small: the four loaders are anonymous inner classes created in instance methods. Each one captures its test instance, mocks included, and keeps it reachable from the static
InjectorConfigurationfor the rest of the fork. A lambda (a -> Collections.emptySet(),a -> Collections.singleton(TestGuiceModule.class)) is shorter and captures nothing. -
The root cause sits in commons, so this is for a separate issue rather than this PR.
InjectorHolderbuilds its injector eagerly.GuiceTestCase.setupGuiceModules()callsInjectorHolder.getInjector()only to saveoldInjector, and that call forces a build from whatever loader is current. A failed build then poisons the class for the rest of the JVM. If the holder built lazily, no module would need its own hook, andopenam-core-rest(#1186), the two modules here andRestRouterITwould all be covered in one place.
CI
Run 39424307815 at 8d8e4d2: all five build-maven (ubuntu-latest, *) legs fail. All four macOS and Windows legs pass, and so do the CodeQL analyses.
This PR does not cause the Ubuntu failures. All five legs stop in openam-core-rest, which the diff does not touch, with the same five failures as master at this PR's base (run 39287288644 on 37cf5ab):
IdentityResourceNotificationTest.setupMocksfails withNoClassDefFoundError: Could not initialize class org.forgerock.guice.core.InjectorHolder. The cause isNo implementation for NotificationBroker was bound, raised fromSessionResourceV2Test.setUp.OathDevicesResourceTest,PushDevicesResourceTest,TrustedDevicesResourceTestandSmsResourceProviderTestthen fail withPreviously setup Realm class - forgot to call teardown.
That is the ubuntu-26.04 class-order failure that #1186 fixes. Both modules this PR changes build before the reactor stops, and both pass on those legs: openam-audit-context (AuditContextFilterTest 2/2) and openam-scripting (196 tests, 0 failures). A green Ubuntu run needs #1186 merged and this branch rebased.
…oader-before-suite
…module loader from a TestNG execution listener The three @BeforeSuite copies covered only runs that included one of the three classes, and came after TestNG had already created every test instance. A TestNG IExecutionListener registered through META-INF/services/org.testng.ITestNGListener runs before any test instance is created, in every TestNG run of the module, so one listener replaces the three hooks and covers constructors, field initializers and runs of any other class too. GroovyAstTransformEscapeTest no longer overrides setupGuiceModules() just to clear its canary: clearCanary() now runs before and after each test.
… after each test TestGuiceModule now feeds the holder's injector for the whole JVM, so any other class that asks the holder for TransactionIdConfiguration would see whatever trustHeader value the last AuditContextFilterTest method left. Put it back to its default after each test. Install the loader with a lambda instead of an anonymous class.
|
Pushed ebf306f and 72fdecf on top of a merge of master, which brings in #1186.
Tests at 72fdecf, offline (scratch classes and suite XMLs not committed):
|
vharseko
left a comment
There was a problem hiding this comment.
Re-reviewed 72fdecf against the six points from the last round. All six are addressed, so I'm approving.
1. RestRouterIT. The explanation in #1187 holds. RestRouterIT asks the holder for HttpApplication and the internal router only after super.setupGuiceModules() has registered the test's own injector (lines 235 and 364). The root injector would matter only if building it threw, and your measurement shows that the default loader builds it on the openam-rest test classpath. Leaving it out of scope is fine.
2. One listener. EmptyGuiceModuleLoaderListener replaces the three copies. Neither module publishes a test-jar, so the service file stays on this module's test classpath. GuiceTestCase.setupGuiceModules() carries @BeforeMethod itself (I checked the guice test 3.1.2 jar), so dropping the overrides keeps the per-test injector.
I reproduced your check at 72fdecf. A scratch class whose field initializer calls InjectorHolder.getInstance(ArrayList.class) ran first, ahead of the three Guice classes, with -Dsurefire.runOrder=alphabetical:
- With the service file: 20 passed, 0 failed.
- Without it, the run cannot start:
Cannot instantiate class org.forgerock.openam.scripting.AaaHolderProbeTest.
3. trustHeader reset. @AfterMethod(alwaysRun = true) puts it back to true, so the flag is reset after a failed test too.
4. clearCanary(). It now has both @BeforeMethod and @AfterMethod, and the order is unchanged: TestNG runs the inherited setupGuiceModules() first, then clears the canary, as the removed override did.
5. Lambdas. Done in the listener and in AuditContextFilterTest.
6. Thanks for filing OpenIdentityPlatform/commons#376.
Locally (macOS, JDK 27), mvn -o -pl openam-scripting,openam-audit/openam-audit-context test at 72fdecf gives 196 passed and 2 passed, 0 failed.
CI at 72fdecf is green on every leg, including all five ubuntu-latest ones, now that #1186 is merged in. The branch is one commit behind master (#1189, which changes only openam-ui/openam-ui-ria/package-lock.json) and merges cleanly.
InjectorHolderbuilds the first injector of the JVM from the module loaderInjectorConfigurationholds at that moment, and never reads the loader again. The Guice test classes inopenam-scriptingandopenam-audit-contextinstalled their loader from@BeforeMethodor@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 foropenam-core-rest.Changes (test code only):
openam-scripting: a TestNG execution listener,EmptyGuiceModuleLoaderListener, installs the empty loader. It is registered insrc/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.setupGuiceModules()overrides inGroovyValidatorTest,JavaScriptValidatorTestandGroovyAstTransformEscapeTest, which installed the loader before each test.GuiceTestCase.setupGuiceModules()carries its own@BeforeMethodand still registers the per-test injector from@GuiceModules.GroovyAstTransformEscapeTestno longer overridessetupGuiceModules()at all:clearCanary()now runs before and after each test.openam-audit-context:AuditContextFilterTestinstalls its loader from@BeforeSuite(alwaysRun = true)instead of@BeforeClass, and resetsTransactionIdConfigurationImpl.trustHeaderto its default after each test.AuditContextFilterTestis not aGuiceTestCase, so no per-test injector is registered, andAuditContextFilterreadsTransactionIdConfigurationfrom the holder's own injector. The loader keeps returningTestGuiceModule, which binds it. With an empty loader both tests fail withNo implementation for TransactionIdConfiguration was bound.Limits of the
openam-audit-contexthook, stated in its javadoc:@BeforeSuitemethod, so a constructor or field initializer that reachesInjectorHolderstill comes first. No class in the module does that today.@BeforeSuitewould make the result depend on class order again.Out of scope:
RestRouterITinopenam-reststill 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):
InjectorHolderwithout a hook, run first:openam-scriptinghad 3 failures and 63 skipped (NoClassDefFoundError: Could not initialize class org.forgerock.guice.core.InjectorHolderinsetupGuiceModulesof all three classes), andopenam-audit-contexthad 2 failures.openam-scriptingat 72fdecf: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);-Dtest: 1 of 1;-Dtest=<Class>: 5, 5 and 9 pass;mvn -o -pl openam-scripting testin the default order, with-Dsurefire.runOrder=reversealphabeticaland with-DreuseForks=false: 196 tests, 0 failures each.openam-audit-contextat 72fdecf:AuditContextFilterTestwithshouldIgnoreTransactionIdHttpHeaderIfConfiguredToDoSolast, then a scratch class that asserts the holder'sTransactionIdConfigurationtrusts the header: 4 of 4. Without the reset the last one fails withExpecting 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