Conversation
maximthomas
left a comment
There was a problem hiding this comment.
praise: Each fix is at the line the CodeQL alert flags, and none goes further than the alert needs.
BatchRemoteCache.resultLockis now a privatenew Object()instead of the interned"resultLock"literal.FrameworkUtil.getFrameworkVersion()isstatic synchronized, which pairs it with the already synchronizedsetFrameworkVersion. The(ClassLoader)overload is renamed toreadFrameworkVersion, and its only callers are the two inFrameworkUtilTests.
question (non-blocking): Should connector-framework-contract keep working for connectors that pin TestNG 7.5 or later? ContractITCase.createInstances now instantiates the TestNG-internal org.testng.internal.ObjectFactoryImpl on every call.
OpenICF-java-framework/connector-framework-contract/src/main/java/org/identityconnectors/contract/test/ContractITCase.java:72, :39
At the base, ObjectFactoryImpl was referenced only from the nested ContractTestFactory, which nothing ever instantiated, so the class was never loaded. Line 72 now runs before the loop on every @Factory call. TestNG 7.5 moved the class to org.testng.internal.objects, and 7.10.2 removed org.testng.IObjectFactory as well. I compiled copies of the base and head versions of the method and ran them on TestNG 6.9.10, 7.5, 7.6.0 and 7.10.2. The base version took its fallback on all four. The head version threw NoClassDefFoundError on the three 7.x versions. The factory is still never used, because no class in DEFAULT_TEST_CLASSES has a public (String) constructor. No build in this repo runs ContractITCase, so the break only shows up in downstream connectors that override TestNG. If those connectors are meant to be supported, this should block the merge; if not, it can wait. Plain reflection does the same job without the internal class:
for (Class<?> testClass: getContractTestClasses(context)) {
try {
Object test = testClass.getConstructor(String.class).newInstance("");
injector.injectMembers(test);
result.add(test);
} catch (NoSuchMethodException e) {
result.add(injector.getInstance(testClass));
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Cannot instantiate " + testClass.getName(), e);
}
}Then the IObjectFactory and ObjectFactoryImpl imports can go.
issue (non-blocking): getGroovyScriptEngine() stores the engine in the field before initializeCustomizer() runs. If the customizer fails, the engine stays in the field without its customizations.
OpenICF-groovy-connector/src/main/java/org/forgerock/openicf/misc/scriptedcommon/ScriptedConfiguration.java:797-800, :820-822
The field has to be set first, because getCustomizerClass() (:727) calls the getter again. But when the customizer script throws, initializeCustomizer() only wraps the exception and rethrows it; it never clears the field. From then on every caller takes the fast path at :786 and runs scripts on an engine whose customizer never ran. For example, ScriptedRESTConfiguration.getHttpClient then fails on a null initClosure and the original error is lost. A thread that reaches :786 while the first thread is still inside the customizer also gets the engine without its customizations. This bug existed before the PR, and volatile does not make it worse. It does mean that volatile alone does not make this lazy initialization correct. Clearing the field on failure fixes the failure case. The case of a concurrent thread needs a separate "customized" flag.
groovyScriptEngine =
new GroovyScriptEngine(getRoots(compilerConfiguration, loader), loader);
try {
initializeCustomizer();
} catch (RuntimeException e) {
groovyScriptEngine = null;
throw e;
}suggestion (non-blocking): Read groovyScriptEngine into a local variable once in getGroovyScriptEngine().
OpenICF-groovy-connector/src/main/java/org/forgerock/openicf/misc/scriptedcommon/ScriptedConfiguration.java:786, :804, :668
The fast path checks the volatile field at :786 and reads it again at :804. Meanwhile release() sets it to null at :668 under a lock the fast path does not take. On connector-server shutdown, ConnectorServerImpl.stop eventually calls ScriptedConfiguration.release, and ConnectionListener.shutdown does not wait for in-flight requests to finish. A script evaluation still running at that moment can get null back and throw an NPE in evaluate at :744. The window is small, and the race existed before the PR. Reading the field once into a local is the usual way to write this pattern with volatile:
GroovyScriptEngine engine = groovyScriptEngine;
if (null == engine) {
synchronized (this) {
engine = groovyScriptEngine;
if (null == engine) {
final CompilerConfiguration compilerConfiguration =
new CompilerConfiguration(config);
compilerConfiguration.addCompilationCustomizers(getImportCustomizer(null));
final GroovyClassLoader loader =
new GroovyClassLoader(getParentLoader(), compilerConfiguration, true);
engine = new GroovyScriptEngine(getRoots(compilerConfiguration, loader), loader);
groovyScriptEngine = engine;
initializeCustomizer();
}
}
}
return engine;09a1ed7 to
9aa4f8d
Compare
|
I rebased the branch onto current master with no conflicts and addressed all three points in 9aa4f8d. question: issue: the engine stays uncustomized after a customizer failure. Opened as #148, together with the concurrent-caller case you described. I fixed both here instead of only clearing the field on failure. suggestion: read the field once. Opened as #149 and fixed in the same change. The field is read once into a local on the fast path and once under the lock, so I also updated the PR description: |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The round-1 points are fixed where they started.
ScriptedConfiguration.getGroovyScriptEngine()writes the volatile field only afterinitializeCustomizer()returns. The re-entrant call goes throughcustomizingGroovyScriptEngine, which is guarded by the monitor. A failed customizer publishes nothing, and no other thread can see an uncustomized engine.testFailedCustomizerIsRetriedOnTheNextCallfails on the previous code: the second call returned at the fast path andcallsstayed 1. It ran green in CI at 9aa4f8d (groovy-connector: 127 run, 0 failed).ContractITCase.createInstancesno longer referencesorg.testng.internal.
issue (non-blocking): A REST/CREST/SSH customizer that keeps failing leaks one more GroovyClassLoader on every call.
OpenICF-groovy-connector/src/main/java/org/forgerock/openicf/misc/scriptedcommon/ScriptedConfiguration.java:814-823, :832-845; OpenICF-groovy-connector/src/main/groovy/org/forgerock/openicf/connectors/scriptedrest/ScriptedRESTConfiguration.groovy:189, OpenICF-groovy-connector/src/main/groovy/org/forgerock/openicf/connectors/scriptedcrest/ScriptedCRESTConfiguration.groovy:224, OpenICF-ssh-connector/src/main/groovy/org/forgerock/openicf/connectors/ssh/SSHConfiguration.groovy:387
When the customizer throws, nothing is published. Each later getGroovyScriptEngine() call, from evaluate, loadScript or getHttpClient, builds a new loader and engine and runs the customizer again. That retry is intended. The REST, CREST and SSH createCustomizerScript overrides, though, run customizerClass.metaClass.customize << {…} before the script runs. That registers an ExpandoMetaClass on the newly compiled class, and on Groovy 2.4.21 the registration keeps the class's loader alive. In a probe of 50 failed attempts, every loader was still reachable after the heap was exhausted when the EMC was registered, and none was without it. So a customizer that fails at run time, for example a typo that raises MissingPropertyException, leaks one loader per failed operation. Before this change it was one per engine. GroovyClassLoader.clearCache() in 2.4.21 only clears its maps. InvokerHelper.removeClass unregisters the class. The fix below was not run.
private void initializeCustomizer() {
Class customizerClass = null;
try {
customizerClass = getCustomizerClass();
if (null != customizerClass) {
Binding binding = new Binding();
binding.setVariable(LOGGER, getLogger(customizerClass));
createCustomizerScript(customizerClass, binding).run();
}
} catch (Throwable t) {
if (null != customizerClass) {
// The retry compiles a new class; unregister this one so its loader can be collected.
InvokerHelper.removeClass(customizerClass);
}
logger.error(t, "Failed to customize the connector");
throw ConnectorException.wrap(t);
}
}Plus import org.codehaus.groovy.runtime.InvokerHelper;.
suggestion (non-blocking): Document that a customizer must not wait for another thread that uses this configuration's engine.
OpenICF-groovy-connector/src/main/java/org/forgerock/openicf/misc/scriptedcommon/ScriptedConfiguration.java:798-805
Only the thread that holds the monitor sees customizingGroovyScriptEngine. Every other thread blocks on synchronized (this) until the customizer returns, which is what #148 asks for. One consequence: a customizer that runs Thread.start { configuration.loadScript('X.groovy') }.join(), or waits on an evaluate() it submitted, now hangs forever. The previous code published the engine early, so the same script finished. No bundled customizer does this.
/*
* This must be called once from thread-safe location and inside the
* synchronized to avoid deadlock. The customizer runs while this
* configuration's monitor is held: it must not wait for another thread
* that calls getGroovyScriptEngine(), evaluate() or loadScript().
*/
private void initializeCustomizer() {suggestion (non-blocking): testEngineIsHiddenFromOtherThreadsUntilCustomized takes a 500 ms timeout as proof that the second caller blocked.
OpenICF-groovy-connector/src/test/java/org/forgerock/openicf/misc/scriptedcommon/ScriptedConfigurationTest.java:114-120
Suppose the engine were published early again. The second caller would then return as soon as it is scheduled, and that return is the only thing that turns this test red. If the pool thread is not scheduled within 500 ms on a loaded runner, the TimeoutException is caught, both futures later return the same engine, and isSameAs passes. On a normal schedule the test catches the regression, but not on every run. HEAD itself cannot fail spuriously.
final AtomicReference<Thread> secondThread = new AtomicReference<>();
Future<GroovyScriptEngine> second = executor.submit(() -> {
secondThread.set(Thread.currentThread());
return configuration.getGroovyScriptEngine();
});
while (!second.isDone() && (secondThread.get() == null
|| secondThread.get().getState() != Thread.State.BLOCKED)) {
Thread.sleep(10);
}
assertThat(second.isDone()).as("second caller returned while the customizer ran").isFalse();Pin: this replaces lines 114-120, plus import java.util.concurrent.atomic.AtomicReference;. An early publish then fails on every run, not only when the pool thread is scheduled within 500 ms.
…Platform#132, align the 3A copyright lines
9aa4f8d to
9ce4b58
Compare
|
I rebased the branch onto current master with no conflicts and addressed all three points in 9ce4b58. issue: loader leak on a failing customizer. suggestion: document the deadlock constraint. Added your wording to the comment on suggestion: the 500 ms timeout. Replaced it with your loop: the test waits until the second thread is I checked the three tests against a reverted fix (no |
9ce4b58 to
863f079
Compare
|
Right after my previous comment #133, #134, #136, #141 and #143 were merged, and the branch conflicted with master again. I rebased it once more; the head is now 863f079. The only conflict was in On the new base I checked the three tests against reverted parts of the fix: engine published before the customizer, no |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The rebase keeps #133's synchronised getter and adds only the #148 part that master lacked.
ScriptedConfiguration.getGroovyScriptEngine()publishes the engine (:823) only afterinitializeCustomizer()has returned, and re-entrant customizer calls get the engine under construction fromcustomizingGroovyScriptEngine(:802-803).- Every access to
groovyScriptEnginenow holds the configuration's monitor (the getter at:800,release()at:661), so the round-2 double read is gone. initializeCustomizer()unregisters the failed customizer class withInvokerHelper.removeClass(:848), andtestFailedCustomizerClassIsUnregisteredchecks that its strong metaclass slot is empty.
issue (non-blocking): release() never unregisters the customizer class of the published engine.
OpenICF-groovy-connector/src/main/java/org/forgerock/openicf/misc/scriptedcommon/ScriptedConfiguration.java:660-674, :848
removeClass runs only when the customizer fails. A customizer class whose script succeeded keeps the ExpandoMetaClass that ScriptedRESTConfiguration.groovy:189, ScriptedCRESTConfiguration.groovy:224 and SSHConfiguration.groovy:387 register with customizerClass.metaClass.customize <<. Each configuration compiles its customizer with its own GroovyClassLoader (:810-814), so every facade dispose or configuration change of a REST/CREST/SSH connector keeps one more loader, its scripts and the released configuration. On Groovy 2.4.21 such a loader survives heap exhaustion (round-2 probe: 50 attempts, all alive). The code on master is the same, and this PR claims only the per-retry pin, which it fixes, so this belongs in a follow-up.
/** The customizer class of the published engine; release() unregisters it. Guarded by {@code this}. */
private Class publishedCustomizerClass = null;
// getGroovyScriptEngine(); initializeCustomizer() returns customizerClass at the end of its try block
final Class customizerClass;
customizingGroovyScriptEngine = engine;
try {
customizerClass = initializeCustomizer();
} finally {
customizingGroovyScriptEngine = null;
}
publishedCustomizerClass = customizerClass;
groovyScriptEngine = engine;
// release(), inside synchronized (this), after the release closure has run
if (null != publishedCustomizerClass) {
InvokerHelper.removeClass(publishedCustomizerClass);
publishedCustomizerClass = null;
}
groovyScriptEngine = null;Pin: the stand-in from testFailedCustomizerClassIsUnregistered with a customizer that succeeds, then configuration.release(), then ClassInfo.getClassInfo(clazz).getStrongMetaClass() is null. It fails without the release() hunk.
issue (non-blocking): For SSH and CREST, a successful retry that defines no release {} leaves the failed attempt's release closure for release() to run.
OpenICF-ssh-connector/src/main/groovy/org/forgerock/openicf/connectors/ssh/SSHConfiguration.groovy:387-397, OpenICF-groovy-connector/src/main/groovy/org/forgerock/openicf/connectors/scriptedcrest/ScriptedCRESTConfiguration.groovy:201, :224-236
The SSH customize reset clears init, onCreateConnection and onCloseConnection, not the closure that release {} stores through setReleaseClosure (:397). CREST's release = null (:226) writes a private field that nothing reads. Only ScriptedRESTConfiguration.groovy:191 clears the closure release() runs, and initializeCustomizer()'s catch does not clear it. Example: a customizer registers release { r1 } and then throws; the script is edited, or a branch skips release {}; the retry succeeds; facade dispose then runs r1. It needs a customizer that behaves differently between attempts, and the retry this PR adds is what makes it reachable.
// SSHConfiguration.groovy:387
customizerClass.metaClass.customize << { Closure cl ->
init = null
onCreateConnection = null
onCloseConnection = null
setReleaseClosure(null)
// ScriptedCRESTConfiguration.groovy:224 — replaces the unread `release = null`; drop `private Closure release` at :201
customizerClass.metaClass.customize << { Closure cl ->
init = null
setReleaseClosure(null)## Summary Follow-up to the unused-parameter cleanup (#141): investigated all the newly-surfaced small `note`-severity CodeQL categories. - **`GuardedString`** now overrides `toString()` (returns `"GuardedString(...)"`), so it no longer inherits `Object`'s default. Fixes `call-to-object-tostring` at its root instead of patching the two current call sites (`SharedSecretPrincipal`, `ScriptOnResourceApiOpTests`) individually — any future logging of a `GuardedString` is safe by construction. This is more than cosmetic: the inherited output printed `hashCode()`, which is derived from the unsalted SHA-1 hash of the clear text, so a fingerprint of the secret reached log text — through `SharedSecretPrincipal.toString()`, and, with OK-level logging on, through `LoggingProxy`, which prints every API argument and return value: a `GuardedString` attribute value (e.g. `__PASSWORD__`) passed to create/update or returned by getObject was printed via `Attribute.toString()`. - **`GuardedByteArray`** gets the same override (`"GuardedByteArray(...)"`): its `hashCode()` is derived from the clear bytes the same way, and it is a supported attribute type, so the same `LoggingProxy` → `Attribute.toString()` path printed it. - Fixes a shadowed local in `LdapInternalSearch.execute()` (`local-shadows-field`). Also dismissed on GitHub: 3 `ignored-error-status-of-call` (the ignored return values are already covered by a subsequent check or exception path), 8 `jdk-internal-api-access` (AD DirSync's `com.sun.jndi.ldap.Ber*`, no public JDK alternative exists), 1 `confusing-method-signature` (`Log.log(..)` is long-standing, heavily-used public API — renaming to remove the overload is too invasive for the benefit). Covered by other PRs instead of this one: - #134 (opened independently, earlier) fixes byte-for-byte the same lines in `AttributeTypeUtil.java`, `MultiOpTests.java`, `PrettyStringBuilder.java`, `StringUtil.java`, `ActiveDirectoryChangeLogSyncStrategy.java`, `XSDAnnotationParser.java` (with `AttributeTypeUtilTests.java`): `uncaught-number-format-exception`, `inefficient-boxed-constructor`, `inefficient-empty-string-test`, `inefficient-key-set-iterator`, `unknown-javadoc-parameter`, `missing-space-in-concatenation`. Dropped here on 2026-09-21. - #132 removes the dead `ContractTestFactory` inner class from `ContractITCase` (`unused-reference-type`) along with more of that file's dead code; the two PRs conflicted there, so this PR no longer touches `ContractITCase`. Dropped here on 2026-10-04. **Update 2026-10-02 (review round 1):** rebased onto the current `master`; added the `GuardedByteArray` override; replaced the `toString()` test, which passed even without the override, by one that pins the fix; reworded the Javadoc to say why the override matters. **Update 2026-10-04 (review round 2):** rebased onto the current `master` (picks up #146, which fixes the `docs.groovy-lang.org` javadoc failure that turned the `windows-latest, 11` cell red); both `toString()` tests now also pin the exact output; corrected the description of the `GuardedByteArray` leak (it was reachable, not latent); dropped the `ContractITCase` change in favour of #132; aligned the added 3A copyright lines to the repository's `Portions Copyrighted 2026 3A Systems, LLC`. ## Test plan - [x] `GuardedStringTests.testToStringDoesNotDependOnTheSecret` and `GuardedByteArrayTests.testToStringDoesNotDependOnTheSecret`: two different secrets print the same `toString()`, and it is exactly `"GuardedString(...)"` / `"GuardedByteArray(...)"`. Verified red with the overrides removed (round 1) and with `toString()` returning `null` / `""` (round 2). - [x] `connector-framework` test suite after round 2 — 194 tests, 0 failures. - [x] `connector-framework`, `connector-framework-contract`, `OpenICF-ldap-connector` build after round 2. - [x] `mvn install` on `connector-framework`, `connector-framework-contract`, `OpenICF-ldap-connector` (incl. existing test suites, LDAP/OpenDJ integration tests included) — all green, 946 tests, 0 failures (before the review rounds).
…nterned-string locks, racy lazy init SQLUtil tested "instanceof Integer" twice, the second branch unreachable. ContractITCase dereferenced a factory that was always null; it now uses TestNG's ObjectFactoryImpl, and the unused nested ContractTestFactory goes. BatchRemoteCache synchronised on an interned string literal, a monitor shared with any other code using the same literal; it locks a private object now. The double-checked lazy initialisation of WebSocketConnectionGroup.operationContext, ScriptedConfiguration.groovyScriptEngine and TstStatefulConnectorConfig.executorService reads a non-volatile field outside the lock; the fields are volatile now. FrameworkUtil's version getter is synchronised like its setter, and the class-loader overload, which reads a resource rather than the field, is readFrameworkVersion.
…tNG internals from ContractITCase ScriptedConfiguration.getGroovyScriptEngine() stored the engine before initializeCustomizer() ran: a failed customizer left an uncustomized engine in place for good, and a concurrent caller could get it mid-customization (OpenIdentityPlatform#148). The fast path also read the volatile field twice while release() may null it in between (OpenIdentityPlatform#149). The field is now read once, and the engine is published only after the customizer succeeds; the customizer's own re-entrant calls get it from a lock-guarded field. ContractITCase.createInstances instantiated org.testng.internal.ObjectFactoryImpl on every call; TestNG 7.5 moved that class and 7.10.2 removed IObjectFactory. It now calls the (String) constructor through plain reflection. Fixes OpenIdentityPlatform#148 Fixes OpenIdentityPlatform#149
… in the test Since a failed customizer is now retried on the next call, each failure compiled a new customizer class. The REST, CREST and SSH configurations register an ExpandoMetaClass on it, which keeps its GroovyClassLoader alive, so a customizer that keeps failing leaked one loader per call. initializeCustomizer() now removes the class with InvokerHelper.removeClass when the customizer fails. The comment on initializeCustomizer() states that the customizer runs under the configuration's monitor and must not wait for another thread that uses the engine. testEngineIsHiddenFromOtherThreadsUntilCustomized took a 500 ms timeout as proof that the second caller blocked; it now waits until that thread is BLOCKED or done, so an early publish fails the test on every run.
A failed customizer is retried on the next call (OpenIdentityPlatform#148). When the failed attempt registered release {} and the successful retry did not, release() still ran the failed attempt's closure: SSH's customize never reset it, and CREST's "release = null" wrote a private field that nothing reads. Both now clear it with setReleaseClosure(null), the way REST's customize already resets its closure, and CREST's unread field is gone.
863f079 to
2e6e8d7
Compare
|
I rebased the branch onto current master (after #131, #135, #139, #142 and #145) with no conflicts and addressed round 4. The head is now 2e6e8d7. issue: issue: SSH/CREST keep the failed attempt's release closure. I fixed it here with your change, because the retry from this PR is what makes the case reachable. The SSH I updated the PR description: there is a new row for the |
Closes the remaining 10 open CodeQL alerts of severity
errorthat carry no security rating:java/contradictory-type-checks#1536 #1537,java/dereferenced-value-is-always-null#1560,java/sync-on-boxed-types#1554 #1555 #1556,java/unsafe-double-checked-locking#1550 #1551,java/unsynchronized-getter#1544 #1545. The eleventh, #1549 (ScriptedConfiguration), was closed by #133.Fixes #148
Fixes #149
SQLUtil.setParamelse if (val instanceof Integer)appears twice; the second branch can never runContractITCase.createInstancesIObjectFactory objectFactory = nullis dereferenced for any test class with a(String)constructor. None of the default classes has one today, so the code only worked because theNoSuchMethodExceptionpath was always taken(String)constructor is called through plain reflection, so the method no longer depends on TestNG's internalObjectFactoryImpl, which TestNG 7.5 moved and whoseIObjectFactory7.10.2 removed. The unused nestedContractTestFactorywith its three never-read fields is goneBatchRemoteCache(testbundlev1)synchronized (resultLock)on the interned literal"resultLock", a monitor shared with any other code in the JVM that synchronises on the same stringnew Object()WebSocketConnectionGroup.operationContext,TstStatefulConnectorConfig.executorServicevolatile, which makes the pattern correct under the JMMScriptedConfiguration.getGroovyScriptEngine()synchronizedfor the whole initialisation. That closed the double-checked-locking alert and #149 (the getter andrelease()share the monitor now), and concurrent callers wait for the customizer. But the getter still stored the engine before its customizer ran, so a failed customizer left an uncustomized engine in place for good (#148)initializeCustomizer()succeeds. The customizer's own re-entrant calls get the engine under construction from a second lock-guarded field. If the customizer fails, nothing is published and the next call retries; the failed customizer class is removed withInvokerHelper.removeClass, so the metaclass the REST, CREST and SSH configurations register on it does not keep one class loader alive per retry. The customizer runs under the configuration's monitor, so it must not wait for another thread that uses the engine; the comment oninitializeCustomizer()says socustomizeinSSHConfiguration,ScriptedCRESTConfigurationrelease {}left its closure forrelease()to run after a successful retry that registered none: the SSHcustomizenever reset it, and CREST'srelease = nullwrote a private field nothing readssetReleaseClosure(null), as REST'scustomizealready does; CREST's unread field is removedFrameworkUtil.getFrameworkVersion()setFrameworkVersionis synchronisedstatic synchronized(not a hot path). The(ClassLoader)overload readsconnectors-framework.propertiesrather than the field and is only used byFrameworkUtilTests, so it is renamed toreadFrameworkVersionand the getter/setter pairing no longer applies to itScriptedConfigurationTestcovers #148: a customizer that fails once is retried on the next call, a second thread blocks until the customizer has finished, and a failed customizer class has no metaclass left registered. Each test fails when its part of the fix is reverted (engine published before the customizer, noremoveClass, getter not synchronised).ScriptedConfigurationTest(CREST) andSSHConfigurationCustomizerTest(SSH) cover the release closure: a first attempt registersrelease {}and fails, the retry registers none, andrelease()must not run the stale closure; both fail without thecustomizereset. #149 is fixed on master by #133; it was a window between two adjacent field reads that no test can hold open, so it has no test. The other changes have no behaviour a unit test can observe: dead branches,volatile, a private monitor and a synchronised getter.createInstancestakes its classes from a fixed list with no(String)constructors, so its reflective path cannot be reached from a test.The class of a customizer that succeeded is still never unregistered, so
release()keeps its loader alive; master behaves the same, and that is tracked separately in #154.Local reactor run (rebased on current master, after #131 #133 #134 #135 #136 #139 #141 #142 #143 #145) of all touched modules and everything they depend on: connector-framework 200, dbcommon 89, framework-internal 524 (2 skipped), contract 43, connector-framework-server 33, groovy-connector 130 (42 skipped as on master, 4 new), ssh-connector 97 (82 skipped, 1 new), testbundlev1 compiles — all green.