You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by js...@apache.org on 2021/06/16 14:00:09 UTC
[sling-org-apache-sling-junit-core] 01/01: SLING-10497 - JUnit
Jupiter ParameterResolver for OSGi
This is an automated email from the ASF dual-hosted git repository.
jsedding pushed a commit to branch feature/SLING-10497-jupiter-parameter-resolver-for-osgi
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-junit-core.git
commit 3ed448a258a8503684893ffd6c41b46680fc9422
Author: Julian Sedding <js...@apache.org>
AuthorDate: Wed Jun 16 15:58:46 2021 +0200
SLING-10497 - JUnit Jupiter ParameterResolver for OSGi
---
bnd.bnd | 2 +
pom.xml | 22 +-
.../junit5/JUnit5TestExecutionStrategy.java | 45 +--
.../impl/servlet/junit5/JUnitPlatformHelper.java | 117 ++++++
.../org/apache/sling/junit/jupiter/osgi/OSGi.java | 53 +++
.../apache/sling/junit/jupiter/osgi/Service.java | 72 ++++
.../apache/sling/junit/jupiter/osgi/Services.java | 36 ++
.../impl/AbstractTypeBasedParameterResolver.java | 66 ++++
.../osgi/impl/BundleContextParameterResolver.java | 33 ++
.../jupiter/osgi/impl/BundleParameterResolver.java | 33 ++
.../junit/jupiter/osgi/impl/ReflectionHelper.java | 103 ++++++
.../osgi/impl/ServiceParameterResolver.java | 263 ++++++++++++++
.../osgi/impl/TypeBasedParameterResolver.java | 53 +++
.../sling/junit/jupiter/osgi/package-info.java | 22 ++
.../sling/junit/impl/servlet/HtmlRendererTest.java | 16 +-
.../junit/jupiter/osgi/OSGiAnnotationTest.java | 392 +++++++++++++++++++++
16 files changed, 1270 insertions(+), 58 deletions(-)
diff --git a/bnd.bnd b/bnd.bnd
index aea6f89..196c623 100644
--- a/bnd.bnd
+++ b/bnd.bnd
@@ -1,8 +1,10 @@
Bundle-Activator: org.apache.sling.junit.Activator
Export-Package: !org.junit.platform.*, \
+ !org.junit.jupiter.*, \
junit.*;version=${junit.version}, \
org.junit.*;version=${junit.version}, \
org.hamcrest.*;version=${hamcrest.version};-split-package:=merge-first
Import-Package: org.junit.platform.*;resolution:=optional, \
+ org.junit.jupiter.*;resolution:=optional, \
*
-includeresource: @org.jacoco.agent-*.jar!/org/jacoco/agent/rt/IAgent*
diff --git a/pom.xml b/pom.xml
index 9e68ab1..4428527 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,7 +37,7 @@
<junit.version>4.13</junit.version>
<hamcrest.version>1.3</hamcrest.version>
<jacoco.version>0.6.2.201302030002</jacoco.version>
- <junit-jupiter.version>5.7.1</junit-jupiter.version>
+ <junit-jupiter.version>5.5.0</junit-jupiter.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
@@ -285,6 +285,13 @@
<version>2.2.2</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ <version>3.0</version>
+ <scope>provided</scope>
+ </dependency>
+
<!-- optional imports for JUnit 5 support -->
<dependency>
<groupId>org.junit.platform</groupId>
@@ -319,7 +326,7 @@
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit-jupiter.version}</version>
- <scope>test</scope>
+ <scope>provided</scope>
</dependency>
<dependency>
@@ -345,8 +352,15 @@
<dependency>
<groupId>org.mockito</groupId>
- <artifactId>mockito-core</artifactId>
- <version>3.5.7</version>
+ <artifactId>mockito-inline</artifactId>
+ <version>3.10.0</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.testing.osgi-mock.junit5</artifactId>
+ <version>3.1.2</version>
<scope>test</scope>
</dependency>
</dependencies>
diff --git a/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnit5TestExecutionStrategy.java b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnit5TestExecutionStrategy.java
index 7363374..e39ed73 100644
--- a/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnit5TestExecutionStrategy.java
+++ b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnit5TestExecutionStrategy.java
@@ -21,22 +21,14 @@ package org.apache.sling.junit.impl.servlet.junit5;
import org.apache.sling.junit.TestSelector;
import org.apache.sling.junit.impl.TestExecutionStrategy;
import org.apache.sling.junit.impl.TestsManagerImpl;
-import org.jetbrains.annotations.NotNull;
-import org.junit.platform.engine.DiscoverySelector;
-import org.junit.platform.engine.TestEngine;
-import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
-import org.junit.platform.launcher.core.LauncherConfig;
-import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
-import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.runner.notification.RunListener;
import org.osgi.framework.BundleContext;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.stream.Stream;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
@@ -73,39 +65,10 @@ public class JUnit5TestExecutionStrategy implements TestExecutionStrategy {
@Override
public void execute(TestSelector selector, RunListener runListener) throws Exception {
- Launcher launcher = createLauncher(runListener, testEngineTracker.getAvailableTestEngines());
+ Launcher launcher = JUnitPlatformHelper.createLauncher(testEngineTracker.getAvailableTestEngines());
final LauncherDiscoveryRequest request = testsManager.createTestRequest(selector,
- JUnit5TestExecutionStrategy::methodRequest,
- JUnit5TestExecutionStrategy::classesRequest);
- launcher.execute(request);
- }
-
- @NotNull
- public static Launcher createLauncher(RunListener runListener, TestEngine... availableTestEngines) {
- return LauncherFactory.create(
- LauncherConfig.builder()
- .addTestEngines(availableTestEngines)
- .addTestExecutionListeners(new RunListenerAdapter(runListener))
- .enableTestEngineAutoRegistration(false)
- .enableTestExecutionListenerAutoRegistration(false)
- .build()
- );
- }
-
- @NotNull
- public static LauncherDiscoveryRequest methodRequest(Class<?> testClass, String testMethodName) {
- return LauncherDiscoveryRequestBuilder.request()
- .selectors(selectMethod(testClass, testMethodName))
- .build();
- }
-
- @NotNull
- public static LauncherDiscoveryRequest classesRequest(Class<?>... testClasses) {
- final DiscoverySelector[] selectors = Stream.of(testClasses)
- .map(DiscoverySelectors::selectClass)
- .toArray(DiscoverySelector[]::new);
- return LauncherDiscoveryRequestBuilder.request()
- .selectors(selectors)
- .build();
+ JUnitPlatformHelper::methodRequest,
+ JUnitPlatformHelper::classesRequest);
+ launcher.execute(request, new RunListenerAdapter(runListener));
}
}
diff --git a/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnitPlatformHelper.java b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnitPlatformHelper.java
new file mode 100644
index 0000000..f6c0312
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnitPlatformHelper.java
@@ -0,0 +1,117 @@
+/*
+ * 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.junit.impl.servlet.junit5;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.platform.commons.util.ReflectionUtils;
+import org.junit.platform.engine.DiscoverySelector;
+import org.junit.platform.engine.TestEngine;
+import org.junit.platform.engine.discovery.DiscoverySelectors;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.TestExecutionListener;
+import org.junit.platform.launcher.core.LauncherConfig;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import org.junit.platform.launcher.core.LauncherFactory;
+
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
+
+/**
+ * Utilities for running tests via the JUnit Platform. I.e. depending on the supplied {@link TestEngine}(s),
+ * it is possible to run JUnit3, JUnit4 or Jupiter based tests using {@code TestEngines} supplied by
+ * the JUnit team. No issues are expected running bespoke {@code TestEngines}.
+ */
+public final class JUnitPlatformHelper {
+
+ /**
+ * Execute a test class (if {@code testMethodName == null}) or a single test method with a specified {@code TestEngine}.
+ * All provided {@code TestExecutionListener}s are registered to be notified of the test's execution.
+ *
+ * @param testEngine a {@code TestEngine} instance
+ * @param testClass a test class that can be executed by the given {@code TestEngine}
+ * @param testMethodName the name of a test method in the given test class or null to run all test methods
+ * @param listeners any number of {@code TestExecutionListener}s that should be notified
+ */
+ public static void executeTest(@NotNull TestEngine testEngine, @NotNull Class<?> testClass, @Nullable String testMethodName, @NotNull TestExecutionListener... listeners) {
+ final Launcher launcher = JUnitPlatformHelper.createLauncher(testEngine);
+ final LauncherDiscoveryRequest request = testMethodName != null
+ ? JUnitPlatformHelper.methodRequest(testClass, testMethodName)
+ : JUnitPlatformHelper.classesRequest(testClass);
+ launcher.execute(request, listeners);
+ }
+
+ /**
+ * Utility method to create a {@link Launcher} for the given {@code TestEngines} only, without
+ * any automatically registered {@code TestEngines} or {@code TestExecutionListeners}.
+ *
+ * @param testEngines The test engines available to the {@code Launcher} instance.
+ * @return A JUnit Platform {@code Launcher} instance.
+ */
+ @NotNull
+ public static Launcher createLauncher(TestEngine... testEngines) {
+ return LauncherFactory.create(LauncherConfig.builder()
+ .enableTestEngineAutoRegistration(false)
+ .enableTestExecutionListenerAutoRegistration(false)
+ .addTestEngines(testEngines)
+ .build());
+ }
+
+ /**
+ * Utility to create a {@link LauncherDiscoveryRequest} for a particular test method, specified by the
+ * test class and the test method's name. If multiple overloaded test methods with different parameters
+ * exist, they would all be executed.
+ *
+ * @param testClass a test class
+ * @param testMethodName the name of a test method in the given test class or null to run all test methods
+ * @return a {@code LauncherDiscoveryRequest} representing the specified test method.
+ */
+ @NotNull
+ public static LauncherDiscoveryRequest methodRequest(Class<?> testClass, String testMethodName) {
+ final LauncherDiscoveryRequestBuilder requestBuilder = LauncherDiscoveryRequestBuilder.request();
+ ReflectionUtils.findMethods(testClass, method -> Objects.equals(method.getName(), testMethodName)).stream()
+ .map(method -> selectMethod(testClass, method))
+ .forEach(requestBuilder::selectors);
+ return requestBuilder.build();
+ }
+
+
+ /**
+ * Utility to create a {@link LauncherDiscoveryRequest} for all test methods of the specified test class(es).
+ *
+ * @param testClasses a number of test classes
+ * @return a {@code LauncherDiscoveryRequest} representing the specified test classes.
+ */
+ @NotNull
+ public static LauncherDiscoveryRequest classesRequest(Class<?>... testClasses) {
+ final DiscoverySelector[] selectors = Stream.of(testClasses)
+ .map(DiscoverySelectors::selectClass)
+ .toArray(DiscoverySelector[]::new);
+ return LauncherDiscoveryRequestBuilder.request()
+ .selectors(selectors)
+ .build();
+ }
+
+ private JUnitPlatformHelper() {
+ // no instances
+ }
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/OSGi.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/OSGi.java
new file mode 100644
index 0000000..f5a07ae
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/OSGi.java
@@ -0,0 +1,53 @@
+/*
+ * 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.junit.jupiter.osgi;
+
+import org.apache.sling.junit.jupiter.osgi.impl.BundleContextParameterResolver;
+import org.apache.sling.junit.jupiter.osgi.impl.BundleParameterResolver;
+import org.apache.sling.junit.jupiter.osgi.impl.ServiceParameterResolver;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * OSGi test annotation, for running unit tests within OSGi frameworks. The annotation supports
+ * injecting {@link Bundle}, {@link BundleContext} and service instances in conjunction with the
+ * {@link Service @Service} annotation. The annotation can be used on test classes or on individual
+ * test methods. If used on test classes injection of constructor parameters is supported in addition
+ * to injection of method parameters.
+ * <br>
+ * Note: the implementation relies on calling {@link FrameworkUtil#getBundle(Class)} with the test class
+ * in order to gain access to the world of OSGi.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+@ExtendWith({
+ BundleParameterResolver.class,
+ BundleContextParameterResolver.class,
+ ServiceParameterResolver.class
+})
+@Inherited
+public @interface OSGi {}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/Service.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/Service.java
new file mode 100644
index 0000000..3e901c3
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/Service.java
@@ -0,0 +1,72 @@
+/*
+ * 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.junit.jupiter.osgi;
+
+import org.osgi.framework.Filter;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation;
+
+/**
+ * The {@code @Service} annotation is to be used for test classes or methods annotated
+ * with the {@link OSGi @OSGi} annotation. Note that tests using this annotation are
+ * expected to be run within an OSGi environment. It is a repeatable annotation and can
+ * be used to specify a number of different services using the {@link #value() service
+ * type} and an optional {@link #filter() LDAP filter expression}.
+ * <br>
+ * Supported parameter types ar the service type itself for mandatory and unary references (1..1),
+ * a {@code Collection} or {@code List} of the service type for optional and multiple references (0..n).
+ * Currently no other cardinalities are supported.
+ * <br>
+ * When used on a test class, the specified services are made available for injection as parameters
+ * to all of the test's methods.
+ * <br>
+ * When used on a test method, the specified services are made available for injection as parameters
+ * to exactly that method.
+ * <br>
+ * When used on a method parameter, the specified service is made available for injection for exactly
+ * that parameter. In this case, the {@link #value() service type} need not be specified, it can
+ * be inferred from the parameter's type. However, it may still be useful to specify a filter expression.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
+@Repeatable(Services.class)
+@Inherited
+public @interface Service {
+
+ /**
+ * The type of the service to be injected.
+ * <br>
+ * May be omitted if the annotation is used to annotate a method parameter, as the service type can
+ * be inferred from the parameter's type.
+ */
+ Class<?> value() default Object.class;
+
+ /**
+ * An optional filter expression conforming to the LDAP filter syntax used in OSGi {@link Filter}s.
+ */
+ String filter() default "";
+}
+
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/Services.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/Services.java
new file mode 100644
index 0000000..477c12f
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/Services.java
@@ -0,0 +1,36 @@
+/*
+ * 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.junit.jupiter.osgi;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Utility annotation used to allow the {@link Service @Service} annotation to be repeatable.
+ * It is possible, but unnecessary, to use this annotation directly.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
+@Inherited
+public @interface Services {
+ Service[] value() default {};
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/AbstractTypeBasedParameterResolver.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/AbstractTypeBasedParameterResolver.java
new file mode 100644
index 0000000..c2817fe
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/AbstractTypeBasedParameterResolver.java
@@ -0,0 +1,66 @@
+/*
+ * 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.junit.jupiter.osgi.impl;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolver;
+
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.Map;
+
+import static org.apache.sling.junit.jupiter.osgi.impl.ReflectionHelper.determineTypeArguments;
+
+/**
+ * Abstract {@link ParameterResolver} class that resolves any type-arguments in the parameter's type
+ * to their actual type and provides this {@code resolvedParameterType} to the abstract methods
+ * {@link #supportsParameter(ParameterContext, ExtensionContext, Type)} and
+ * {@link #resolveParameter(ParameterContext, ExtensionContext, Type)}.
+ */
+public abstract class AbstractTypeBasedParameterResolver implements ParameterResolver {
+
+ protected abstract boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType);
+
+ protected abstract Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType);
+
+ @Override
+ public final boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
+ final Type type = getTypeOfParameter(parameterContext, extensionContext);
+ return supportsParameter(parameterContext, extensionContext, type);
+ }
+
+ @Override
+ public final Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
+ final Type typeOfParameter = getTypeOfParameter(parameterContext, extensionContext);
+ return resolveParameter(parameterContext, extensionContext, typeOfParameter);
+ }
+
+ @NotNull
+ private static Type getTypeOfParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
+ Type type = parameterContext.getParameter().getParameterizedType();
+ if (type instanceof TypeVariable) {
+ final Map<TypeVariable<?>, Type> typeVariableTypeMap = determineTypeArguments(extensionContext.getRequiredTestClass());
+ return typeVariableTypeMap.getOrDefault((TypeVariable<?>) type, type);
+ } else {
+ return type;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/BundleContextParameterResolver.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/BundleContextParameterResolver.java
new file mode 100644
index 0000000..a77b747
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/BundleContextParameterResolver.java
@@ -0,0 +1,33 @@
+/*
+ * 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.junit.jupiter.osgi.impl;
+
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+
+import java.lang.reflect.Type;
+
+public class BundleContextParameterResolver extends TypeBasedParameterResolver<BundleContext> {
+ @Override
+ protected BundleContext resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType) {
+ return FrameworkUtil.getBundle(extensionContext.getRequiredTestClass()).getBundleContext();
+ }
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/BundleParameterResolver.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/BundleParameterResolver.java
new file mode 100644
index 0000000..4bcc7e1
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/BundleParameterResolver.java
@@ -0,0 +1,33 @@
+/*
+ * 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.junit.jupiter.osgi.impl;
+
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.FrameworkUtil;
+
+import java.lang.reflect.Type;
+
+public class BundleParameterResolver extends TypeBasedParameterResolver<Bundle> {
+ @Override
+ protected Bundle resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType) {
+ return FrameworkUtil.getBundle(extensionContext.getRequiredTestClass());
+ }
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/ReflectionHelper.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/ReflectionHelper.java
new file mode 100644
index 0000000..8ed7fe9
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/ReflectionHelper.java
@@ -0,0 +1,103 @@
+/*
+ * 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.junit.jupiter.osgi.impl;
+
+import org.apache.commons.lang3.reflect.TypeUtils;
+import org.jetbrains.annotations.NotNull;
+import org.junit.platform.commons.util.Preconditions;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utility class used for resolving type-arguments to concrete types.
+ */
+public class ReflectionHelper {
+
+ public static Map<TypeVariable<?>, Type> determineTypeArguments(@NotNull Class<?> clazz) {
+ final Map<TypeVariable<?>, Type> typeVariableTypeMap = new HashMap<>();
+ determineTypeArguments(clazz, typeVariableTypeMap);
+ return Collections.unmodifiableMap(typeVariableTypeMap);
+ }
+
+ private static void determineTypeArguments(Class<?> clazz, Map<TypeVariable<?>, Type> typeVariableTypeMap) {
+ final Type genericSuperclass = clazz.getGenericSuperclass();
+ if (genericSuperclass instanceof ParameterizedType) {
+ final ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
+ typeVariableTypeMap.putAll(TypeUtils.determineTypeArguments(clazz, parameterizedType));
+
+ final Type rawType = parameterizedType.getRawType();
+ if (!(rawType instanceof Class<?>)) {
+ throw new UnsupportedOperationException("Expected Class#getGenericSuperclass() to return an object of type Class<?>");
+ }
+ determineTypeArguments((Class<?>) rawType, typeVariableTypeMap);
+ } else if (genericSuperclass instanceof Class<?>) {
+ if (genericSuperclass == Object.class && ((Class<?>) genericSuperclass).isArray()) {
+ // TODO: handle array types, see docs for Class#getGenericSuperclass()
+ throw new UnsupportedOperationException("Unsupported case where genericSuperclass == Object.class");
+ } else {
+ determineTypeArguments((Class<?>) genericSuperclass, typeVariableTypeMap);
+ }
+ } else if (genericSuperclass == null) {
+ final Type[] genericInterfaces = clazz.getGenericInterfaces();
+ for (Type genericInterface : genericInterfaces) {
+ if (genericInterface instanceof ParameterizedType) {
+ final ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
+ typeVariableTypeMap.putAll(TypeUtils.determineTypeArguments(clazz, parameterizedType));
+ }
+ }
+ } else {
+ throw new UnsupportedOperationException("Expected Class#getGenericSuperclass() to return null or an object of type Class<?> or ParameterizedType");
+ }
+ }
+
+
+ @NotNull
+ public static ParameterizedType parameterizedTypeForBaseClass(@NotNull Class<?> baseClass, @NotNull Class<?> clazz) {
+ ParameterizedType parameterizedType = findParameterizedTypeForBaseClass(baseClass, clazz);
+ Preconditions.notNull(parameterizedType,
+ () -> String.format(
+ "Failed to discover type supported by %s; "
+ + "potentially caused by lacking parameterized type in class declaration.",
+ clazz.getName()));
+ return parameterizedType;
+ }
+
+ private static ParameterizedType findParameterizedTypeForBaseClass(Class<?> baseClass, Class<?> clazz) {
+ Class<?> superclass = clazz.getSuperclass();
+
+ // Abort?
+ if (superclass == null || superclass == Object.class) {
+ return null;
+ }
+
+ Type genericSuperclass = clazz.getGenericSuperclass();
+ if (genericSuperclass instanceof ParameterizedType) {
+ Type rawType = ((ParameterizedType) genericSuperclass).getRawType();
+ if (rawType == baseClass) {
+ return ((ParameterizedType) genericSuperclass);
+ }
+ }
+ return findParameterizedTypeForBaseClass(baseClass, superclass);
+ }
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/ServiceParameterResolver.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/ServiceParameterResolver.java
new file mode 100644
index 0000000..05a2af2
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/ServiceParameterResolver.java
@@ -0,0 +1,263 @@
+/*
+ * 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.junit.jupiter.osgi.impl;
+
+import org.apache.sling.junit.jupiter.osgi.Service;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolutionException;
+import org.junit.platform.commons.support.AnnotationSupport;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation;
+
+public class ServiceParameterResolver extends AbstractTypeBasedParameterResolver {
+
+ private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(ServiceParameterResolver.class);
+
+ @Override
+ protected boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType) {
+ return computeServiceType(resolvedParameterType)
+ .flatMap(serviceType -> findServiceAnnotation(parameterContext, extensionContext, serviceType))
+ .isPresent();
+ }
+
+ @Override
+ protected Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType) {
+ return computeServiceType(resolvedParameterType)
+ .map(serviceType -> {
+ final Optional<Service> serviceAnnotation = findServiceAnnotation(parameterContext, extensionContext, serviceType);
+ return serviceAnnotation
+ .map(ann -> toKey(serviceType, ann))
+ .map(key -> extensionContext.getStore(NAMESPACE)
+ .getOrComputeIfAbsent(key, serviceHolderFactory(extensionContext, serviceType), ServiceHolder.class))
+ .map(serviceHolder -> isMultiple(resolvedParameterType) ? serviceHolder.getServices() : serviceHolder.getService())
+ .orElseThrow(() -> createServiceNotFoundException(serviceAnnotation.map(Service::filter).orElse(null), serviceType));
+ })
+ .orElseThrow(() -> new ParameterResolutionException("Cannot handle type " + resolvedParameterType));
+ }
+
+ private static ServiceHolder.Key toKey(Class<?> serviceType, Service serviceAnnotation) {
+ return new ServiceHolder.Key(serviceType, serviceAnnotation);
+ }
+
+ @NotNull
+ private static Optional<Class<?>> computeServiceType(Type resolvedParameterType) {
+ if (resolvedParameterType instanceof ParameterizedType) {
+ final ParameterizedType parameterizedType = (ParameterizedType) resolvedParameterType;
+ final Class<?> clazz = getRawClass(parameterizedType);
+ if (Collection.class == clazz || List.class.isAssignableFrom(clazz)) {
+ final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
+ if (actualTypeArguments.length == 1 && actualTypeArguments[0] instanceof Class<?>) {
+ return Optional.of((Class<?>) actualTypeArguments[0]);
+ }
+ }
+ } else if (resolvedParameterType instanceof Class<?>) {
+ return Optional.of((Class<?>) resolvedParameterType);
+ }
+ return Optional.empty();
+ }
+
+ @NotNull
+ private static Class<?> getRawClass(ParameterizedType parameterizedType) {
+ final Type rawType = parameterizedType.getRawType();
+ if (!(rawType instanceof Class<?>)) {
+ throw new UnsupportedOperationException("Unexpected raw type of parametereized type " + parameterizedType + ": " + rawType);
+ }
+ return (Class<?>) rawType;
+ }
+
+ private ParameterResolutionException createServiceNotFoundException(String ldapFilter, Type resolvedParameterType) {
+ return Optional.ofNullable(ldapFilter)
+ .map(String::trim)
+ .filter(filter -> !filter.isEmpty())
+ .map(filter -> new ParameterResolutionException("No service of type " + resolvedParameterType + " with filter \"" + filter + "\" available"))
+ .orElseGet(() -> new ParameterResolutionException("No service of type " + resolvedParameterType + " available"));
+ }
+
+ @NotNull
+ private static Function<ServiceHolder.Key, ServiceHolder> serviceHolderFactory(ExtensionContext extensionContext, Class<?> requiredServiceType) {
+ return key -> new ServiceHolder(getBundleContext(extensionContext), key);
+ }
+
+ @Nullable
+ private static BundleContext getBundleContext(ExtensionContext extensionContext) {
+ return Optional.ofNullable(FrameworkUtil.getBundle(extensionContext.getRequiredTestClass()))
+ .map(Bundle::getBundleContext)
+ .orElse(null);
+ }
+
+ @NotNull
+ private static Optional<Service> findServiceAnnotation(ParameterContext parameterContext, ExtensionContext extensionContext, Class<?> requiredServiceType) {
+ return Stream.<Supplier<Optional<Service>>>of(
+ () -> findServiceAnnotationOnParameter(parameterContext, requiredServiceType),
+ () -> findServiceAnnotationOnMethodOrClass(extensionContext, requiredServiceType))
+ .map(Supplier::get)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .findFirst();
+ }
+
+ private static Optional<Service> findServiceAnnotationOnMethodOrClass(ExtensionContext extensionContext, Class<?> requiredServiceType) {
+ return extensionContext.getElement()
+ .map(ae -> findMatchingServiceAnnotation(ae, requiredServiceType))
+ .filter(Optional::isPresent)
+ .orElseGet(() -> extensionContext.getParent().flatMap(p -> findServiceAnnotationOnMethodOrClass(p, requiredServiceType)));
+ }
+
+ private static Optional<Service> findServiceAnnotationOnParameter(ParameterContext parameterContext, Class<?> requiredServiceType) {
+ final Optional<Service> serviceAnnotation = findAnnotation(parameterContext.getParameter(), Service.class);
+ serviceAnnotation.ifPresent(ann -> {
+ if (!ann.value().isAssignableFrom(requiredServiceType)) {
+ throw new ParameterResolutionException("Mismatched types in annotation and parameter. " +
+ "Annotation type is \"" + ann.value().getSimpleName() + "\", parameter type is \"" + requiredServiceType.getSimpleName() + "\"");
+ }
+ });
+ return serviceAnnotation;
+ }
+
+ private static Optional<Service> findMatchingServiceAnnotation(AnnotatedElement annotatedElement, Class<?> requiredServiceType) {
+ return AnnotationSupport.findRepeatableAnnotations(annotatedElement, Service.class)
+ .stream()
+ .filter(serviceAnnotation -> Objects.equals(serviceAnnotation.value(), requiredServiceType))
+ .findFirst();
+ }
+
+ private boolean isMultiple(Type resolvedParameterType) {
+ if (resolvedParameterType instanceof ParameterizedType) {
+ final Class<?> type = getRawClass((ParameterizedType) resolvedParameterType);
+ return Collection.class == type || List.class.isAssignableFrom(type);
+ }
+ return false;
+ }
+
+ private static class ServiceHolder implements ExtensionContext.Store.CloseableResource {
+
+ private final ServiceTracker<?, ?> serviceTracker;
+
+ private ServiceHolder(BundleContext bundleContext, Key key) {
+ final Filter filter = createFilter(bundleContext, key.type(), key.filter());
+ serviceTracker = new SortingServiceTracker<>(bundleContext, filter);
+ serviceTracker.open();
+ }
+
+ @Override
+ public void close() throws Throwable {
+ serviceTracker.close();
+ }
+
+ public Object getService() {
+ return serviceTracker.getService();
+ }
+
+ public List<Object> getServices() {
+ final Object[] services = serviceTracker.getServices();
+ return services == null ? Collections.emptyList() : Arrays.asList(services);
+ }
+
+ private static Filter createFilter(BundleContext bundleContext, Class<?> clazz, String ldapFilter) {
+ final String classFilter = String.format("(%s=%s)", Constants.OBJECTCLASS, clazz.getName());
+ final String combinedFilter;
+ if (ldapFilter == null || ldapFilter.trim().isEmpty()) {
+ combinedFilter = classFilter;
+ } else {
+ combinedFilter = String.format("(&%s%s)", classFilter, ldapFilter);
+ }
+ try {
+ return bundleContext.createFilter(combinedFilter);
+ } catch (InvalidSyntaxException e) {
+ throw new IllegalArgumentException("Invalid filter expression: \"" + ldapFilter + "\"", e);
+ }
+ }
+
+ private static class SortingServiceTracker<T> extends ServiceTracker<T, T> {
+ public SortingServiceTracker(BundleContext bundleContext, Filter filter) {
+ super(bundleContext, filter, null);
+ }
+
+ @Override
+ public ServiceReference<T>[] getServiceReferences() {
+ return Optional.ofNullable(super.getServiceReferences())
+ .map(serviceReferences -> {
+ Arrays.sort(serviceReferences);
+ return serviceReferences;
+ })
+ .orElse(null);
+ }
+ }
+
+ private static class Key {
+
+ private final Class<?> serviceType;
+
+ private final Service serviceAnnotation;
+
+ public Key(Class<?> serviceType, Service serviceAnnotation) {
+ this.serviceType = serviceType;
+ this.serviceAnnotation = serviceAnnotation;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Key)) {
+ return false;
+ }
+ Key key = (Key) o;
+ return this == o
+ || (Objects.equals(serviceType, key.serviceType)
+ && Objects.equals(serviceAnnotation, key.serviceAnnotation));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(serviceType, serviceAnnotation);
+ }
+
+ public Class<?> type() {
+ return serviceType;
+ }
+
+ public String filter() {
+ return serviceAnnotation.filter();
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/TypeBasedParameterResolver.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/TypeBasedParameterResolver.java
new file mode 100644
index 0000000..a8ec0d2
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/TypeBasedParameterResolver.java
@@ -0,0 +1,53 @@
+/*
+ * 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.junit.jupiter.osgi.impl;
+
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+
+import static org.apache.sling.junit.jupiter.osgi.impl.ReflectionHelper.parameterizedTypeForBaseClass;
+
+/**
+ * Abstract implementation of a {@link org.junit.jupiter.api.extension.ParameterResolver} that resolves
+ * parameters of one given type. Implementations need only implement the abstract method
+ * {@link #resolveParameter(ParameterContext, ExtensionContext, Type)}, the supported parameter type is
+ * inferred from the classes type-argument {@code T}.
+ *
+ * @param <T>
+ */
+public abstract class TypeBasedParameterResolver<T> extends AbstractTypeBasedParameterResolver {
+
+ private final Type supportedType;
+
+ public TypeBasedParameterResolver() {
+ ParameterizedType parameterizedType = parameterizedTypeForBaseClass(TypeBasedParameterResolver.class, getClass());
+ this.supportedType = parameterizedType.getActualTypeArguments()[0];
+ }
+
+ @Override
+ protected boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType) {
+ return supportedType == resolvedParameterType;
+ }
+
+ @Override
+ protected abstract T resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType);
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/package-info.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/package-info.java
new file mode 100644
index 0000000..83d36bf
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+@Version("1.0.0")
+package org.apache.sling.junit.jupiter.osgi;
+
+import org.osgi.annotation.versioning.Version;
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/junit/impl/servlet/HtmlRendererTest.java b/src/test/java/org/apache/sling/junit/impl/servlet/HtmlRendererTest.java
index dd86d3d..c175746 100644
--- a/src/test/java/org/apache/sling/junit/impl/servlet/HtmlRendererTest.java
+++ b/src/test/java/org/apache/sling/junit/impl/servlet/HtmlRendererTest.java
@@ -18,7 +18,8 @@
*/
package org.apache.sling.junit.impl.servlet;
-import org.apache.sling.junit.impl.servlet.junit5.JUnit5TestExecutionStrategy;
+import org.apache.sling.junit.impl.servlet.junit5.JUnitPlatformHelper;
+import org.apache.sling.junit.impl.servlet.junit5.RunListenerAdapter;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Assume;
@@ -29,9 +30,6 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.platform.engine.TestEngine;
-import org.junit.platform.launcher.Launcher;
-import org.junit.platform.launcher.LauncherDiscoveryRequest;
-import org.junit.runner.notification.RunListener;
import org.junit.vintage.engine.VintageTestEngine;
import java.io.PrintWriter;
@@ -86,18 +84,10 @@ public class HtmlRendererTest {
final StringWriter out = new StringWriter();
final HtmlRenderer htmlRenderer = new HtmlRenderer();
htmlRenderer.setWriter(new PrintWriter(out));
- runTest(testEngine, htmlRenderer, testClass, methodName);
+ JUnitPlatformHelper.executeTest(testEngine, testClass, methodName, new RunListenerAdapter(htmlRenderer));
return out.toString();
}
- private static void runTest(TestEngine testEngine, RunListener runListener, Class<?> testClass, String methodName) {
- final Launcher launcher = JUnit5TestExecutionStrategy.createLauncher(runListener, testEngine);
- final LauncherDiscoveryRequest request = methodName != null
- ? JUnit5TestExecutionStrategy.methodRequest(testClass, methodName)
- : JUnit5TestExecutionStrategy.classesRequest(testClass);
- launcher.execute(request);
- }
-
public static class ExampleTestCases {
public static final String ASSUMPTION_IS_ALWAYS_INVALID = "Assumption is always invalid";
diff --git a/src/test/java/org/apache/sling/junit/jupiter/osgi/OSGiAnnotationTest.java b/src/test/java/org/apache/sling/junit/jupiter/osgi/OSGiAnnotationTest.java
new file mode 100644
index 0000000..e315622
--- /dev/null
+++ b/src/test/java/org/apache/sling/junit/jupiter/osgi/OSGiAnnotationTest.java
@@ -0,0 +1,392 @@
+/*
+ * 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.junit.jupiter.osgi;
+
+import org.apache.sling.junit.impl.servlet.junit5.JUnitPlatformHelper;
+import org.apache.sling.testing.mock.osgi.junit5.OsgiContext;
+import org.apache.sling.testing.mock.osgi.junit5.OsgiContextExtension;
+import org.hamcrest.Matchers;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ParameterResolutionException;
+import org.junit.jupiter.engine.JupiterTestEngine;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.platform.commons.annotation.Testable;
+import org.junit.platform.commons.support.HierarchyTraversalMode;
+import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.opentest4j.MultipleFailuresError;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.util.Arrays.asList;
+import static org.apache.sling.junit.jupiter.osgi.impl.ReflectionHelper.parameterizedTypeForBaseClass;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedMethods;
+
+/**
+ * This test executes pseudo test classes using the {@link JupiterTestEngine} in order to
+ * verify the correct injection of parameters via the {@code @OSGi} and {@code @Service}
+ * annotations.
+ * <br>
+ * In order to achieve this, test methods from the pseudo test classes ({@code PseudoTest*}
+ * are executed and the test summary evaluated to verify expectations. The additional indirection
+ * can be a little tricky, but is necessary to test the annotations work correctly. Particularly
+ * when testing failure scenarios, where the failure of a pseudo test is required to pass the actual
+ * test.
+ */
+@ExtendWith(OsgiContextExtension.class)
+public class OSGiAnnotationTest {
+
+ private static final JupiterTestEngine JUPITER_TEST_ENGINE = new JupiterTestEngine();
+
+ OsgiContext osgiContext = new OsgiContext();
+
+ @SuppressWarnings("unused") // provides parameters
+ static Stream<Arguments> frameworkObjectsInjectionTests() {
+ return Stream.of(PseudoTestBundleInjection.class, PseudoTestBundleContextInjection.class)
+ .flatMap(OSGiAnnotationTest::allTestMethods);
+ }
+
+ @ParameterizedTest(name = "{0}#{2}")
+ @MethodSource("frameworkObjectsInjectionTests")
+ void injectFrameworkObjects(String name, Class<?> testClass, String testMethodName) {
+ withMockedFrameworkUtil(() -> {
+ assertNoFailures(testClass, testMethodName);
+ });
+ }
+
+ @SuppressWarnings("unused") // provides parameters
+ static Stream<Arguments> serviceInjectionTests() {
+ return Stream
+ .of(
+ PseudoTestServiceInjectionGloballyAnnotated.class,
+ PseudoTestServiceInjectionGloballyAnnotatedWithFilter.class,
+ PseudoTestInheritedServiceInjectionGloballyAnnotated.class)
+ .flatMap(OSGiAnnotationTest::allTestMethods);
+ }
+
+ @ParameterizedTest(name = "{0}#{2}")
+ @MethodSource("serviceInjectionTests")
+ void injectServices(String name, Class<?> testClass, String testMethodName) {
+ osgiContext.registerService(ServiceInterface.class, new ServiceA(), "foo", "quz");
+ withMockedFrameworkUtil(() -> {
+ assertNoFailures(testClass, testMethodName);
+ });
+ }
+
+ @SuppressWarnings("unused") // provides parameters
+ static Stream<Arguments> failConstructionDueToMissingServiceInjectionTests() {
+ return Stream.of(PseudoTestServiceInjectionNotAnnotated.class, PseudoTestServiceInjectionGloballyAnnotatedWithFilter.class)
+ .flatMap(namedMethods("injectedConstructorParameter"));
+ }
+
+ @ParameterizedTest(name = "{0}#{2}")
+ @MethodSource("failConstructionDueToMissingServiceInjectionTests")
+ void failConstructionDueToMissingServiceInjection(String name, Class<?> testClass, String testMethodName) {
+ // setup service with non-matching filter
+ osgiContext.registerService(ServiceInterface.class, new ServiceA(), "foo", "no match");
+ withMockedFrameworkUtil(() -> {
+ assertTestConstructionFailsDueToMissingService(testClass, testMethodName);
+ });
+ }
+
+ @Test
+ void injectServiceAsAnnotatedMethodParameterWithExplicitClass() {
+ osgiContext.registerService(ServiceInterface.class, new ServiceA());
+ withMockedFrameworkUtil(() -> {
+ assertNoFailures(PseudoTestServiceMethodInjection.class, "annotatedParameterWithExplicitClass");
+ });
+ }
+
+ @Test
+ void injectServiceAsAnnotatedMethodParameterWithImplicitClass() {
+ osgiContext.registerService(ServiceInterface.class, new ServiceA());
+ withMockedFrameworkUtil(() -> {
+ assertNoFailures(PseudoTestServiceMethodInjection.class, "annotatedParameterWithImpliedClass");
+ });
+ }
+
+ @Test
+ void injectServiceAsAnnotatedMethodParameterWithExplicitClassMultiple() {
+ osgiContext.registerService(ServiceInterface.class, new ServiceA(), "service.ranking", 1);
+ osgiContext.registerService(ServiceInterface.class, new ServiceC(), "service.ranking", 3);
+ osgiContext.registerService(ServiceInterface.class, new ServiceB(), "service.ranking", 2);
+ withMockedFrameworkUtil(() -> {
+ assertNoFailures(PseudoTestServiceMethodInjection.class, "annotatedParameterWithExplicitClassMultiple");
+ });
+ }
+
+ @Test
+ void injectServiceAsAnnotatedMethodParameterWithImplicitClassMultiple() {
+ osgiContext.registerService(ServiceInterface.class, new ServiceA(), "service.ranking", 1);
+ osgiContext.registerService(ServiceInterface.class, new ServiceC(), "service.ranking", 3);
+ osgiContext.registerService(ServiceInterface.class, new ServiceB(), "service.ranking", 2);
+ withMockedFrameworkUtil(() -> {
+ assertNoFailures(PseudoTestServiceMethodInjection.class, "annotatedParameterWithImpliedClassMultiple");
+ });
+ }
+
+
+ @Test
+ void injectServiceAsAnnotatedMethodParameterWithImplicitClassEmptyMultiple() {
+ withMockedFrameworkUtil(() -> {
+ assertNoFailures(PseudoTestServiceMethodInjection.class, "annotatedParameterWithImpliedClassEmptyMultiple");
+ });
+ }
+
+ @Test
+ void injectServiceAsAnnotatedMethodParameterWithIncorrectExplicitClassMultiple() {
+ osgiContext.registerService(ServiceInterface.class, new ServiceA(), "service.ranking", 1);
+ osgiContext.registerService(ServiceInterface.class, new ServiceC(), "service.ranking", 3);
+ osgiContext.registerService(ServiceInterface.class, new ServiceB(), "service.ranking", 2);
+ withMockedFrameworkUtil(() -> {
+ final TestExecutionSummary summary = executeAndSummarize(PseudoTestServiceMethodInjection.class, "annotatedParameterWithIncorrectExplicitClassMultiple");
+ assertEquals(1, summary.getTestsFailedCount(), "expected test failure count");
+ final Throwable exception = summary.getFailures().get(0).getException();
+ assertThat(exception, instanceOf(ParameterResolutionException.class));
+ assertThat(exception.getMessage(), equalTo("Mismatched types in annotation and parameter. " +
+ "Annotation type is \"ServiceB\", parameter type is \"ServiceInterface\""));
+ });
+ }
+
+ @Test
+ void injectServiceAsParameterOfAnnotatedMethod() {
+ osgiContext.registerService(ServiceInterface.class, new ServiceA());
+ withMockedFrameworkUtil(() -> {
+ assertNoFailures(PseudoTestServiceMethodInjection.class, "annotatedMethod");
+ });
+ }
+
+ @OSGi
+ static class PseudoTestServiceMethodInjection {
+ @Test
+ void annotatedParameterWithExplicitClass(@Service(ServiceInterface.class) ServiceInterface serviceA) {
+ assertThat(serviceA, instanceOf(ServiceA.class));
+ }
+
+ @Test
+ void annotatedParameterWithImpliedClass(@Service ServiceInterface serviceA) {
+ assertThat(serviceA, instanceOf(ServiceA.class));
+ }
+
+ @Test
+ void annotatedParameterWithExplicitClassMultiple(@Service(ServiceInterface.class) List<ServiceInterface> services) {
+ assertThat(services, instanceOf(List.class));
+ assertThat(services, contains(asList(instanceOf(ServiceA.class), instanceOf(ServiceB.class), instanceOf(ServiceC.class))));
+ }
+
+ @Test
+ void annotatedParameterWithImpliedClassMultiple(@Service Collection<ServiceInterface> services) {
+ assertThat(services, instanceOf(Collection.class));
+ assertThat(services, contains(asList(instanceOf(ServiceA.class), instanceOf(ServiceB.class), instanceOf(ServiceC.class))));
+ }
+
+ @Test
+ void annotatedParameterWithImpliedClassEmptyMultiple(@Service List<ServiceInterface> services) {
+ assertThat(services, instanceOf(List.class));
+ assertThat(services, empty());
+ }
+
+ @Test
+ void annotatedParameterWithIncorrectExplicitClassMultiple(@Service(ServiceB.class) List<ServiceInterface> services) {
+ assertThat(services, instanceOf(List.class));
+ assertThat(services, contains(instanceOf(ServiceA.class)));
+ }
+
+ @Test
+ @Service(ServiceInterface.class)
+ void annotatedMethod(ServiceInterface serviceA) {
+ assertThat(serviceA, instanceOf(ServiceA.class));
+ }
+ }
+
+ private void withMockedFrameworkUtil(Runnable callback) {
+ try (final MockedStatic<FrameworkUtil> frameworkUtilMock = Mockito.mockStatic(FrameworkUtil.class)) {
+ frameworkUtilMock
+ .when(() -> FrameworkUtil.getBundle(Mockito.any()))
+ .then(invocation -> osgiContext.bundleContext().getBundle());
+ callback.run();
+ }
+ }
+
+ @NotNull
+ private static Stream<Arguments> allTestMethods(Class<?> cls) {
+ return findAnnotatedMethods(cls, Testable.class, HierarchyTraversalMode.BOTTOM_UP).stream()
+ .map(toArguments(cls));
+ }
+
+ @NotNull
+ private static Function<Class<?>, Stream<Arguments>> namedMethods(String... testMethodNames) {
+ return cls -> findAnnotatedMethods(cls, Testable.class, HierarchyTraversalMode.BOTTOM_UP).stream()
+ .filter(method -> asList(testMethodNames).contains(method.getName()))
+ .map(toArguments(cls));
+ }
+
+ @NotNull
+ private static Function<Method, Arguments> toArguments(Class<?> cls) {
+ return method -> Arguments.of(cls.getSimpleName(), cls, method.getName());
+ }
+
+ private static TestExecutionSummary executeAndSummarize(@NotNull Class<?> testClass, @Nullable String testMethodName) {
+ final SummaryGeneratingListener listener = new SummaryGeneratingListener();
+ JUnitPlatformHelper.executeTest(JUPITER_TEST_ENGINE, testClass, testMethodName, listener);
+ return listener.getSummary();
+ }
+
+ private static void assertNoFailures(@NotNull Class<?> testClass, @Nullable String testMethodName) {
+ final TestExecutionSummary summary = executeAndSummarize(testClass, testMethodName);
+ assertThat("number of tests found", (int) summary.getTestsFoundCount(), greaterThan(0));
+ final List<TestExecutionSummary.Failure> failures = summary.getFailures();
+ switch (failures.size()) {
+ case 0:
+ break;
+ case 1:
+ fail(failures.get(0).getException());
+ default:
+ throw new MultipleFailuresError(null, failures.stream().map(TestExecutionSummary.Failure::getException).collect(Collectors.toList()));
+ }
+ }
+
+ private void assertTestConstructionFailsDueToMissingService(Class<?> testClass, String testMethodName) {
+ final TestExecutionSummary summary = executeAndSummarize(testClass, testMethodName);
+ final List<TestExecutionSummary.Failure> failures = summary.getFailures();
+ assertEquals(1, failures.size(), "number of test failures");
+ final TestExecutionSummary.Failure failure = failures.get(0);
+ final Throwable exception = failure.getException();
+ assertThat(exception, Matchers.instanceOf(ParameterResolutionException.class));
+ assertThat(exception.getMessage(), anyOf(
+ allOf(containsString("No ParameterResolver registered for parameter "), containsString(" in constructor ")),
+ // allOf(containsString("Failed to resolve parameter "), containsString(" in constructor ")),
+ allOf(containsString("No service of type "), containsString(" available"))
+ ));
+ }
+
+ @OSGi
+ static class PseudoTestBundleContextInjection extends Injection<BundleContext> {
+ public PseudoTestBundleContextInjection(BundleContext object) {
+ super(object);
+ }
+ }
+
+ @OSGi
+ static class PseudoTestBundleInjection extends Injection<Bundle> {
+ public PseudoTestBundleInjection(Bundle object) {
+ super(object);
+ }
+ }
+
+ @OSGi
+ static class PseudoTestServiceInjectionNotAnnotated extends Injection<ServiceInterface> {
+ public PseudoTestServiceInjectionNotAnnotated(ServiceInterface object) {
+ super(object);
+ }
+
+ @Override
+ void injectedMethodParameter(ServiceInterface objectFromMethodInjection) {
+ super.injectedMethodParameter(objectFromMethodInjection);
+ }
+ }
+
+ @OSGi
+ @Service(ServiceInterface.class)
+ static class PseudoTestServiceInjectionGloballyAnnotated extends Injection<ServiceInterface> {
+ public PseudoTestServiceInjectionGloballyAnnotated(ServiceInterface object) {
+ super(object);
+ }
+ }
+
+ static class PseudoTestInheritedServiceInjectionGloballyAnnotated extends PseudoTestServiceInjectionGloballyAnnotated {
+ public PseudoTestInheritedServiceInjectionGloballyAnnotated(ServiceInterface object) {
+ super(object);
+ }
+ }
+
+ @OSGi
+ @Service(value = ServiceInterface.class, filter = "(foo=quz)")
+ static class PseudoTestServiceInjectionGloballyAnnotatedWithFilter extends Injection<ServiceInterface> {
+ public PseudoTestServiceInjectionGloballyAnnotatedWithFilter(ServiceInterface object) {
+ super(object);
+ }
+ }
+
+ static abstract class Injection<T> {
+
+ T objectFromConstructor;
+
+ private final String typeName;
+
+ public Injection(T object) {
+ this.objectFromConstructor = object;
+ final ParameterizedType parameterizedType = parameterizedTypeForBaseClass(Injection.class, getClass());
+ this.typeName = ((Class<?>) parameterizedType.getActualTypeArguments()[0]).getSimpleName();
+ }
+
+ @Test
+ final void injectedConstructorParameter() {
+ assertNotNull(objectFromConstructor, typeName + " constructor parameter");
+ }
+
+ @Test
+ void injectedMethodParameter(T objectFromMethodInjection) {
+ assertNotNull(objectFromMethodInjection, typeName + " method parameter");
+ assertSame(objectFromConstructor, objectFromMethodInjection,
+ typeName + " same parameter should be injected into method and constructor");
+ }
+ }
+
+ interface ServiceInterface {
+ }
+
+ static class ServiceA implements ServiceInterface {
+ }
+
+ static class ServiceB implements ServiceInterface {
+ }
+
+ static class ServiceC implements ServiceInterface {
+ }
+}