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/23 14:55:31 UTC
[sling-org-apache-sling-junit-core] 01/01: @Service annotation for
Jupiter should explicitly allow for optional and mandatory service
injection
This is an automated email from the ASF dual-hosted git repository.
jsedding pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-junit-core.git
commit 05eeeb42319eace3d8d7d111a9b60ef2b467b2e5
Author: Julian Sedding <js...@apache.org>
AuthorDate: Wed Jun 23 16:37:31 2021 +0200
@Service annotation for Jupiter should explicitly allow for optional and mandatory service injection
- added @Service#cardinality() attribute allowing AUTO, OPTIONAL
and MANDATORY injection
- refactored and enhanced tests
---
.../osgi/BundleContextParameterResolver.java | 10 +-
.../jupiter/osgi/BundleParameterResolver.java | 8 +-
.../apache/sling/junit/jupiter/osgi/Service.java | 23 +-
.../junit/jupiter/osgi/ServiceCardinality.java | 58 ++++
.../jupiter/osgi/ServiceParameterResolver.java | 183 ++++++-----
.../impl/AbstractTypeBasedParameterResolver.java | 4 +-
.../osgi/impl/TypeBasedParameterResolver.java | 5 +-
.../sling/junit/jupiter/osgi/package-info.java | 2 +-
.../junit/jupiter/osgi/OSGiAnnotationTest.java | 357 +++++++++++++++------
.../junit/jupiter/osgi/utils/MemberMatcher.java | 75 +++++
.../sling/junit/jupiter/osgi/utils/MetaTest.java | 89 +++++
11 files changed, 633 insertions(+), 181 deletions(-)
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/BundleContextParameterResolver.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/BundleContextParameterResolver.java
index 13b5f6d..f6e6914 100644
--- a/src/main/java/org/apache/sling/junit/jupiter/osgi/BundleContextParameterResolver.java
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/BundleContextParameterResolver.java
@@ -19,16 +19,22 @@
package org.apache.sling.junit.jupiter.osgi;
import org.apache.sling.junit.jupiter.osgi.impl.TypeBasedParameterResolver;
+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.ParameterResolutionException;
+import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import java.lang.reflect.Type;
+import java.util.Optional;
class BundleContextParameterResolver extends TypeBasedParameterResolver<BundleContext> {
@Override
- protected BundleContext resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType) {
- return FrameworkUtil.getBundle(extensionContext.getRequiredTestClass()).getBundleContext();
+ protected BundleContext resolveParameter(@NotNull ParameterContext parameterContext, @NotNull ExtensionContext extensionContext, @NotNull Type resolvedParameterType) {
+ return Optional.ofNullable(FrameworkUtil.getBundle(extensionContext.getRequiredTestClass()))
+ .map(Bundle::getBundleContext)
+ .orElseThrow(() -> new ParameterResolutionException("@OSGi and @Service annotations can only be used with tests running in an OSGi environment"));
}
}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/BundleParameterResolver.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/BundleParameterResolver.java
index cde68f3..cd065d4 100644
--- a/src/main/java/org/apache/sling/junit/jupiter/osgi/BundleParameterResolver.java
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/BundleParameterResolver.java
@@ -19,16 +19,20 @@
package org.apache.sling.junit.jupiter.osgi;
import org.apache.sling.junit.jupiter.osgi.impl.TypeBasedParameterResolver;
+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.ParameterResolutionException;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import java.lang.reflect.Type;
+import java.util.Optional;
class BundleParameterResolver extends TypeBasedParameterResolver<Bundle> {
@Override
- protected Bundle resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType) {
- return FrameworkUtil.getBundle(extensionContext.getRequiredTestClass());
+ protected Bundle resolveParameter(@NotNull ParameterContext parameterContext, @NotNull ExtensionContext extensionContext, @NotNull Type resolvedParameterType) {
+ return Optional.ofNullable(FrameworkUtil.getBundle(extensionContext.getRequiredTestClass()))
+ .orElseThrow(() -> new ParameterResolutionException("@OSGi and @Service annotations can only be used with tests running in an OSGi environment"));
}
}
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
index 3384a37..9705197 100644
--- a/src/main/java/org/apache/sling/junit/jupiter/osgi/Service.java
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/Service.java
@@ -18,6 +18,8 @@
*/
package org.apache.sling.junit.jupiter.osgi;
+import org.osgi.annotation.versioning.ConsumerType;
+import org.osgi.annotation.versioning.ProviderType;
import org.osgi.framework.Filter;
import java.lang.annotation.ElementType;
@@ -34,9 +36,13 @@ import java.lang.annotation.Target;
* 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.
+ * Supported parameter types are the service type itself for unary references (1..1),
+ * a {@link java.util.Collection} or {@link java.util.List} of the service type for optional
+ * and multiple references (0..n).
+ * <br>
+ * Additionally it is possible to refine the cardinality by specifying the {@code cardinality}
+ * attribute in order to require or not the presence of at least one service instance, thus
+ * achieving cardinalities (0..1) and (1..n).
* <br>
* When used on a test class, the specified services are made available for injection as parameters
* to all of the test's methods.
@@ -47,9 +53,11 @@ import java.lang.annotation.Target;
* 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.
+ *
+ * @see ServiceCardinality
*/
@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
+@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER})
@Repeatable(Services.class)
@Inherited
public @interface Service {
@@ -66,5 +74,12 @@ public @interface Service {
* An optional filter expression conforming to the LDAP filter syntax used in OSGi {@link Filter}s.
*/
String filter() default "";
+
+ /**
+ * Whether or not injection fails in the absence of a suitable service.
+ *
+ * @see ServiceCardinality
+ */
+ ServiceCardinality cardinality() default ServiceCardinality.AUTO;
}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/ServiceCardinality.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/ServiceCardinality.java
new file mode 100644
index 0000000..a15a479
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/ServiceCardinality.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+/**
+ * The cardinality of a service being injected is controlled via the type of the injected
+ * parameter and additionally via the {@link @Service} annotation's {@code cardinality}
+ * attribute.
+ * <br>
+ * The cardinality can be either {@code OPTIONAL} or {@code MANDATORY}. {@code OPTIONAL}
+ * does not require the presence of a service, whereas {@code MANDATORY} requires at
+ * least one service to be present, otherwise an exception is thrown.
+ * <br>
+ * The other aspect of cardinality, namely whether a single service or multiple services
+ * should be injected, is controlled via the type of the annotated field. For single service
+ * injection, the field's type is expected to be the type of the injected service. For multiple
+ * service injection, the field's type is expected to be a {@link java.util.Collection} or
+ * {@link java.util.List} with the type of the injected service as its generic type-argument.
+ * <br>
+ * Any further constraints, i.e. checking for an exact number or a range of services must be
+ * done via assertions.
+ *
+ * @see Service#cardinality()
+ */
+public enum ServiceCardinality {
+ /**
+ * For unary service injection, {@code AUTO} defaults to {@code MANDATORY}.
+ * Whereas for multiple service injection, {@code AUTO} defaults to {@code OPTIONAL}.
+ */
+ AUTO,
+
+ /**
+ * If no service is present, {@code null} is injected for unary service injection, and an empty
+ * {@code List} is injected for multiple service injection.
+ */
+ OPTIONAL,
+
+ /**
+ * At least one service must be present, otherwise a {@code ParameterResolutionException} is thrown.
+ */
+ MANDATORY
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/ServiceParameterResolver.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/ServiceParameterResolver.java
index f2addda..f7c0ab8 100644
--- a/src/main/java/org/apache/sling/junit/jupiter/osgi/ServiceParameterResolver.java
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/ServiceParameterResolver.java
@@ -45,35 +45,29 @@ 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;
-
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))
+ protected boolean supportsParameter(@NotNull ParameterContext parameterContext, @NotNull ExtensionContext extensionContext, @NotNull Type resolvedParameterType) {
+ final Optional<Service> service = computeServiceType(resolvedParameterType)
+ .flatMap(serviceType -> findServiceAnnotation(parameterContext, extensionContext, serviceType));
+ return service
.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));
- })
+ protected Object resolveParameter(@NotNull ParameterContext parameterContext, @NotNull ExtensionContext extensionContext, @NotNull Type resolvedParameterType) {
+ final ServiceHolder.Key key = computeServiceType(resolvedParameterType)
+ .flatMap(serviceType -> findServiceAnnotation(parameterContext, extensionContext, serviceType)
+ .map(ann -> toKey(serviceType, ann)))
.orElseThrow(() -> new ParameterResolutionException("Cannot handle type " + resolvedParameterType));
+
+ final ServiceHolder serviceHolder = extensionContext.getStore(NAMESPACE).getOrComputeIfAbsent(key, serviceHolderFactory(extensionContext), ServiceHolder.class);
+ return isMultiple(resolvedParameterType) ? serviceHolder.getServices() : serviceHolder.getService();
}
private static ServiceHolder.Key toKey(Class<?> serviceType, Service serviceAnnotation) {
@@ -81,11 +75,10 @@ class ServiceParameterResolver extends AbstractTypeBasedParameterResolver {
}
@NotNull
- private static Optional<Class<?>> computeServiceType(Type resolvedParameterType) {
+ private static Optional<Class<?>> computeServiceType(@NotNull Type resolvedParameterType) {
if (resolvedParameterType instanceof ParameterizedType) {
final ParameterizedType parameterizedType = (ParameterizedType) resolvedParameterType;
- final Class<?> clazz = getRawClass(parameterizedType);
- if (Collection.class == clazz || List.class.isAssignableFrom(clazz)) {
+ if (isMultiple(parameterizedType)) {
final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
if (actualTypeArguments.length == 1 && actualTypeArguments[0] instanceof Class<?>) {
return Optional.of((Class<?>) actualTypeArguments[0]);
@@ -106,63 +99,55 @@ class ServiceParameterResolver extends AbstractTypeBasedParameterResolver {
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) {
+ private static Function<ServiceHolder.Key, ServiceHolder> serviceHolderFactory(ExtensionContext extensionContext) {
return key -> new ServiceHolder(getBundleContext(extensionContext), key);
}
- @Nullable
- private static BundleContext getBundleContext(ExtensionContext extensionContext) {
+ @NotNull
+ private static BundleContext getBundleContext(@NotNull ExtensionContext extensionContext) {
return Optional.ofNullable(FrameworkUtil.getBundle(extensionContext.getRequiredTestClass()))
.map(Bundle::getBundleContext)
- .orElse(null);
+ .orElseThrow(() -> new ParameterResolutionException("@OSGi and @Service annotations can only be used with tests running in an OSGi environment"));
}
@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)
+ private static Optional<Service> findServiceAnnotation(@NotNull ParameterContext parameterContext, @NotNull ExtensionContext extensionContext, @NotNull Class<?> requiredServiceType) {
+ return Stream.concat(
+ Stream.of(findMatchingServiceAnnotationOnParameter(parameterContext, requiredServiceType)),
+ Stream.of(parameterContext.getDeclaringExecutable(), extensionContext.getRequiredTestClass())
+ .map(annotatedElement -> findMatchingServiceAnnotation(annotatedElement, requiredServiceType)))
+ .filter(Objects::nonNull)
.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 Service findMatchingServiceAnnotationOnParameter(@NotNull ParameterContext parameterContext, @NotNull Class<?> requiredServiceType) {
+ final List<Service> serviceAnnotations = parameterContext.findRepeatableAnnotations(Service.class);
+ switch (serviceAnnotations.size()) {
+ case 0:
+ return null;
+ case 1:
+ final Service serviceAnnotation = serviceAnnotations.get(0);
+ if (!serviceAnnotation.value().isAssignableFrom(requiredServiceType)) {
+ throw new ParameterResolutionException("Mismatched types in annotation and parameter. " +
+ "Annotation type is \"" + serviceAnnotation.value().getSimpleName() + "\", parameter type is \"" + requiredServiceType.getSimpleName() + "\"");
+ }
+ return serviceAnnotation;
+ default:
+ throw new ParameterResolutionException("Parameters must not be annotated with multiple @Service annotations: " + parameterContext.getDeclaringExecutable());
+ }
}
- private static Optional<Service> findMatchingServiceAnnotation(AnnotatedElement annotatedElement, Class<?> requiredServiceType) {
+ @Nullable
+ private static Service findMatchingServiceAnnotation(@Nullable AnnotatedElement annotatedElement, @NotNull Class<?> requiredServiceType) {
return AnnotationSupport.findRepeatableAnnotations(annotatedElement, Service.class)
.stream()
.filter(serviceAnnotation -> Objects.equals(serviceAnnotation.value(), requiredServiceType))
- .findFirst();
+ .findFirst()
+ .orElse(null);
}
- private boolean isMultiple(Type resolvedParameterType) {
+ private static boolean isMultiple(@NotNull Type resolvedParameterType) {
if (resolvedParameterType instanceof ParameterizedType) {
final Class<?> type = getRawClass((ParameterizedType) resolvedParameterType);
return Collection.class == type || List.class.isAssignableFrom(type);
@@ -172,9 +157,12 @@ class ServiceParameterResolver extends AbstractTypeBasedParameterResolver {
private static class ServiceHolder implements ExtensionContext.Store.CloseableResource {
+ private final Key key;
+
private final ServiceTracker<?, ?> serviceTracker;
- private ServiceHolder(BundleContext bundleContext, Key key) {
+ private ServiceHolder(@NotNull BundleContext bundleContext, @NotNull Key key) {
+ this.key = key;
final Filter filter = createFilter(bundleContext, key.type(), key.filter());
serviceTracker = new SortingServiceTracker<>(bundleContext, filter);
serviceTracker.open();
@@ -185,19 +173,52 @@ class ServiceParameterResolver extends AbstractTypeBasedParameterResolver {
serviceTracker.close();
}
- public Object getService() {
- return serviceTracker.getService();
+ @Nullable
+ public Object getService() throws ParameterResolutionException{
+ final Object service = serviceTracker.getService();
+ return checkCardinality(service, false);
+ }
+
+ @NotNull
+ public List<Object> getServices() throws ParameterResolutionException {
+ @Nullable final Object[] services = serviceTracker.getServices();;
+ return Optional.ofNullable(checkCardinality(services, true))
+ .map(Arrays::asList)
+ .orElseGet(Collections::emptyList);
+ }
+
+ @Nullable
+ private <T> T checkCardinality(@Nullable T service, boolean isMultiple) throws ParameterResolutionException {
+ final ServiceCardinality effectiveCardinality = calculateEffectiveCardinality(isMultiple);
+ if (service == null && effectiveCardinality == ServiceCardinality.MANDATORY) {
+ throw createServiceNotFoundException(key.filter(), key.type());
+ }
+ return service;
+ }
+
+ @NotNull
+ private ServiceCardinality calculateEffectiveCardinality(boolean isMultiple) {
+ final ServiceCardinality cardinality = key.cardinality();
+ if (cardinality == ServiceCardinality.AUTO) {
+ return isMultiple ? ServiceCardinality.OPTIONAL : ServiceCardinality.MANDATORY;
+ }
+ return cardinality;
}
- public List<Object> getServices() {
- final Object[] services = serviceTracker.getServices();
- return services == null ? Collections.emptyList() : Arrays.asList(services);
+ @NotNull
+ private static ParameterResolutionException createServiceNotFoundException(@NotNull String ldapFilter, @NotNull Type resolvedParameterType) {
+ return Optional.of(ldapFilter)
+ .map(String::trim)
+ .filter(filter -> !filter.isEmpty())
+ .map(filter -> new ParameterResolutionException("No service of type \"" + resolvedParameterType.getTypeName() + "\" with filter \"" + filter + "\" available"))
+ .orElseGet(() -> new ParameterResolutionException("No service of type \"" + resolvedParameterType.getTypeName() + "\" available"));
}
- private static Filter createFilter(BundleContext bundleContext, Class<?> clazz, String ldapFilter) {
+ @NotNull
+ private static Filter createFilter(@NotNull BundleContext bundleContext, @NotNull Class<?> clazz, @NotNull String ldapFilter) {
final String classFilter = String.format("(%s=%s)", Constants.OBJECTCLASS, clazz.getName());
final String combinedFilter;
- if (ldapFilter == null || ldapFilter.trim().isEmpty()) {
+ if (ldapFilter.trim().isEmpty()) {
combinedFilter = classFilter;
} else {
combinedFilter = String.format("(&%s%s)", classFilter, ldapFilter);
@@ -205,16 +226,17 @@ class ServiceParameterResolver extends AbstractTypeBasedParameterResolver {
try {
return bundleContext.createFilter(combinedFilter);
} catch (InvalidSyntaxException e) {
- throw new IllegalArgumentException("Invalid filter expression: \"" + ldapFilter + "\"", e);
+ throw new ParameterResolutionException("Invalid filter expression used in @Service annotation :\"" + ldapFilter + "\"", e);
}
}
private static class SortingServiceTracker<T> extends ServiceTracker<T, T> {
- public SortingServiceTracker(BundleContext bundleContext, Filter filter) {
+ public SortingServiceTracker(@NotNull BundleContext bundleContext, @NotNull Filter filter) {
super(bundleContext, filter, null);
}
@Override
+ @Nullable
public ServiceReference<T>[] getServiceReferences() {
return Optional.ofNullable(super.getServiceReferences())
.map(serviceReferences -> {
@@ -231,11 +253,26 @@ class ServiceParameterResolver extends AbstractTypeBasedParameterResolver {
private final Service serviceAnnotation;
- public Key(Class<?> serviceType, Service serviceAnnotation) {
+ public Key(@NotNull Class<?> serviceType, @NotNull Service serviceAnnotation) {
this.serviceType = serviceType;
this.serviceAnnotation = serviceAnnotation;
}
+ @NotNull
+ public Class<?> type() {
+ return serviceType;
+ }
+
+ @NotNull
+ public String filter() {
+ return serviceAnnotation.filter();
+ }
+
+ @NotNull
+ public ServiceCardinality cardinality() {
+ return serviceAnnotation.cardinality();
+ }
+
@Override
public boolean equals(Object o) {
if (!(o instanceof Key)) {
@@ -251,14 +288,6 @@ class ServiceParameterResolver extends AbstractTypeBasedParameterResolver {
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/AbstractTypeBasedParameterResolver.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/AbstractTypeBasedParameterResolver.java
index 58697fa..58e10bd 100644
--- 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
@@ -37,9 +37,9 @@ import static org.apache.sling.junit.jupiter.osgi.impl.ReflectionHelper.determin
*/
public abstract class AbstractTypeBasedParameterResolver implements ParameterResolver {
- protected abstract boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType);
+ protected abstract boolean supportsParameter(@NotNull ParameterContext parameterContext, @NotNull ExtensionContext extensionContext, @NotNull Type resolvedParameterType);
- protected abstract Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType);
+ protected abstract Object resolveParameter(@NotNull ParameterContext parameterContext, @NotNull ExtensionContext extensionContext, @NotNull Type resolvedParameterType);
@Override
public final boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
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
index 9bec7af..30aca67 100644
--- 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
@@ -18,6 +18,7 @@
*/
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;
@@ -44,10 +45,10 @@ public abstract class TypeBasedParameterResolver<T> extends AbstractTypeBasedPar
}
@Override
- protected boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType) {
+ protected boolean supportsParameter(@NotNull ParameterContext parameterContext, @NotNull ExtensionContext extensionContext, @NotNull Type resolvedParameterType) {
return supportedType == resolvedParameterType;
}
@Override
- protected abstract T resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType);
+ protected abstract T resolveParameter(@NotNull ParameterContext parameterContext, @NotNull ExtensionContext extensionContext, @NotNull 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
index 83d36bf..cf693ad 100644
--- 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
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-@Version("1.0.0")
+@Version("1.1.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/jupiter/osgi/OSGiAnnotationTest.java b/src/test/java/org/apache/sling/junit/jupiter/osgi/OSGiAnnotationTest.java
index 03c637a..5fd8bd4 100644
--- a/src/test/java/org/apache/sling/junit/jupiter/osgi/OSGiAnnotationTest.java
+++ b/src/test/java/org/apache/sling/junit/jupiter/osgi/OSGiAnnotationTest.java
@@ -19,18 +19,17 @@
package org.apache.sling.junit.jupiter.osgi;
import org.apache.sling.junit.impl.servlet.junit5.JUnitPlatformHelper;
+import org.apache.sling.junit.jupiter.osgi.utils.MetaTest;
import org.apache.sling.testing.mock.osgi.junit5.OsgiContext;
import org.apache.sling.testing.mock.osgi.junit5.OsgiContextExtension;
-import org.hamcrest.Matchers;
+import org.hamcrest.Matcher;
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;
@@ -44,6 +43,7 @@ import org.osgi.framework.FrameworkUtil;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
@@ -51,16 +51,23 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Arrays.asList;
+import static org.apache.sling.junit.jupiter.osgi.ServiceCardinality.MANDATORY;
+import static org.apache.sling.junit.jupiter.osgi.ServiceCardinality.OPTIONAL;
import static org.apache.sling.junit.jupiter.osgi.impl.ReflectionHelper.parameterizedTypeForBaseClass;
+import static org.apache.sling.junit.jupiter.osgi.utils.MemberMatcher.hasMember;
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.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
@@ -85,32 +92,19 @@ class OSGiAnnotationTest {
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")
+ @MetaTest({BundleInjection.class, BundleContextInjection.class})
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")
+ @MetaTest({ServiceInjectionGloballyAnnotated.class,
+ InheritedServiceInjectionGloballyAnnotated.class,
+ ServiceInjectionGloballyAnnotatedWithFilter.class,
+ MultipleServiceInjectionParameterAnnotated.class,
+ MultipleServiceInjectionMethodAnnotated.class,
+ MultipleServiceInjectionClassAnnotated.class})
void injectServices(String name, Class<?> testClass, String testMethodName) {
osgiContext.registerService(ServiceInterface.class, new ServiceA(), "foo", "quz");
withMockedFrameworkUtil(() -> {
@@ -118,36 +112,47 @@ class OSGiAnnotationTest {
});
}
- @SuppressWarnings("unused") // provides parameters
- static Stream<Arguments> failConstructionDueToMissingServiceInjectionTests() {
- return Stream.of(PseudoTestServiceInjectionNotAnnotated.class, PseudoTestServiceInjectionGloballyAnnotatedWithFilter.class)
- .flatMap(namedMethods("injectedConstructorParameter"));
+ @MetaTest(value = ServiceInjectionNoServiceAnnotation.class, methods = "injectedConstructorParameter")
+ void failConstructionDueToMissingServiceAnnotation(String name, Class<?> testClass, String testMethodName) {
+ osgiContext.registerService(ServiceInterface.class, new ServiceA());
+ withMockedFrameworkUtil(() -> {
+ assertFailure(testClass, testMethodName, ParameterResolutionException.class,
+ allOf(containsString("No ParameterResolver registered for parameter "), containsString(" in constructor ")));
+ });
}
- @ParameterizedTest(name = "{0}#{2}")
- @MethodSource("failConstructionDueToMissingServiceInjectionTests")
- void failConstructionDueToMissingServiceInjection(String name, Class<?> testClass, String testMethodName) {
+ @MetaTest(value = ServiceInjectionGloballyAnnotatedWithFilter.class, methods = "injectedConstructorParameter")
+ void failConstructionDueToMissingService(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);
+ assertFailure(testClass, testMethodName, ParameterResolutionException.class,
+ allOf(containsString("No service of type "), containsString(" available")));
});
}
- @SuppressWarnings("unused")
- static Stream<Arguments> serviceInjectionVariantsTests() {
- return Stream.of(PseudoTestServiceMethodInjection.class)
- .flatMap(namedMethods(
- "annotatedParameterWithExplicitClass",
- "annotatedParameterWithImpliedClass",
- "annotatedParameterWithExplicitClassMultiple",
- "annotatedParameterWithImpliedClassMultiple",
- "annotatedMethod"));
+ @MetaTest(MultipleAnnotationsOnParameterFailure.class)
+ void failMultipleAnnotationsOnParameter(String name, Class<?> testClass, String testMethodName) {
+ withMockedFrameworkUtil(() -> {
+ assertFailure(testClass, testMethodName, ParameterResolutionException.class,
+ allOf(
+ startsWith("Parameters must not be annotated with multiple @Service annotations: "),
+ containsString("MultipleAnnotationsOnParameterFailure"), // class name
+ containsString("multipleAnnotationsFailure"), // method name
+ containsString("MissingServiceInterface"))); // parameter type
+ });
}
- @ParameterizedTest(name = "{0}#{2}")
- @MethodSource("serviceInjectionVariantsTests")
- void serviceInjectionVariants(String name, Class<?> testClass, String testMethodName) {
+ @MetaTest(InvalidFilterExpressionFailure.class)
+ void failInvalidFilterExpression(String name, Class<?> testClass, String testMethodName) {
+ withMockedFrameworkUtil(() -> {
+ assertFailure(testClass, testMethodName, ParameterResolutionException.class,
+ equalTo("Invalid filter expression used in @Service annotation :\"(abc = def\""));
+ });
+ }
+
+ @MetaTest(ServiceMethodInjection.class)
+ void allServiceMethodInjectionTests(String name, Class<?> testClass, String testMethodName) {
osgiContext.registerService(ServiceInterface.class, new ServiceA(), "service.ranking", 3);
osgiContext.registerService(ServiceInterface.class, new ServiceC(), "service.ranking", 1);
osgiContext.registerService(ServiceInterface.class, new ServiceB(), "service.ranking", 2);
@@ -156,30 +161,149 @@ class OSGiAnnotationTest {
});
}
- @Test
- void injectServiceAsAnnotatedMethodParameterWithImplicitClassEmptyMultiple() {
+ @MetaTest(value = ServiceMethodInjection.class, methods = {
+ "annotatedParameterWithImpliedClassOptionalMultiple",
+ "annotatedParameterWithImpliedClassExplicitOptionalMultiple"
+ })
+ void injectServiceAsAnnotatedMethodParameterWithImplicitClassEmptyMultiple(String name, Class<?> testClass, String testMethodName) {
+ withMockedFrameworkUtil(() -> {
+ assertNoFailures(testClass, testMethodName);
+ });
+ }
+
+ @MetaTest(MismatchedServiceTypeOnAnnotatedParameter.class)
+ void failOnMismatchedServiceType(String name, Class<?> testClass, String testMethodName) {
+ 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, "annotatedParameterWithImpliedClassEmptyMultiple");
+ assertFailure(testClass, testMethodName, ParameterResolutionException.class,
+ equalTo("Mismatched types in annotation and parameter. " +
+ "Annotation type is \"ServiceB\", parameter type is \"ServiceInterface\""));
});
}
- @Test
- void injectServiceAsAnnotatedMethodParameterWithIncorrectExplicitClassMultiple() {
+ @MetaTest(MissingMandatoryServiceInjectionOnAnnotatedParameter.class)
+ void failOnMissingMandatoryService(String name, Class<?> testClass, String testMethodName) {
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\""));
+ assertFailure(testClass, testMethodName, ParameterResolutionException.class,
+ equalTo("No service of type \"org.apache.sling.junit." +
+ "jupiter.osgi.OSGiAnnotationTest$MissingServiceInterface\" available"));
});
}
+ @MetaTest(MissingFilteredMandatoryServiceInjectionOnAnnotatedParameter.class)
+ void failOnMissingFilteredService(String name, Class<?> testClass, String testMethodName) {
+ 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(() -> {
+ assertFailure(testClass, testMethodName, ParameterResolutionException.class,
+ equalTo("No service of type \"org.apache.sling.junit." +
+ "jupiter.osgi.OSGiAnnotationTest$ServiceInterface\" " +
+ "with filter \"(service.ranking=100)\" available"));
+ });
+ }
+
+
+ @MetaTest({BundleInjection.class, BundleContextInjection.class, ServiceInjectionGloballyAnnotated.class})
+ void failOutsideOSGiEnvironment(String name, Class<?> testClass, String testMethodName) {
+ assertFailure(testClass, testMethodName, ParameterResolutionException.class,
+ endsWith("@OSGi and @Service annotations can only be used with tests running in an OSGi environment"));
+ }
+
+ private void assertFailure(Class<?> testClass, String testMethodName, Class<? extends Throwable> exceptionClass, Matcher<String> exceptionMessageMatcher) {
+ final TestExecutionSummary summary = executeAndSummarize(testClass, testMethodName);
+ final List<TestExecutionSummary.Failure> failures = summary.getFailures().stream().filter(failure -> failure.getTestIdentifier().isTest()).collect(Collectors.toList());
+ assertThat(failures, contains(
+ hasMember("getException()", TestExecutionSummary.Failure::getException,
+ allOf(instanceOf(exceptionClass), hasMember("getMessage()", Throwable::getMessage, exceptionMessageMatcher)))));
+ }
+
@OSGi
- static class PseudoTestServiceMethodInjection {
+ static class MismatchedServiceTypeOnAnnotatedParameter {
+
+ @Test
+ void unary(@Service(ServiceB.class) ServiceInterface service) {
+ failMismatchedServiceType();
+ }
+
+ @Test
+ void multiple(@Service(ServiceB.class) List<ServiceInterface> services) {
+ failMismatchedServiceType();
+ }
+
+ private static void failMismatchedServiceType() {
+ fail("Method not be called due to mismatching service types");
+ }
+ }
+
+ @OSGi
+ @Service(value = ServiceInterface.class, cardinality = MANDATORY, filter = "(service.ranking=100)")
+ static class MissingFilteredMandatoryServiceInjectionOnAnnotatedParameter {
+
+ @Test
+ void missingUnaryFilteredService(ServiceInterface service) {
+ failMissing();
+ }
+
+ @Test
+ void missingMultipleFilteredService(List<ServiceInterface> services) {
+ failMissing();
+ }
+
+ private static void failMissing() {
+ fail("Method must not be called due to missing service with matching filter");
+ }
+
+ }
+
+ @OSGi
+ static class MissingMandatoryServiceInjectionOnAnnotatedParameter {
+
+ @Test
+ void missingImplictlyMandatoryUnaryService(@Service MissingServiceInterface service) {
+ failMandatory();
+ }
+
+ @Test
+ void missingExplicitlyMandatoryUnaryService(@Service(cardinality = MANDATORY) MissingServiceInterface service) {
+ failMandatory();
+ }
+
+ @Test
+ void missingMandatoryMultipleService(@Service(cardinality = MANDATORY) List<MissingServiceInterface> services) {
+ failMandatory();
+ }
+
+ private static void failMandatory() {
+ fail("Method must not be called due to missing mandatory service");
+ }
+ }
+
+ @OSGi
+ static class ServiceMethodInjection {
+
+ @Test
+ void annotatedParameterWithMissingOptionalService(@Service(cardinality = OPTIONAL) MissingServiceInterface service) {
+ assertThat(service, nullValue());
+ }
+
+ @Test
+ void annotatedParameterWithImpliedClassExplicitOptionalMultiple(@Service(cardinality = OPTIONAL) List<MissingServiceInterface> services) {
+ assertThat(services, instanceOf(List.class));
+ assertThat(services, empty());
+ }
+
+ @Test
+ void annotatedParameterWithImpliedClassOptionalMultiple(@Service List<MissingServiceInterface> services) {
+ assertThat(services, instanceOf(List.class));
+ assertThat(services, empty());
+ }
+
@Test
void annotatedParameterWithExplicitClass(@Service(ServiceInterface.class) ServiceInterface serviceA) {
assertThat(serviceA, instanceOf(ServiceA.class));
@@ -203,15 +327,14 @@ class OSGiAnnotationTest {
}
@Test
- void annotatedParameterWithImpliedClassEmptyMultiple(@Service List<ServiceInterface> services) {
+ void annotatedParameterWithImpliedClassExplicitMandatoryMultiple(@Service(cardinality = MANDATORY) List<ServiceInterface> services) {
assertThat(services, instanceOf(List.class));
- assertThat(services, empty());
+ assertThat(services, contains(asList(instanceOf(ServiceA.class), instanceOf(ServiceB.class), instanceOf(ServiceC.class))));
}
@Test
- void annotatedParameterWithIncorrectExplicitClassMultiple(@Service(ServiceB.class) List<ServiceInterface> services) {
- assertThat(services, instanceOf(List.class));
- assertThat(services, contains(instanceOf(ServiceA.class)));
+ void annotatedParameterWithExistingOptionalService(@Service(cardinality = OPTIONAL) ServiceInterface service) {
+ assertThat(service, instanceOf(ServiceA.class));
}
@Test
@@ -269,78 +392,127 @@ class OSGiAnnotationTest {
}
}
- 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 BundleContextInjection extends Injection<BundleContext> {
+ public BundleContextInjection(BundleContext object) {
+ super(object);
+ }
+ }
+
+ @OSGi
+ static class BundleInjection extends Injection<Bundle> {
+ public BundleInjection(Bundle object) {
+ super(object);
+ }
+ }
+
+ @OSGi
+ static class ServiceInjectionNoServiceAnnotation extends Injection<ServiceInterface> {
+ public ServiceInjectionNoServiceAnnotation(ServiceInterface object) {
+ super(object);
+ }
}
@OSGi
- static class PseudoTestBundleContextInjection extends Injection<BundleContext> {
- public PseudoTestBundleContextInjection(BundleContext object) {
+ @Service(ServiceInterface.class)
+ static class ServiceInjectionGloballyAnnotated extends Injection<ServiceInterface> {
+ public ServiceInjectionGloballyAnnotated(ServiceInterface object) {
super(object);
}
}
@OSGi
- static class PseudoTestBundleInjection extends Injection<Bundle> {
- public PseudoTestBundleInjection(Bundle object) {
+ @Service(value = ServiceInterface.class, filter = "(foo=quz)")
+ static class ServiceInjectionGloballyAnnotatedWithFilter extends Injection<ServiceInterface> {
+ public ServiceInjectionGloballyAnnotatedWithFilter(ServiceInterface object) {
+ super(object);
+ }
+ }
+
+ static class InheritedServiceInjectionGloballyAnnotated extends ServiceInjectionGloballyAnnotated {
+ public InheritedServiceInjectionGloballyAnnotated(ServiceInterface object) {
super(object);
}
}
@OSGi
- static class PseudoTestServiceInjectionNotAnnotated extends Injection<ServiceInterface> {
- public PseudoTestServiceInjectionNotAnnotated(ServiceInterface object) {
+ static class MultipleServiceInjectionParameterAnnotated extends AbstractMultipleServiceInjection {
+ public MultipleServiceInjectionParameterAnnotated(@Service List<ServiceInterface> object) {
super(object);
}
@Override
- void injectedMethodParameter(ServiceInterface objectFromMethodInjection) {
+ void injectedMethodParameter(@Service List<ServiceInterface> objectFromMethodInjection) {
super.injectedMethodParameter(objectFromMethodInjection);
}
}
@OSGi
- @Service(ServiceInterface.class)
- static class PseudoTestServiceInjectionGloballyAnnotated extends Injection<ServiceInterface> {
- public PseudoTestServiceInjectionGloballyAnnotated(ServiceInterface object) {
+ static class MultipleServiceInjectionMethodAnnotated extends AbstractMultipleServiceInjection {
+
+ @Service(ServiceInterface.class)
+ public MultipleServiceInjectionMethodAnnotated(List<ServiceInterface> object) {
super(object);
}
+
+ @Test @Override
+ @Service(ServiceInterface.class)
+ void injectedMethodParameter(List<ServiceInterface> objectFromMethodInjection) {
+ super.injectedMethodParameter(objectFromMethodInjection);
+ }
}
- static class PseudoTestInheritedServiceInjectionGloballyAnnotated extends PseudoTestServiceInjectionGloballyAnnotated {
- public PseudoTestInheritedServiceInjectionGloballyAnnotated(ServiceInterface object) {
+ @OSGi
+ @Service(ServiceInterface.class)
+ static class MultipleServiceInjectionClassAnnotated extends AbstractMultipleServiceInjection {
+ public MultipleServiceInjectionClassAnnotated(List<ServiceInterface> object) {
super(object);
}
}
@OSGi
- @Service(value = ServiceInterface.class, filter = "(foo=quz)")
- static class PseudoTestServiceInjectionGloballyAnnotatedWithFilter extends Injection<ServiceInterface> {
- public PseudoTestServiceInjectionGloballyAnnotatedWithFilter(ServiceInterface object) {
+ static class MultipleAnnotationsOnParameterFailure {
+ @Test
+ void multipleAnnotationsFailure(@Service @Service(MissingServiceInterface.class) MissingServiceInterface service) {
+ fail("Method must not be called due to duplicate @Service annotation on parameter");
+ }
+ }
+
+ @OSGi
+ static class InvalidFilterExpressionFailure {
+ @Test
+ void invalidFilterExpressionFailure(@Service(filter = "(abc = def") MissingServiceInterface service) {
+ fail("Method must not be called due to duplicate @Service annotation on parameter");
+ }
+ }
+
+ static abstract class AbstractMultipleServiceInjection extends Injection<List<ServiceInterface>> {
+
+ public AbstractMultipleServiceInjection(List<ServiceInterface> object) {
super(object);
}
+
+ @Test @Override
+ void injectedMethodParameter(List<ServiceInterface> objectFromMethodInjection) {
+ assertNotNull(objectFromMethodInjection, typeName + " method parameter");
+ assertEquals(objectFromConstructor, objectFromMethodInjection);
+ assertThat("number of services", objectFromMethodInjection.size(), is(1));
+ assertThat( "same service instance should be contained in the Lists injected into method and constructor",
+ objectFromConstructor, contains(sameInstance(objectFromMethodInjection.get(0))));
+ }
}
static abstract class Injection<T> {
- T objectFromConstructor;
+ protected T objectFromConstructor;
- private final String typeName;
+ protected final String typeName;
public Injection(T object) {
this.objectFromConstructor = object;
final ParameterizedType parameterizedType = parameterizedTypeForBaseClass(Injection.class, getClass());
- this.typeName = ((Class<?>) parameterizedType.getActualTypeArguments()[0]).getSimpleName();
+ final Type actualTypeArgument = parameterizedType.getActualTypeArguments()[0];
+ this.typeName = actualTypeArgument.getTypeName();
}
@Test
@@ -367,4 +539,7 @@ class OSGiAnnotationTest {
static class ServiceC implements ServiceInterface {
}
+
+ interface MissingServiceInterface {
+ }
}
diff --git a/src/test/java/org/apache/sling/junit/jupiter/osgi/utils/MemberMatcher.java b/src/test/java/org/apache/sling/junit/jupiter/osgi/utils/MemberMatcher.java
new file mode 100644
index 0000000..65d003c
--- /dev/null
+++ b/src/test/java/org/apache/sling/junit/jupiter/osgi/utils/MemberMatcher.java
@@ -0,0 +1,75 @@
+/*
+ * 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.utils;
+
+import org.hamcrest.Description;
+import org.hamcrest.Factory;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeDiagnosingMatcher;
+
+import java.util.function.Function;
+
+import static org.hamcrest.Condition.matched;
+
+public class MemberMatcher<S, T> extends TypeSafeDiagnosingMatcher<S> {
+
+ private final String memberName;
+
+ private final Function<S, T> memberAccessor;
+
+ private final Matcher<T> valueMatcher;
+
+ private MemberMatcher(String memberName, Function<S,T> memberAccessor, Matcher<T> valueMatcher) {
+ this.memberName = memberName;
+ this.memberAccessor = memberAccessor;
+ this.valueMatcher = valueMatcher;
+ }
+
+ @Override
+ public boolean matchesSafely(S object, Description mismatch) {
+ T value = memberAccessor.apply(object);
+ return matched(value, mismatch)
+ .matching(valueMatcher);
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("(").appendText(memberName).appendText(", ")
+ .appendDescriptionOf(valueMatcher).appendText(")");
+ }
+
+
+ /**
+ * Creates a matcher that matches when the examined object has a JavaBean property
+ * with the specified name whose value satisfies the specified matcher.
+ * <p/>
+ * For example:
+ * <pre>assertThat(myBean, hasProperty("foo", equalTo("bar"))</pre>
+ *
+ * @param memberAccessor
+ * the name of the JavaBean property that examined beans should possess
+ * @param valueMatcher
+ * a matcher for the value of the specified property of the examined bean
+ */
+ @Factory
+ public static <S, T> Matcher<S> hasMember(String name, Function<S, T> memberAccessor, Matcher<T> valueMatcher) {
+ return new MemberMatcher<>(name, memberAccessor, valueMatcher);
+ }
+}
+
diff --git a/src/test/java/org/apache/sling/junit/jupiter/osgi/utils/MetaTest.java b/src/test/java/org/apache/sling/junit/jupiter/osgi/utils/MetaTest.java
new file mode 100644
index 0000000..08ea623
--- /dev/null
+++ b/src/test/java/org/apache/sling/junit/jupiter/osgi/utils/MetaTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.utils;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.ArgumentsProvider;
+import org.junit.jupiter.params.provider.ArgumentsSource;
+import org.junit.jupiter.params.support.AnnotationConsumer;
+import org.junit.platform.commons.annotation.Testable;
+import org.junit.platform.commons.support.HierarchyTraversalMode;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import static java.util.Arrays.asList;
+import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedMethods;
+
+@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@ArgumentsSource(MetaTestSourceProvider.class)
+@ParameterizedTest(name = "{0}#{2}")
+public @interface MetaTest {
+ /**
+ * Meta test classes, i.e. classes with methods directly or indirectly
+ * annotated as {@code @Testable}.
+ */
+ Class<?>[] value();
+
+ /**
+ * List of test method names that should be called on the test class(es).
+ * By default all {@code @Testable} methods are called.
+ */
+ String[] methods() default {};
+}
+
+class MetaTestSourceProvider implements ArgumentsProvider, AnnotationConsumer<MetaTest> {
+
+ private Class<?>[] testClasses;
+
+ private List<String> allowedMethodNames;
+
+ @Override
+ public void accept(MetaTest metaTest) {
+ testClasses = metaTest.value();
+ allowedMethodNames = asList(metaTest.methods());
+ }
+
+ @Override
+ public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
+ return Stream.of(testClasses).flatMap(this::permutate);
+ }
+
+ @NotNull
+ private Stream<Arguments> permutate(Class<?> cls) {
+ return findAnnotatedMethods(cls, Testable.class, HierarchyTraversalMode.BOTTOM_UP).stream()
+ .filter(method -> allowedMethodNames.isEmpty() || allowedMethodNames.contains(method.getName()))
+ .map(toArguments(cls));
+ }
+
+ @NotNull
+ private static Function<Method, Arguments> toArguments(Class<?> cls) {
+ return method -> Arguments.of(cls.getSimpleName(), cls, method.getName());
+ }
+}