This is an automated email from the ASF dual-hosted git repository.

jsedding pushed a commit to branch org.apache.sling.testing.osgi.unit
in repository https://gitbox.apache.org/repos/asf/sling-whiteboard.git

commit 2181bb3ad3d4207a3b90405680b2fe18fcdbfd42
Author: Julian Sedding <[email protected]>
AuthorDate: Tue Jan 9 01:41:06 2024 +0100

    use AppClassLoader for bundles importing org.junit.jupiter.api.extension
    
    - this allows e.g. TestInfo and RepetitionInfo to be injected
---
 README.md                                          |  10 --
 pom.xml                                            |   6 +
 .../osgi/unit/BundleWithClassLoaderContent.java    | 173 +++++++++++++++++++++
 .../osgi/unit/OSGiSupportFrameworkHandler.java     |  64 +++++++-
 .../unit/OSGiSupportInvocationInterceptor.java     |   2 +-
 .../osgi/unit/OSGiSupportParameterResolver.java    |   1 +
 .../testing/osgi/unit/impl/OSGiSupportTest.java    |  16 +-
 7 files changed, 244 insertions(+), 28 deletions(-)

diff --git a/README.md b/README.md
index 2d2f63ef..025cd4e3 100644
--- a/README.md
+++ b/README.md
@@ -40,13 +40,3 @@ class SampleTest {
     } 
 }
 ```
-
-## Known Limitations
-
-Due to the fact that the test-class and test-instance are processed outside 
the OSGi environment,
-`org.junit.jupiter.api.extension.ParameterResolver` implementations returning 
objects that are **not
-exported by the system-bundle** in a running OSGi framework, cannot currently 
be injected.
-
-Known injections that are currently impossible due to this limitation are
-- `org.junit.jupiter.api.TestInfo`
-- `org.junit.jupiter.api.RepetitionInfo`
diff --git a/pom.xml b/pom.xml
index 8531d1d3..b7fc5d94 100644
--- a/pom.xml
+++ b/pom.xml
@@ -121,6 +121,12 @@
             <scope>provided</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.framework</artifactId>
+            <version>1.10.0</version>
+            <scope>compile</scope>
+        </dependency>
         <dependency>
             <groupId>biz.aQute.bnd</groupId>
             <artifactId>biz.aQute.bndlib</artifactId>
diff --git 
a/src/main/java/org/apache/sling/testing/osgi/unit/BundleWithClassLoaderContent.java
 
b/src/main/java/org/apache/sling/testing/osgi/unit/BundleWithClassLoaderContent.java
new file mode 100644
index 00000000..f38da4d5
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/testing/osgi/unit/BundleWithClassLoaderContent.java
@@ -0,0 +1,173 @@
+/*
+ * 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.sling.testing.osgi.unit;
+
+import aQute.bnd.osgi.resource.ResourceUtils;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.framework.connect.ConnectContent;
+import org.osgi.resource.Resource;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+class BundleWithClassLoaderContent implements ConnectContent {
+
+    private final Resource resource;
+
+    private final ClassLoader classLoader;
+
+    private final Path tempDir;
+
+    private volatile List<String> entries;
+
+    public BundleWithClassLoaderContent(Resource resource, ClassLoader 
classLoader, Path tempDir) {
+        this.resource = resource;
+        this.classLoader = classLoader;
+        this.tempDir = tempDir;
+    }
+
+    @Override
+    public Optional<Map<String, String>> getHeaders() {
+        return Optional.empty();
+    }
+
+    @Override
+    public Iterable<String> getEntries() {
+        return entries;
+    }
+
+    @Override
+    public Optional<ConnectEntry> getEntry(String path) {
+        return Optional.of(path)
+                .filter(entries::contains)
+                .map(Entry::new);
+    }
+
+    @Override
+    public Optional<ClassLoader> getClassLoader() {
+        return Optional.of(classLoader);
+    }
+
+    @Override
+    public void open() throws IOException {
+        final Optional<URI> uri = ResourceUtils.getURI(resource);
+        try (ZipFile zipFile = new ZipFile(Path.of(uri.get()).toFile())) {
+            this.entries = Collections.list(zipFile.entries()).stream()
+                    .map(ZipEntry::getName)
+                    .collect(Collectors.toUnmodifiableList());
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        Files.walkFileTree(getBundleDirectory(), new SimpleFileVisitor<>() {
+            @Override
+            public FileVisitResult visitFile(Path file, BasicFileAttributes 
attrs) throws IOException {
+                Files.delete(file);
+                return FileVisitResult.CONTINUE;
+            }
+
+            @Override
+            public FileVisitResult postVisitDirectory(Path dir, IOException 
exc) throws IOException {
+                Files.delete(dir);
+                return FileVisitResult.CONTINUE;
+            }
+        });
+        this.entries = List.of();
+    }
+
+    @NotNull
+    private Path getBundleDirectory() {
+        return this.tempDir;
+    }
+
+    private class Entry implements ConnectEntry {
+        private final String path;
+
+        public Entry(String path) {
+            this.path = path;
+        }
+
+        @Override
+        public String getName() {
+            return path;
+        }
+
+        @Override
+        public long getContentLength() {
+            return withZipEntry(((zipFile, zipEntry) -> zipEntry.getSize()));
+        }
+
+        @Override
+        public long getLastModified() {
+            return withZipEntry(((zipFile, zipEntry) -> 
zipEntry.getLastModifiedTime().toMillis()));
+        }
+
+        @Override
+        public InputStream getInputStream() {
+            return withZipEntry(this::copyAndRead);
+        }
+
+        private InputStream copyAndRead(ZipFile zipFile, ZipEntry zipEntry) 
throws IOException {
+            final long size = zipEntry.getSize();
+            if (0 <= size && size < 512 * 1024) {
+                final ByteArrayOutputStream bytes = new 
ByteArrayOutputStream((int) size);
+                zipFile.getInputStream(zipEntry).transferTo(bytes);
+                return new ByteArrayInputStream(bytes.toByteArray());
+            } else {
+                final Path entryPath = 
getBundleDirectory().resolve(zipEntry.getName());
+                if (Files.notExists(entryPath)) {
+                    Files.createDirectories(entryPath.getParent());
+                    Files.copy(zipFile.getInputStream(zipEntry), entryPath);
+                }
+                return Files.newInputStream(entryPath);
+            }
+        }
+
+        private <T> T withZipEntry(ThrowingBiFunction<ZipFile, ZipEntry, T> 
zipEntryFn) {
+            final URI uri = ResourceUtils.getURI(resource).get();
+            try (ZipFile zipFile = new ZipFile(Path.of(uri).toFile())) {
+                final ZipEntry entry = zipFile.getEntry(path);
+                return zipEntryFn.apply(zipFile, entry);
+            } catch (Throwable e) {
+                throw new UncheckedIOException(new IOException(e));
+            }
+        }
+    }
+
+    private interface ThrowingBiFunction<S, T, R> {
+        R apply(S s, T t) throws Throwable;
+    }
+}
diff --git 
a/src/main/java/org/apache/sling/testing/osgi/unit/OSGiSupportFrameworkHandler.java
 
b/src/main/java/org/apache/sling/testing/osgi/unit/OSGiSupportFrameworkHandler.java
index 126d78c5..f9ff774c 100644
--- 
a/src/main/java/org/apache/sling/testing/osgi/unit/OSGiSupportFrameworkHandler.java
+++ 
b/src/main/java/org/apache/sling/testing/osgi/unit/OSGiSupportFrameworkHandler.java
@@ -32,12 +32,17 @@ import 
org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
 import org.junit.jupiter.api.extension.ExtensionContext;
 import org.junit.platform.commons.support.AnnotationSupport;
 import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleActivator;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.BundleException;
+import org.osgi.framework.connect.ConnectContent;
+import org.osgi.framework.connect.ConnectFrameworkFactory;
+import org.osgi.framework.connect.ConnectModule;
+import org.osgi.framework.connect.ModuleConnector;
 import org.osgi.framework.launch.Framework;
-import org.osgi.framework.launch.FrameworkFactory;
 import org.osgi.framework.startlevel.BundleStartLevel;
 import org.osgi.framework.startlevel.FrameworkStartLevel;
+import org.osgi.resource.Requirement;
 import org.osgi.resource.Resource;
 import org.osgi.resource.Wire;
 import org.osgi.service.resolver.ResolutionException;
@@ -60,6 +65,7 @@ import java.util.Optional;
 import java.util.ServiceLoader;
 import java.util.concurrent.CountDownLatch;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -67,17 +73,23 @@ import static java.util.Arrays.asList;
 import static java.util.function.Predicate.not;
 import static 
org.apache.sling.testing.osgi.unit.impl.BundleUtil.collectJarFilesFromClassPath;
 import static org.osgi.framework.Constants.FRAMEWORK_STORAGE;
+import static org.osgi.framework.namespace.PackageNamespace.PACKAGE_NAMESPACE;
 
 class OSGiSupportFrameworkHandler implements BeforeTestExecutionCallback, 
AfterTestExecutionCallback {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(OSGiSupportFrameworkHandler.class);
 
     private static final ExtensionContext.Namespace namespace = 
ExtensionContext.Namespace.create(OSGiSupportInvocationInterceptor.class);
+    public static final String JUPITER_EXTENSION_API_PACKAGE = 
"org.junit.jupiter.api.extension";
 
     static ExtensionContext.Store getStore(ExtensionContext context) {
         return context.getStore(namespace);
     }
 
+    static Predicate<Requirement> isPackage(String packageName) {
+        return req -> 
Objects.equals(req.getAttributes().get(PACKAGE_NAMESPACE).toString(), 
packageName);
+    }
+
     @Override
     public void beforeTestExecution(ExtensionContext context) throws Exception 
{
         final Class<?> testClass = context.getRequiredTestClass();
@@ -105,16 +117,56 @@ class OSGiSupportFrameworkHandler implements 
BeforeTestExecutionCallback, AfterT
                 osgiIdentities(bundleSymbolicName, 
config.getLogServiceBundles(), config.getAdditionalBundles()),
                 testBundleJar);
 
+        final Map<Boolean, List<Resource>> bundles = 
resolutions.keySet().stream()
+                .collect(Collectors.groupingBy(res -> 
res.getRequirements(PACKAGE_NAMESPACE).stream()
+                        .anyMatch(isPackage(JUPITER_EXTENSION_API_PACKAGE))
+                ));
+        final List<Resource> jupiterExtensionProviders = 
bundles.get(Boolean.TRUE);
+        final List<Resource> normalBundles = bundles.get(Boolean.FALSE);
+
+        final Map<String, Resource> jupiterExtensionProvidersByUri = 
jupiterExtensionProviders.stream()
+                .collect(Collectors.toMap(res -> 
ResourceUtils.getURI(res).get().toString(), Function.identity()));
+
         final Map<String, String> frameworkProperties = Map.of(
                 FRAMEWORK_STORAGE, 
Files.createDirectories(storageDir).toAbsolutePath().toString());
 
-        ServiceLoader<FrameworkFactory> frameworkFactories = 
ServiceLoader.load(FrameworkFactory.class);
-        final FrameworkFactory factory = frameworkFactories.iterator().next();
-        final Framework framework = factory.newFramework(frameworkProperties);
+        ServiceLoader<ConnectFrameworkFactory> frameworkFactories = 
ServiceLoader.load(ConnectFrameworkFactory.class);
+        final ConnectFrameworkFactory factory = 
frameworkFactories.iterator().next();
+        final Framework framework = factory.newFramework(frameworkProperties, 
new ModuleConnector() {
+            @Override
+            public void initialize(File file, Map<String, String> map) {
+
+            }
+
+            @Override
+            public Optional<ConnectModule> connect(String uri) {
+                return 
Optional.ofNullable(jupiterExtensionProvidersByUri.get(uri))
+                        .map(resource -> new ConnectModule() {
+                            @Override
+                            public ConnectContent getContent() throws 
IOException {
+                                final Path bundleTempDir = 
Files.createDirectories(
+                                        storageDir
+                                                .resolve("connectBundles")
+                                                
.resolve(ResourceUtils.getIdentity(resource) + '-' + 
ResourceUtils.getIdentityVersion(resource)));
+                                return new 
BundleWithClassLoaderContent(resource, getClass().getClassLoader(), 
bundleTempDir);
+                            }
+                        });
+            }
+
+            @Override
+            public Optional<BundleActivator> newBundleActivator() {
+                return Optional.empty();
+            }
+        });
+
         framework.init();
         framework.start();
 
-        for (Resource resource : resolutions.keySet()) {
+        for (Resource resource : jupiterExtensionProvidersByUri.values()) {
+            
framework.getBundleContext().installBundle(ResourceUtils.getURI(resource).get().toString(),
 null);
+        }
+
+        for (Resource resource : normalBundles) {
             // fragments are not added to the resolutions map by the resolver,
             // they need to be retrieved from the wire of a host bundle
             final List<Resource> fragments = resolutions.get(resource).stream()
@@ -218,7 +270,7 @@ class OSGiSupportFrameworkHandler implements 
BeforeTestExecutionCallback, AfterT
                 .collect(Collectors.joining(","));
     }
 
-    private static Optional<Bundle> installBundle(Framework framework, 
Resource resource) throws IOException, BundleException {
+    private static Optional<Bundle> installBundle(Framework framework, 
Resource resource) {
         return ResourceUtils.getURI(resource)
                 .map(uri -> {
                     try (InputStream bundleStream = 
Files.newInputStream(Path.of(uri))) {
diff --git 
a/src/main/java/org/apache/sling/testing/osgi/unit/OSGiSupportInvocationInterceptor.java
 
b/src/main/java/org/apache/sling/testing/osgi/unit/OSGiSupportInvocationInterceptor.java
index 20c430eb..e5af46e1 100644
--- 
a/src/main/java/org/apache/sling/testing/osgi/unit/OSGiSupportInvocationInterceptor.java
+++ 
b/src/main/java/org/apache/sling/testing/osgi/unit/OSGiSupportInvocationInterceptor.java
@@ -45,7 +45,7 @@ import java.util.stream.Stream;
 
 import static java.util.function.Predicate.not;
 
-public class OSGiSupportInvocationInterceptor implements InvocationInterceptor 
{
+class OSGiSupportInvocationInterceptor implements InvocationInterceptor {
 
     @Override
     public void interceptTestMethod(Invocation<Void> invocation, 
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext 
extensionContext) throws Throwable {
diff --git 
a/src/main/java/org/apache/sling/testing/osgi/unit/OSGiSupportParameterResolver.java
 
b/src/main/java/org/apache/sling/testing/osgi/unit/OSGiSupportParameterResolver.java
index 5b32e80d..1b3417f9 100644
--- 
a/src/main/java/org/apache/sling/testing/osgi/unit/OSGiSupportParameterResolver.java
+++ 
b/src/main/java/org/apache/sling/testing/osgi/unit/OSGiSupportParameterResolver.java
@@ -46,6 +46,7 @@ class OSGiSupportParameterResolver implements 
ParameterResolver {
         final Optional<Service> serviceAnnotation = 
parameterContext.findAnnotation(Service.class);
         if (serviceAnnotation.isPresent()) {
             // services need to be resolved late, because they may be loaded 
by different class loaders in OSGi
+            // see OSGiSupportInvocationInterceptor
             serviceAnnotations[parameterContext.getIndex()] = 
serviceAnnotation.get();
             return null;
         }
diff --git 
a/src/test/java/org/apache/sling/testing/osgi/unit/impl/OSGiSupportTest.java 
b/src/test/java/org/apache/sling/testing/osgi/unit/impl/OSGiSupportTest.java
index 79ff3ebf..f787cbe4 100644
--- a/src/test/java/org/apache/sling/testing/osgi/unit/impl/OSGiSupportTest.java
+++ b/src/test/java/org/apache/sling/testing/osgi/unit/impl/OSGiSupportTest.java
@@ -21,7 +21,6 @@ package org.apache.sling.testing.osgi.unit.impl;
 import org.apache.sling.testing.osgi.unit.OSGiSupport;
 import org.apache.sling.testing.osgi.unit.Service;
 import org.jetbrains.annotations.NotNull;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.RepeatedTest;
 import org.junit.jupiter.api.RepetitionInfo;
 import org.junit.jupiter.api.Test;
@@ -45,6 +44,7 @@ import java.nio.file.Path;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -191,7 +191,7 @@ class OSGiSupportTest {
     void serviceReferenceInjection(
             @Service ServiceReference<Condition> serviceReference,
             @Service ServiceReference<Condition>[] serviceReferencesArray,
-            @Service List<ServiceReference<Condition>> serviceReferencesList) 
throws Throwable {
+            @Service List<ServiceReference<Condition>> serviceReferencesList) {
         assertThat(serviceReference)
                 .isNotNull()
                 .matches(ref -> withService(ref, Condition.class::isInstance));
@@ -227,11 +227,7 @@ class OSGiSupportTest {
                 .allMatch(reference -> withService(reference, 
Condition.class::isInstance));
     }
 
-    private interface ThrowingPredicate<T> {
-        boolean test(T object) throws Throwable;
-    }
-
-    private static <T> boolean withService(ServiceObjects<T> serviceObjects, 
ThrowingPredicate<T> servicePredicate) {
+    private static <T> boolean withService(ServiceObjects<T> serviceObjects, 
Predicate<T> servicePredicate) {
         final T service = serviceObjects.getService();
         try {
             return servicePredicate.test(service);
@@ -243,7 +239,7 @@ class OSGiSupportTest {
         }
     }
 
-    private static <T> boolean withService(ServiceReference<T> 
serviceReference, ThrowingPredicate<T> servicePredicate) {
+    private static <T> boolean withService(ServiceReference<T> 
serviceReference, Predicate<T> servicePredicate) {
         final BundleContext bc = 
serviceReference.getBundle().getBundleContext();
         try {
             final T service = bc.getService(serviceReference);
@@ -279,7 +275,6 @@ class OSGiSupportTest {
         assertNotNull(condition);
     }
 
-    @Disabled("The RepetitionInfo class of the resolved object is different 
from the RepetitionInfo class within OSGi ")
     @RepeatedTest(2)
     void repetitionInfoSupport(Framework framework, @Service Condition 
condition, RepetitionInfo repetitionInfo) {
         assertNotNull(framework);
@@ -287,12 +282,11 @@ class OSGiSupportTest {
         assertThat(repetitionInfo.getCurrentRepetition()).isBetween(1, 
repetitionInfo.getTotalRepetitions());
     }
 
-    @Disabled("The TestInfo class of the resolved object is different from the 
TestInfo class within OSGi ")
     @Test
     void testInfoSupport(Framework framework, @Service Condition condition, 
TestInfo testInfo) {
         assertNotNull(framework);
         assertNotNull(condition);
-        assertThat(testInfo.getDisplayName()).isEqualTo("testInfoSupport");
+        
assertThat(testInfo.getDisplayName()).isEqualTo("testInfoSupport(Framework, 
Condition, TestInfo)");
     }
 
     @NotNull

Reply via email to