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:30 UTC
[sling-org-apache-sling-junit-core] branch master updated (9200524
-> 05eeeb4)
This is an automated email from the ASF dual-hosted git repository.
jsedding pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-junit-core.git.
discard 9200524 @Service annotation for Jupiter should explicitly allow for optional and mandatory service injection
new 05eeeb4 @Service annotation for Jupiter should explicitly allow for optional and mandatory service injection
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (9200524)
\
N -- N -- N refs/heads/master (05eeeb4)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../java/org/apache/sling/junit/jupiter/osgi/OSGiAnnotationTest.java | 1 -
1 file changed, 1 deletion(-)
[sling-org-apache-sling-junit-core] 01/01: @Service annotation for
Jupiter should explicitly allow for optional and mandatory service
injection
Posted by js...@apache.org.
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());
+ }
+}