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
