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 + ")";
+        };
+    }
+}

Reply via email to