This is an automated email from the ASF dual-hosted git repository.
sunlan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 0db2e688cf GROOVY-11898: provide test for OSGi (#2430)
0db2e688cf is described below
commit 0db2e688cf566ca1b8682f43f4d6543954c00d62
Author: Paul King <[email protected]>
AuthorDate: Mon Apr 6 15:03:09 2026 +1000
GROOVY-11898: provide test for OSGi (#2430)
---
gradle/verification-metadata.xml | 25 ++-
settings.gradle | 1 +
subprojects/groovy-osgi-test/build.gradle | 42 ++++
.../org/apache/groovy/osgi/OsgiBundleTest.java | 220 +++++++++++++++++++++
4 files changed, 286 insertions(+), 2 deletions(-)
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 48077fa700..872e202996 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -96,6 +96,7 @@
<ignored-key id="991EFB94DB91127D" reason="Key couldn't be downloaded
from any key server"/>
<ignored-key id="9A0B94DEC0FFA7EE" reason="Key couldn't be downloaded
from any key server"/>
<ignored-key id="9AEE152CDCCEBFCB" reason="Key couldn't be downloaded
from any key server"/>
+ <ignored-key id="9B7D32F2D50582E6" reason="Key couldn't be downloaded
from any key server"/>
<ignored-key id="9CC6720FBB1B27BA" reason="Key couldn't be downloaded
from any key server"/>
<ignored-key id="9EB80E92EB2135B1" reason="Key couldn't be downloaded
from any key server"/>
<ignored-key id="A50569C7CA7FA1F0" reason="Key couldn't be downloaded
from any key server"/>
@@ -130,7 +131,10 @@
<ignored-key id="340B090F727518D8" reason="Key couldn't be downloaded
from any key server"/>
</ignored-keys>
<trusted-keys>
- <trusted-key id="0181A4828FA27B6BE6F1F5A68611CD28F472E006"
group="org.jline"/>
+ <trusted-key id="0181A4828FA27B6BE6F1F5A68611CD28F472E006">
+ <trusting group="org.apache.maven"/>
+ <trusting group="org.jline"/>
+ </trusted-key>
<trusted-key id="F3184BCD55F4D016E30D4C9BF42E87F9665015C9"
group="org.jsoup"/>
<trusted-key id="019082BC00E0324E2AEF4CF00D3B328562A119A7"
group="org.openjdk.jmh"/>
<trusted-key id="0191E61ACBBE76323AC15C83B5AD94BDD6BDB924"
group="me.champeau.openbeans" name="openbeans" version="1.0.2"/>
@@ -434,7 +438,7 @@
</component>
<component group="com.github.jk1" name="gradle-license-report"
version="3.1.1">
<artifact name="gradle-license-report-3.1.1.jar">
- <sha512
value="dd46c6b743a114773a89ac7df92c7aae4bb78a9412868fc6212d364acc22059a84adf1ffc7edc7f1ebb781199d5bbfd2ef9dbd909b6b8e806a0a7b5045d2ec5f"
origin="Generated by Gradle"/>
+ <sha512
value="dd46c6b743a114773a89ac7df92c7aae4bb78a9412868fc6212d364acc22059a84adf1ffc7edc7f1ebb781199d5bbfd2ef9dbd909b6b8e806a0a7b5045d2ec5f"
origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.github.jnr" name="jffi" version="1.3.13">
@@ -1232,6 +1236,12 @@
<sha512
value="697af1320d949b0e03d29cc097143c4f8724e26ec944b936c201b983b88117dd7037f843f099b0fd955f1f5355c4d5fc2f12ca7c2b8aea3ce16d9b01b83fbcb5"
origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
+ <component group="org.apache.felix" name="org.apache.felix.framework"
version="7.0.5">
+ <artifact name="org.apache.felix.framework-7.0.5.jar">
+ <pgp value="3E97979229E01DFAB9774BBC9054823A859A7237"/>
+ <sha512
value="b632c8228e70f0917c1a2bda5dafceeff6582c5f3372b4000c7823018baefcfe695d108fa21820151b7b56f8fb24b0fb3876c3c966436bebba4cf5e759a2c51a"
origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="org.apache.httpcomponents" name="httpclient"
version="4.5.13">
<artifact name="httpclient-4.5.13.jar">
<sha512
value="3567739186e551f84cad3e4b6b270c5b8b19aba297675a96bcdff3663ff7d20d188611d21f675fe5ff1bfd7d8ca31362070910d7b92ab1b699872a120aa6f089"
origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -1803,6 +1813,11 @@
<pgp value="82F833963889D7ED06F1E4DC6525FD70CC303655"/>
</artifact>
</component>
+ <component group="org.codehaus.mojo" name="animal-sniffer-annotations"
version="1.9">
+ <artifact name="animal-sniffer-annotations-1.9.jar">
+ <sha512
value="356031c835d382badbec0f9ee246e32510e62b932721b31165397f2cc04a72108365967e241809ec6d190de5628ce34a61803266fd3270126e388496c10c5ec6"
origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
+ </artifact>
+ </component>
<component group="org.codehaus.plexus" name="plexus-cipher"
version="2.0">
<artifact name="plexus-cipher-2.0.jar">
<pgp value="6A814B1F869C2BBEAB7CB7271A2A1C94BDE89688"/>
@@ -1884,6 +1899,7 @@
</component>
<component group="org.codehaus.plexus" name="plexus-utils"
version="4.0.3">
<artifact name="plexus-utils-4.0.3.jar">
+ <pgp value="0181A4828FA27B6BE6F1F5A68611CD28F472E006"/>
<sha512
value="ed864c502a54ab2e8e2d4c74479b1cb48c5e44ee56fcad6ba5aec9e20e3e765148299cb8eb8c9a516fb59bf836b12886caca00a9b12eb5cb036271df3437218d"
origin="Generated by Gradle"/>
</artifact>
</component>
@@ -2263,6 +2279,11 @@
<sha512
value="efded31ef5b342f09422935076e599789076431e93a746685c0607e7de5592719ba6aacde0be670b3f064d1e85630d58d5bce6b34aed2a288fdb34f745efb7bc"
origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
</artifact>
</component>
+ <component group="org.junit.jupiter" name="junit-jupiter"
version="5.10.2">
+ <artifact name="junit-jupiter-5.10.2.jar">
+ <sha512
value="d10edf43b62c5947b50c506e84d65829138970acd6a85c066d7c6ca192477bed197af77866f6b18ea7b8ebc8a1a16666dc7982a967533079e8927a65aa3b484d"
origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="org.junit.jupiter" name="junit-jupiter"
version="5.14.3">
<artifact name="junit-jupiter-5.14.3.jar">
<sha512
value="ec8c3f06c181ce7bc2803d7769701645b40f05d9d4ac948f9de6ed2ebaf76bba727d2b91d4d81defce082ac30215e29ede59028e5fd8d86bd5df1bbc5b7cea23"
origin="Generated by Gradle"/>
diff --git a/settings.gradle b/settings.gradle
index 07f1735d75..a6d7cebf24 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -69,6 +69,7 @@ def subprojects = [
'groovy-macro',
'groovy-macro-library',
'groovy-nio',
+ 'groovy-osgi-test',
'groovy-servlet',
'groovy-sql',
'groovy-swing',
diff --git a/subprojects/groovy-osgi-test/build.gradle
b/subprojects/groovy-osgi-test/build.gradle
new file mode 100644
index 0000000000..7b6d6b2a67
--- /dev/null
+++ b/subprojects/groovy-osgi-test/build.gradle
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// Minimal OSGi integration test using embedded Apache Felix.
+// Installs the Groovy bundle and verifies resolution + class loading.
+
+plugins {
+ id 'java'
+}
+
+dependencies {
+ testImplementation 'org.apache.felix:org.apache.felix.framework:7.0.5'
+ testImplementation "org.junit.jupiter:junit-jupiter:${versions.junit5}"
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+}
+
+def v = rootProject.version
+def groovyJar = rootProject.file("build/libs/groovy-${v}.jar")
+def jsonJar = project(':groovy-json').file("build/libs/groovy-json-${v}.jar")
+
+test {
+ useJUnitPlatform()
+ dependsOn ':jarjar', ':groovy-json:jar'
+ systemProperty 'groovy.bundle.path', groovyJar.absolutePath
+ systemProperty 'groovy.json.bundle.path', jsonJar.absolutePath
+}
diff --git
a/subprojects/groovy-osgi-test/src/test/java/org/apache/groovy/osgi/OsgiBundleTest.java
b/subprojects/groovy-osgi-test/src/test/java/org/apache/groovy/osgi/OsgiBundleTest.java
new file mode 100644
index 0000000000..8982fbf6ba
--- /dev/null
+++
b/subprojects/groovy-osgi-test/src/test/java/org/apache/groovy/osgi/OsgiBundleTest.java
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.osgi;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.launch.Framework;
+import org.osgi.framework.launch.FrameworkFactory;
+import org.osgi.framework.wiring.FrameworkWiring;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Minimal OSGi integration test using embedded Apache Felix.
+ * <p>
+ * Verifies that Groovy bundles have correct OSGi metadata for
+ * resolution and class loading in an OSGi container.
+ */
+class OsgiBundleTest {
+
+ private Framework framework;
+
+ @BeforeEach
+ void startFramework() throws Exception {
+ Path cacheDir = Files.createTempDirectory("osgi-cache");
+
+ Map<String, String> config = new HashMap<>();
+ config.put("org.osgi.framework.storage",
cacheDir.toAbsolutePath().toString());
+ config.put("org.osgi.framework.storage.clean", "onFirstInit");
+ config.put("org.osgi.framework.bootdelegation",
"java.*,javax.*,sun.*,com.sun.*,jdk.*");
+
+ FrameworkFactory factory =
ServiceLoader.load(FrameworkFactory.class).iterator().next();
+ framework = factory.newFramework(config);
+ framework.start();
+ }
+
+ @AfterEach
+ void stopFramework() throws Exception {
+ if (framework != null) {
+ framework.stop();
+ framework.waitForStop(10_000);
+ }
+ }
+
+ // ---- Groovy core bundle tests ----
+
+ @Test
+ void groovyBundleInstalls() throws Exception {
+ Bundle bundle = installGroovyBundle();
+ assertNotNull(bundle);
+ assertTrue(bundle.getState() >= Bundle.INSTALLED);
+ }
+
+ @Test
+ void groovyBundleHasCorrectSymbolicName() throws Exception {
+ Bundle bundle = installGroovyBundle();
+ assertEquals("groovy", bundle.getSymbolicName());
+ }
+
+ @Test
+ void groovyBundleHasCorrectHeaders() throws Exception {
+ Bundle bundle = installGroovyBundle();
+ assertNotNull(bundle.getHeaders().get("Bundle-Version"));
+ assertNotNull(bundle.getHeaders().get("Export-Package"));
+ assertNotNull(bundle.getHeaders().get("Import-Package"));
+ assertEquals("org.apache.groovy",
bundle.getHeaders().get("Automatic-Module-Name"));
+ }
+
+ @Test
+ void groovyBundleResolves() throws Exception {
+ Bundle bundle = installGroovyBundle();
+
+ FrameworkWiring wiring = framework.adapt(FrameworkWiring.class);
+ boolean resolved =
wiring.resolveBundles(Collections.singleton(bundle));
+
+ assertTrue(resolved, "Groovy bundle should resolve. State: " +
stateName(bundle.getState()));
+ }
+
+ @Test
+ void groovyBundleStartsAndLoadsClasses() throws Exception {
+ Bundle bundle = installGroovyBundle();
+
+ FrameworkWiring wiring = framework.adapt(FrameworkWiring.class);
+ wiring.resolveBundles(Collections.singleton(bundle));
+
+ bundle.start();
+ assertEquals(Bundle.ACTIVE, bundle.getState(), "Bundle should be
ACTIVE");
+
+ assertClassLoadable(bundle, "groovy.lang.GroovyObject");
+ assertClassLoadable(bundle, "groovy.lang.GroovyShell");
+ assertClassLoadable(bundle, "groovy.lang.Closure");
+ assertClassLoadable(bundle, "groovy.lang.Script");
+ assertClassLoadable(bundle,
"org.codehaus.groovy.runtime.InvokerHelper");
+ }
+
+ // ---- Groovy JSON fragment tests ----
+
+ @Test
+ @org.junit.jupiter.api.Disabled("Fragment resolution needs investigation —
see GROOVY-5092")
+ void groovyJsonFragmentResolvesAndLoadsClasses() throws Exception {
+ String jsonPath = System.getProperty("groovy.json.bundle.path");
+ if (jsonPath == null || !new File(jsonPath).exists()) {
+ System.out.println("Skipping JSON test — groovy.json.bundle.path
not set or jar not found");
+ return;
+ }
+
+ // Install fragment before host so Felix sees it during resolution
+ BundleContext ctx = framework.getBundleContext();
+ Bundle jsonBundle = ctx.installBundle("file:" + jsonPath);
+ Bundle groovyBundle = installGroovyBundle();
+
+ FrameworkWiring wiring = framework.adapt(FrameworkWiring.class);
+ wiring.resolveBundles(Arrays.asList(groovyBundle, jsonBundle));
+
+ assertTrue(groovyBundle.getState() >= Bundle.RESOLVED,
+ "Groovy host should resolve. State: " +
stateName(groovyBundle.getState()));
+ assertTrue(jsonBundle.getState() >= Bundle.RESOLVED,
+ "groovy-json fragment should resolve. State: " +
stateName(jsonBundle.getState()));
+
+ groovyBundle.start();
+
+ // Fragment classes are loaded through the host bundle
+ assertClassLoadable(groovyBundle, "groovy.json.JsonSlurper");
+ assertClassLoadable(groovyBundle, "groovy.json.JsonOutput");
+ }
+
+ @Test
+ @org.junit.jupiter.api.Disabled("Fragment resolution needs investigation —
see GROOVY-5092")
+ void groovyJsonOutputProducesCorrectResult() throws Exception {
+ String jsonPath = System.getProperty("groovy.json.bundle.path");
+ if (jsonPath == null || !new File(jsonPath).exists()) {
+ System.out.println("Skipping JSON test — groovy.json.bundle.path
not set or jar not found");
+ return;
+ }
+
+ BundleContext ctx = framework.getBundleContext();
+ Bundle jsonBundle = ctx.installBundle("file:" + jsonPath);
+ Bundle groovyBundle = installGroovyBundle();
+
+ FrameworkWiring wiring = framework.adapt(FrameworkWiring.class);
+ wiring.resolveBundles(Arrays.asList(groovyBundle, jsonBundle));
+ groovyBundle.start();
+
+ // Use JsonOutput via reflection — we can't compile against it directly
+ Class<?> jsonOutputClass =
groovyBundle.loadClass("groovy.json.JsonOutput");
+ java.lang.reflect.Method toJson = jsonOutputClass.getMethod("toJson",
Object.class);
+ java.lang.reflect.Method prettyPrint =
jsonOutputClass.getMethod("prettyPrint", String.class);
+
+ // Equivalent of: JsonOutput.prettyPrint(JsonOutput.toJson([one: 1,
two: 2]))
+ Map<String, Integer> data = new java.util.LinkedHashMap<>();
+ data.put("one", 1);
+ data.put("two", 2);
+
+ String jsonStr = (String) toJson.invoke(null, data);
+ String pretty = (String) prettyPrint.invoke(null, jsonStr);
+ String expected = "{\n \"one\": 1,\n \"two\": 2\n}";
+ assertEquals(expected, pretty);
+ }
+
+ // ---- helpers ----
+
+ private Bundle installGroovyBundle() throws BundleException {
+ String bundlePath = System.getProperty("groovy.bundle.path");
+ assertNotNull(bundlePath, "System property 'groovy.bundle.path' must
be set");
+ assertTrue(new File(bundlePath).exists(), "Groovy jar not found: " +
bundlePath);
+
+ BundleContext ctx = framework.getBundleContext();
+ return ctx.installBundle("file:" + bundlePath);
+ }
+
+ private void assertClassLoadable(Bundle bundle, String className) {
+ try {
+ Class<?> clazz = bundle.loadClass(className);
+ assertNotNull(clazz, "loadClass returned null for " + className);
+ } catch (ClassNotFoundException e) {
+ fail("Class not found in bundle: " + className + " — " +
e.getMessage());
+ }
+ }
+
+ private static String stateName(int state) {
+ return switch (state) {
+ case Bundle.UNINSTALLED -> "UNINSTALLED";
+ case Bundle.INSTALLED -> "INSTALLED";
+ case Bundle.RESOLVED -> "RESOLVED";
+ case Bundle.STARTING -> "STARTING";
+ case Bundle.STOPPING -> "STOPPING";
+ case Bundle.ACTIVE -> "ACTIVE";
+ default -> "UNKNOWN(" + state + ")";
+ };
+ }
+}