blob: 03c637abf03d71892f266c45b7b065d5cb2716e3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sling.junit.jupiter.osgi;
import org.apache.sling.junit.impl.servlet.junit5.JUnitPlatformHelper;
import org.apache.sling.testing.mock.osgi.junit5.OsgiContext;
import org.apache.sling.testing.mock.osgi.junit5.OsgiContextExtension;
import org.hamcrest.Matchers;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.engine.JupiterTestEngine;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.platform.commons.annotation.Testable;
import org.junit.platform.commons.support.HierarchyTraversalMode;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.opentest4j.MultipleFailuresError;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Arrays.asList;
import static org.apache.sling.junit.jupiter.osgi.impl.ReflectionHelper.parameterizedTypeForBaseClass;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedMethods;
/**
* This test executes pseudo test classes using the {@link JupiterTestEngine} in order to
* verify the correct injection of parameters via the {@code @OSGi} and {@code @Service}
* annotations.
* <br>
* In order to achieve this, test methods from the pseudo test classes ({@code PseudoTest*}
* are executed and the test summary evaluated to verify expectations. The additional indirection
* can be a little tricky, but is necessary to test the annotations work correctly. Particularly
* when testing failure scenarios, where the failure of a pseudo test is required to pass the actual
* test.
*/
@ExtendWith(OsgiContextExtension.class)
class OSGiAnnotationTest {
private static final JupiterTestEngine JUPITER_TEST_ENGINE = new JupiterTestEngine();
OsgiContext osgiContext = new OsgiContext();
@SuppressWarnings("unused") // provides parameters
static Stream<Arguments> frameworkObjectsInjectionTests() {
return Stream.of(PseudoTestBundleInjection.class, PseudoTestBundleContextInjection.class)
.flatMap(OSGiAnnotationTest::allTestMethods);
}
@ParameterizedTest(name = "{0}#{2}")
@MethodSource("frameworkObjectsInjectionTests")
void injectFrameworkObjects(String name, Class<?> testClass, String testMethodName) {
withMockedFrameworkUtil(() -> {
assertNoFailures(testClass, testMethodName);
});
}
@SuppressWarnings("unused") // provides parameters
static Stream<Arguments> serviceInjectionTests() {
return Stream
.of(
PseudoTestServiceInjectionGloballyAnnotated.class,
PseudoTestServiceInjectionGloballyAnnotatedWithFilter.class,
PseudoTestInheritedServiceInjectionGloballyAnnotated.class)
.flatMap(OSGiAnnotationTest::allTestMethods);
}
@ParameterizedTest(name = "{0}#{2}")
@MethodSource("serviceInjectionTests")
void injectServices(String name, Class<?> testClass, String testMethodName) {
osgiContext.registerService(ServiceInterface.class, new ServiceA(), "foo", "quz");
withMockedFrameworkUtil(() -> {
assertNoFailures(testClass, testMethodName);
});
}
@SuppressWarnings("unused") // provides parameters
static Stream<Arguments> failConstructionDueToMissingServiceInjectionTests() {
return Stream.of(PseudoTestServiceInjectionNotAnnotated.class, PseudoTestServiceInjectionGloballyAnnotatedWithFilter.class)
.flatMap(namedMethods("injectedConstructorParameter"));
}
@ParameterizedTest(name = "{0}#{2}")
@MethodSource("failConstructionDueToMissingServiceInjectionTests")
void failConstructionDueToMissingServiceInjection(String name, Class<?> testClass, String testMethodName) {
// setup service with non-matching filter
osgiContext.registerService(ServiceInterface.class, new ServiceA(), "foo", "no match");
withMockedFrameworkUtil(() -> {
assertTestConstructionFailsDueToMissingService(testClass, testMethodName);
});
}
@SuppressWarnings("unused")
static Stream<Arguments> serviceInjectionVariantsTests() {
return Stream.of(PseudoTestServiceMethodInjection.class)
.flatMap(namedMethods(
"annotatedParameterWithExplicitClass",
"annotatedParameterWithImpliedClass",
"annotatedParameterWithExplicitClassMultiple",
"annotatedParameterWithImpliedClassMultiple",
"annotatedMethod"));
}
@ParameterizedTest(name = "{0}#{2}")
@MethodSource("serviceInjectionVariantsTests")
void serviceInjectionVariants(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);
withMockedFrameworkUtil(() -> {
assertNoFailures(testClass, testMethodName);
});
}
@Test
void injectServiceAsAnnotatedMethodParameterWithImplicitClassEmptyMultiple() {
withMockedFrameworkUtil(() -> {
assertNoFailures(PseudoTestServiceMethodInjection.class, "annotatedParameterWithImpliedClassEmptyMultiple");
});
}
@Test
void injectServiceAsAnnotatedMethodParameterWithIncorrectExplicitClassMultiple() {
osgiContext.registerService(ServiceInterface.class, new ServiceA(), "service.ranking", 1);
osgiContext.registerService(ServiceInterface.class, new ServiceC(), "service.ranking", 3);
osgiContext.registerService(ServiceInterface.class, new ServiceB(), "service.ranking", 2);
withMockedFrameworkUtil(() -> {
final TestExecutionSummary summary = executeAndSummarize(PseudoTestServiceMethodInjection.class, "annotatedParameterWithIncorrectExplicitClassMultiple");
assertEquals(1, summary.getTestsFailedCount(), "expected test failure count");
final Throwable exception = summary.getFailures().get(0).getException();
assertThat(exception, instanceOf(ParameterResolutionException.class));
assertThat(exception.getMessage(), equalTo("Mismatched types in annotation and parameter. " +
"Annotation type is \"ServiceB\", parameter type is \"ServiceInterface\""));
});
}
@OSGi
static class PseudoTestServiceMethodInjection {
@Test
void annotatedParameterWithExplicitClass(@Service(ServiceInterface.class) ServiceInterface serviceA) {
assertThat(serviceA, instanceOf(ServiceA.class));
}
@Test
void annotatedParameterWithImpliedClass(@Service ServiceInterface serviceA) {
assertThat(serviceA, instanceOf(ServiceA.class));
}
@Test
void annotatedParameterWithExplicitClassMultiple(@Service(ServiceInterface.class) List<ServiceInterface> services) {
assertThat(services, instanceOf(List.class));
assertThat(services, contains(asList(instanceOf(ServiceA.class), instanceOf(ServiceB.class), instanceOf(ServiceC.class))));
}
@Test
void annotatedParameterWithImpliedClassMultiple(@Service Collection<ServiceInterface> services) {
assertThat(services, instanceOf(Collection.class));
assertThat(services, contains(asList(instanceOf(ServiceA.class), instanceOf(ServiceB.class), instanceOf(ServiceC.class))));
}
@Test
void annotatedParameterWithImpliedClassEmptyMultiple(@Service List<ServiceInterface> services) {
assertThat(services, instanceOf(List.class));
assertThat(services, empty());
}
@Test
void annotatedParameterWithIncorrectExplicitClassMultiple(@Service(ServiceB.class) List<ServiceInterface> services) {
assertThat(services, instanceOf(List.class));
assertThat(services, contains(instanceOf(ServiceA.class)));
}
@Test
@Service(ServiceInterface.class)
void annotatedMethod(ServiceInterface serviceA) {
assertThat(serviceA, instanceOf(ServiceA.class));
}
}
private void withMockedFrameworkUtil(Runnable callback) {
try (final MockedStatic<FrameworkUtil> frameworkUtilMock = Mockito.mockStatic(FrameworkUtil.class)) {
frameworkUtilMock
.when(() -> FrameworkUtil.getBundle(Mockito.any()))
.then(invocation -> osgiContext.bundleContext().getBundle());
callback.run();
}
}
@NotNull
private static Stream<Arguments> allTestMethods(Class<?> cls) {
return findAnnotatedMethods(cls, Testable.class, HierarchyTraversalMode.BOTTOM_UP).stream()
.map(toArguments(cls));
}
@NotNull
private static Function<Class<?>, Stream<Arguments>> namedMethods(String... testMethodNames) {
return cls -> findAnnotatedMethods(cls, Testable.class, HierarchyTraversalMode.BOTTOM_UP).stream()
.filter(method -> asList(testMethodNames).contains(method.getName()))
.map(toArguments(cls));
}
@NotNull
private static Function<Method, Arguments> toArguments(Class<?> cls) {
return method -> Arguments.of(cls.getSimpleName(), cls, method.getName());
}
private static TestExecutionSummary executeAndSummarize(@NotNull Class<?> testClass, @Nullable String testMethodName) {
final SummaryGeneratingListener listener = new SummaryGeneratingListener();
JUnitPlatformHelper.executeTest(JUPITER_TEST_ENGINE, testClass, testMethodName, listener);
return listener.getSummary();
}
private static void assertNoFailures(@NotNull Class<?> testClass, @Nullable String testMethodName) {
final TestExecutionSummary summary = executeAndSummarize(testClass, testMethodName);
assertThat("number of tests found", (int) summary.getTestsFoundCount(), greaterThan(0));
final List<TestExecutionSummary.Failure> failures = summary.getFailures();
switch (failures.size()) {
case 0:
break;
case 1:
fail("Got one failure instead of none", failures.get(0).getException());
default:
throw new MultipleFailuresError("Got " + failures.size() + " failures instead of none",
failures.stream().map(TestExecutionSummary.Failure::getException).collect(Collectors.toList()));
}
}
private void assertTestConstructionFailsDueToMissingService(Class<?> testClass, String testMethodName) {
final TestExecutionSummary summary = executeAndSummarize(testClass, testMethodName);
final List<TestExecutionSummary.Failure> failures = summary.getFailures();
assertEquals(1, failures.size(), "number of test failures");
final TestExecutionSummary.Failure failure = failures.get(0);
final Throwable exception = failure.getException();
assertThat(exception, Matchers.instanceOf(ParameterResolutionException.class));
assertThat(exception.getMessage(), anyOf(
allOf(containsString("No ParameterResolver registered for parameter "), containsString(" in constructor ")),
// allOf(containsString("Failed to resolve parameter "), containsString(" in constructor ")),
allOf(containsString("No service of type "), containsString(" available"))
));
}
@OSGi
static class PseudoTestBundleContextInjection extends Injection<BundleContext> {
public PseudoTestBundleContextInjection(BundleContext object) {
super(object);
}
}
@OSGi
static class PseudoTestBundleInjection extends Injection<Bundle> {
public PseudoTestBundleInjection(Bundle object) {
super(object);
}
}
@OSGi
static class PseudoTestServiceInjectionNotAnnotated extends Injection<ServiceInterface> {
public PseudoTestServiceInjectionNotAnnotated(ServiceInterface object) {
super(object);
}
@Override
void injectedMethodParameter(ServiceInterface objectFromMethodInjection) {
super.injectedMethodParameter(objectFromMethodInjection);
}
}
@OSGi
@Service(ServiceInterface.class)
static class PseudoTestServiceInjectionGloballyAnnotated extends Injection<ServiceInterface> {
public PseudoTestServiceInjectionGloballyAnnotated(ServiceInterface object) {
super(object);
}
}
static class PseudoTestInheritedServiceInjectionGloballyAnnotated extends PseudoTestServiceInjectionGloballyAnnotated {
public PseudoTestInheritedServiceInjectionGloballyAnnotated(ServiceInterface object) {
super(object);
}
}
@OSGi
@Service(value = ServiceInterface.class, filter = "(foo=quz)")
static class PseudoTestServiceInjectionGloballyAnnotatedWithFilter extends Injection<ServiceInterface> {
public PseudoTestServiceInjectionGloballyAnnotatedWithFilter(ServiceInterface object) {
super(object);
}
}
static abstract class Injection<T> {
T objectFromConstructor;
private final String typeName;
public Injection(T object) {
this.objectFromConstructor = object;
final ParameterizedType parameterizedType = parameterizedTypeForBaseClass(Injection.class, getClass());
this.typeName = ((Class<?>) parameterizedType.getActualTypeArguments()[0]).getSimpleName();
}
@Test
final void injectedConstructorParameter() {
assertNotNull(objectFromConstructor, typeName + " constructor parameter");
}
@Test
void injectedMethodParameter(T objectFromMethodInjection) {
assertNotNull(objectFromMethodInjection, typeName + " method parameter");
assertSame(objectFromConstructor, objectFromMethodInjection,
typeName + " same parameter should be injected into method and constructor");
}
}
interface ServiceInterface {
}
static class ServiceA implements ServiceInterface {
}
static class ServiceB implements ServiceInterface {
}
static class ServiceC implements ServiceInterface {
}
}