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
The following commit(s) were added to refs/heads/master by this push:
new 8a30309 SLING-9795 - JUnit 5 support for server-side tests
8a30309 is described below
commit 8a30309ddebaa86a9edf399d410fe891c15dbf7d
Author: Julian Sedding <[email protected]>
AuthorDate: Tue Oct 6 15:12:19 2020 +0200
SLING-9795 - JUnit 5 support for server-side tests
---
bnd.bnd | 14 +-
pom.xml | 60 ++-
.../java/org/apache/sling/junit/RequestParser.java | 11 +-
.../java/org/apache/sling/junit/TestsManager.java | 41 +-
.../java/org/apache/sling/junit/TestsProvider.java | 14 +-
.../sling/junit/annotations/package-info.java | 2 +-
.../sling/junit/impl/AbstractTestsProvider.java | 36 ++
.../sling/junit/impl/BundleTestsProvider.java | 250 ++++--------
.../junit/impl/JUnit4TestExecutionStrategy.java | 46 +++
.../junit/impl/TestContextRunListenerWrapper.java | 35 +-
.../sling/junit/impl/TestExecutionStrategy.java | 32 ++
.../apache/sling/junit/impl/TestsManagerImpl.java | 306 +++++++-------
.../sling/junit/impl/servlet/HtmlRenderer.java | 16 +-
.../junit/impl/servlet/PlainTextRenderer.java | 15 +-
.../sling/junit/impl/servlet/ServletProcessor.java | 49 +--
.../sling/junit/impl/servlet/XmlRenderer.java | 8 +
.../impl/servlet/junit5/DescriptionGenerator.java | 65 +++
.../junit/impl/servlet/junit5/FailureHelper.java | 44 +++
.../junit5/JUnit5TestExecutionStrategy.java | 84 ++++
.../junit/impl/servlet/junit5/ResultAdapter.java | 80 ++++
.../impl/servlet/junit5/RunListenerAdapter.java | 136 +++++++
.../impl/servlet/junit5/TestEngineTracker.java | 92 +++++
.../java/org/apache/sling/junit/package-info.java | 2 +-
src/main/resources/junit.css | 3 +
.../impl/JUnit4TestExecutionStrategyTest.java | 64 +++
.../sling/junit/impl/TestsManagerImplTest.java | 438 +++++++++++++++++----
.../servlet/junit5/RunListenerAdapterTest.java | 134 +++++++
.../sling/junit/sampletests/JUnit4SlingJUnit.java | 48 +++
28 files changed, 1612 insertions(+), 513 deletions(-)
diff --git a/bnd.bnd b/bnd.bnd
index ccedd05..aea6f89 100644
--- a/bnd.bnd
+++ b/bnd.bnd
@@ -1,12 +1,8 @@
Bundle-Activator: org.apache.sling.junit.Activator
-Export-Package: junit.framework;version=${junit.version}, \
- org.junit;version=${junit.version}, \
- org.junit.matchers.*;version=${junit.version}, \
- org.junit.rules.*;version=${junit.version}, \
- org.junit.runner.*;version=${junit.version}, \
- org.junit.runners.*;version=${junit.version}, \
- org.junit.experimental.categories.*;version=${junit.version}, \
- org.junit.validator.*;version=${junit.version}, \
+Export-Package: !org.junit.platform.*, \
+ junit.*;version=${junit.version}, \
+ org.junit.*;version=${junit.version}, \
org.hamcrest.*;version=${hamcrest.version};-split-package:=merge-first
--conditionalpackage: org.hamcrest.*, org.junit.*, junit.*
+Import-Package: org.junit.platform.*;resolution:=optional, \
+ *
-includeresource: @org.jacoco.agent-*.jar!/org/jacoco/agent/rt/IAgent*
diff --git a/pom.xml b/pom.xml
index 2557f1b..6833e16 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,7 +34,7 @@
<description>Runs JUnit tests in an OSGi framework and provides the JUnit
libraries</description>
<properties>
- <junit.version>4.12</junit.version>
+ <junit.version>4.13</junit.version>
<hamcrest.version>1.3</hamcrest.version>
<jacoco.version>0.6.2.201302030002</jacoco.version>
</properties>
@@ -50,10 +50,6 @@
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-jar-plugin</artifactId>
- </plugin>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<stylesheet>maven</stylesheet>
@@ -61,6 +57,32 @@
</configuration>
</plugin>
</plugins>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>biz.aQute.bnd</groupId>
+ <artifactId>bnd-baseline-maven-plugin</artifactId>
+ <configuration>
+ <diffpackages>
+ <diffpackage>!junit.*</diffpackage>
+ <diffpackage>!org.junit.*</diffpackage>
+ <diffpackage>*</diffpackage>
+ </diffpackages>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.jacoco</groupId>
+ <artifactId>jacoco-maven-plugin</artifactId>
+ <configuration>
+ <excludes>
+ <exclude>junit/**</exclude>
+ <exclude>org/junit/**</exclude>
+ <exclude>org/hamcrest/**</exclude>
+ </excludes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </pluginManagement>
</build>
<profiles>
@@ -117,6 +139,12 @@
<dependencies>
<dependency>
+ <groupId>org.jetbrains</groupId>
+ <artifactId>annotations</artifactId>
+ <version>19.0.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.annotation.versioning</artifactId>
</dependency>
@@ -193,6 +221,15 @@
<artifactId>org.apache.sling.commons.osgi</artifactId>
<version>2.2.2</version>
</dependency>
+
+ <!-- optional imports for JUnit 5 support -->
+ <dependency>
+ <groupId>org.junit.platform</groupId>
+ <artifactId>junit-platform-launcher</artifactId>
+ <version>1.6.2</version>
+ <scope>provided</scope>
+ <optional>true</optional>
+ </dependency>
<!-- This bundle exposes the following dependencies at runtime,
therefore make those dependencies available in a transitive fashion (i.e. with
compile scope).
All bundles providing remote unit tests, should rely on the same
version of JUnit and Hamcrest.
-->
@@ -214,16 +251,17 @@
<version>${hamcrest.version}</version>
<scope>compile</scope>
</dependency>
+
<dependency>
- <groupId>org.powermock</groupId>
- <artifactId>powermock-module-junit4</artifactId>
- <version>2.0.5</version>
+ <groupId>org.junit.vintage</groupId>
+ <artifactId>junit-vintage-engine</artifactId>
+ <version>5.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>org.powermock</groupId>
- <artifactId>powermock-api-mockito2</artifactId>
- <version>2.0.5</version>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <version>3.5.7</version>
<scope>test</scope>
</dependency>
</dependencies>
diff --git a/src/main/java/org/apache/sling/junit/RequestParser.java
b/src/main/java/org/apache/sling/junit/RequestParser.java
index d680a2c..f8a0223 100644
--- a/src/main/java/org/apache/sling/junit/RequestParser.java
+++ b/src/main/java/org/apache/sling/junit/RequestParser.java
@@ -77,12 +77,17 @@ public class RequestParser implements TestSelector {
public String toString() {
return getClass().getSimpleName()
- + ", testSelector [" + testNameSelector + "]"
- + ", methodName [" + selectedMethodName + "]"
- + ", extension [" + extension + "]"
+ + ", testSelector [" + safeForLogging(testNameSelector) + "]"
+ + ", methodName [" + safeForLogging(selectedMethodName) + "]"
+ + ", extension [" + safeForLogging(extension) + "]"
;
}
+ private static String safeForLogging(String str) {
+ // protect against logging injection attacks
+ return str.replaceAll("[\n\r\t]", "_");
+ }
+
public String getTestSelectorString() {
return testNameSelector;
}
diff --git a/src/main/java/org/apache/sling/junit/TestsManager.java
b/src/main/java/org/apache/sling/junit/TestsManager.java
index 497ad0b..76f4781 100644
--- a/src/main/java/org/apache/sling/junit/TestsManager.java
+++ b/src/main/java/org/apache/sling/junit/TestsManager.java
@@ -18,6 +18,8 @@ package org.apache.sling.junit;
import java.util.Collection;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import org.osgi.annotation.versioning.ProviderType;
/**
@@ -31,13 +33,7 @@ public interface TestsManager {
* @param selector if null, returns all available tests.
* @return the name of the tests
*/
- Collection<String> getTestNames(TestSelector selector);
-
- /**
- * Clear our internal caches. Useful in automated testing, to make sure
changes introduced by recent uploads or configuration or bundles
- * changes are taken into account immediately.
- */
- void clearCaches();
+ Collection<String> getTestNames(@Nullable TestSelector selector);
/**
* Instantiate test class for specified test
@@ -46,7 +42,7 @@ public interface TestsManager {
* @return an instance of the class
* @throws ClassNotFoundException if a class for {@code testName} cannot
be found
*/
- Class<?> getTestClass(String testName) throws ClassNotFoundException;
+ Class<?> getTestClass(@NotNull String testName) throws
ClassNotFoundException;
/**
* List tests using supplied Renderer - does NOT call setup or cleanup on
renderer.
@@ -55,7 +51,17 @@ public interface TestsManager {
* @param renderer the renderer to use
* @throws Exception if any error occurs
*/
- void listTests(Collection<String> testNames, Renderer renderer) throws
Exception;
+ void listTests(@NotNull Collection<String> testNames, @NotNull Renderer
renderer) throws Exception;
+
+ /**
+ * Execute tests and report results using supplied Renderer - does NOT
call setup or cleanup on renderer.
+ *
+ * @param renderer the renderer to use for the reporting
+ * @param selector the selector used to select tests and test methods;
all tests are executed if this is null
+ * @throws NoTestCasesFoundException if no tests matching the selector are
available
+ * @throws Exception if an error occurs
+ */
+ void executeTests(@NotNull Renderer renderer, @Nullable TestSelector
selector) throws NoTestCasesFoundException, Exception;
/**
* Execute tests and report results using supplied Renderer - does NOT
call setup or cleanup on renderer.
@@ -64,6 +70,21 @@ public interface TestsManager {
* @param renderer the renderer to use for the reporting
* @param selector the selector used to select tests and test methods (it
can be {@code null})
* @throws Exception if any error occurs
+ *
+ * @deprecated use {@link #executeTests(Renderer, TestSelector)} instead
+ */
+ @Deprecated
+ void executeTests(@Nullable Collection<String> testNames, @NotNull
Renderer renderer, @Nullable TestSelector selector)
+ throws Exception;
+
+ /**
+ * Clear our internal caches. Useful in automated testing, to make sure
changes introduced by recent uploads or configuration or bundles
+ * changes are taken into account immediately.
+ *
+ * @deprecated Caches have been removed.
*/
- void executeTests(Collection<String> testNames, Renderer renderer,
TestSelector selector) throws Exception;
+ @Deprecated
+ void clearCaches();
+
+ class NoTestCasesFoundException extends RuntimeException {}
}
diff --git a/src/main/java/org/apache/sling/junit/TestsProvider.java
b/src/main/java/org/apache/sling/junit/TestsProvider.java
index 31e0a69..2e36f79 100644
--- a/src/main/java/org/apache/sling/junit/TestsProvider.java
+++ b/src/main/java/org/apache/sling/junit/TestsProvider.java
@@ -25,8 +25,11 @@ public interface TestsProvider {
/**
* Return this service's PID, client might use it later to instantiate a
specific test.
*
- * @return the service pid
+ * @return the service pid or null
+ *
+ * @deprecated No longer used.
*/
+ @Deprecated
String getServicePid();
/**
@@ -49,7 +52,14 @@ public interface TestsProvider {
/**
* Return the timestamp at which our list of tests was last modified
*
- * @return the last modified date of the tests list as a timestamp
+ * @return the last modified date of the tests list as a timestamp or -1
if not supported
+ *
+ * @deprecated No longer used. {@code TestManager} always gets the latest
tests
+ * from the {@code TestsProvider} instances. Any performance issues need
to be
+ * addressed inside the {@code TestsProvider} implementation, e.g. by
+ * caching.
*/
+ @Deprecated
long lastModified();
+
}
diff --git a/src/main/java/org/apache/sling/junit/annotations/package-info.java
b/src/main/java/org/apache/sling/junit/annotations/package-info.java
index 46f3101..f265cce 100644
--- a/src/main/java/org/apache/sling/junit/annotations/package-info.java
+++ b/src/main/java/org/apache/sling/junit/annotations/package-info.java
@@ -16,7 +16,7 @@
~ specific language governing permissions and limitations
~ under the License.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
-@Version("1.0.8")
+@Version("1.1.0")
package org.apache.sling.junit.annotations;
import org.osgi.annotation.versioning.Version;
diff --git
a/src/main/java/org/apache/sling/junit/impl/AbstractTestsProvider.java
b/src/main/java/org/apache/sling/junit/impl/AbstractTestsProvider.java
new file mode 100644
index 0000000..815ae32
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/impl/AbstractTestsProvider.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.impl;
+
+import org.apache.sling.junit.TestsProvider;
+
+/**
+ * To help with backwards compatibility of deprecated methods.
+ */
+public abstract class AbstractTestsProvider implements TestsProvider {
+ @Override
+ public String getServicePid() {
+ return null;
+ }
+
+ @Override
+ public long lastModified() {
+ return -1;
+ }
+}
diff --git a/src/main/java/org/apache/sling/junit/impl/BundleTestsProvider.java
b/src/main/java/org/apache/sling/junit/impl/BundleTestsProvider.java
index 55a47f5..de7b785 100644
--- a/src/main/java/org/apache/sling/junit/impl/BundleTestsProvider.java
+++ b/src/main/java/org/apache/sling/junit/impl/BundleTestsProvider.java
@@ -17,21 +17,28 @@
package org.apache.sling.junit.impl;
import java.net.URL;
-import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.Enumeration;
-import java.util.HashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
+import java.util.stream.Collectors;
import org.apache.sling.junit.TestsProvider;
+import org.jetbrains.annotations.NotNull;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
-import org.osgi.framework.BundleListener;
-import org.osgi.service.component.ComponentContext;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.util.tracker.BundleTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,191 +46,108 @@ import org.slf4j.LoggerFactory;
* that have a Sling-Test-Regexp header and corresponding
* exported classes.
*/
-@Component
-public class BundleTestsProvider implements TestsProvider, BundleListener {
- private final Logger log = LoggerFactory.getLogger(getClass());
-private static final String COMPONENT_NAME = "component.name";
- private long lastModified;
- private BundleContext bundleContext;
- private String componentName;
-
+@Component(service = TestsProvider.class)
+public class BundleTestsProvider extends AbstractTestsProvider {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(BundleTestsProvider.class);
+
public static final String SLING_TEST_REGEXP = "Sling-Test-Regexp";
- /** Symbolic names of bundles that changed state - if not empty, need
- * to adjust the list of tests
- */
- private final List<String> changedBundles = new ArrayList<String>();
-
- /** List of (candidate) test classes, keyed by bundle so that we can
- * update them easily when bundles come and go
- */
- private final Map<String, List<String>> testClassesMap = new
HashMap<String, List<String>>();
-
- protected void activate(ComponentContext ctx) {
- bundleContext = ctx.getBundleContext();
- bundleContext.addBundleListener(this);
-
- // Initially consider all bundles as "changed"
- for(Bundle b : bundleContext.getBundles()) {
- if(getSlingTestRegexp(b) != null) {
- changedBundles.add(b.getSymbolicName());
- log.debug("Will look for test classes inside bundle {}",
b.getSymbolicName());
- }
+ private TestClassesTracker tracker;
+
+ @Activate
+ protected void activate(BundleContext ctx) {
+ tracker = new TestClassesTracker(ctx);
+ tracker.open();
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ if (tracker != null) {
+ tracker.close();
+ tracker = null;
}
-
- lastModified = System.currentTimeMillis();
- componentName = (String)ctx.getProperties().get(COMPONENT_NAME);
}
-
- protected void deactivate(ComponentContext ctx) {
- bundleContext.removeBundleListener(this);
- bundleContext = null;
- changedBundles.clear();
+
+ public Class<?> createTestClass(String testName) throws
ClassNotFoundException {
+ final Bundle bundle = tracker.getTracked().entrySet().stream()
+ .filter(entry -> entry.getValue().contains(testName))
+ .map(Map.Entry::getKey)
+ .findFirst()
+ .orElseThrow(() -> new ClassNotFoundException("No Bundle found
that supplies test class " + testName));
+ return bundle.loadClass(testName);
}
-
- @Override
- public String toString() {
- return getClass().getSimpleName() + ", componentName(pid)=" +
componentName;
+
+ public List<String> getTestNames() {
+ return tracker.getTracked().values().stream()
+ .flatMap(Collection::stream)
+ .collect(Collectors.toList());
}
- /** Update testClasses if bundle changes require it */
- private void maybeUpdateTestClasses() {
- if(changedBundles.isEmpty()) {
- return;
+ private static class TestClassesTracker extends BundleTracker<Set<String>>
{
+ public TestClassesTracker(BundleContext ctx) {
+ super(ctx, Bundle.ACTIVE, null);
}
- // Get the list of bundles that have changed
- final List<String> bundlesToUpdate = new ArrayList<String>();
- synchronized (changedBundles) {
- bundlesToUpdate.addAll(changedBundles);
- changedBundles.clear();
- }
-
- // Remove test classes that belong to changed bundles
- for(String symbolicName : bundlesToUpdate) {
- testClassesMap.remove(symbolicName);
- }
-
- // Get test classes from bundles that are in our list
- for(Bundle b : bundleContext.getBundles()) {
- if(bundlesToUpdate.contains(b.getSymbolicName())) {
- final List<String> testClasses = getTestClasses(b);
- if(testClasses != null) {
- testClassesMap.put(b.getSymbolicName(), testClasses);
- log.debug("{} test classes found in bundle {}, added to
our list",
- testClasses.size(), b.getSymbolicName());
- } else {
- log.debug("No test classes found in bundle {}",
b.getSymbolicName());
- }
+ @Override
+ public Set<String> addingBundle(Bundle bundle, BundleEvent event) {
+ super.addingBundle(bundle, event);
+ if (isFragment(bundle)) {
+ return null;
}
+ final Set<String> testClasses = getTestClasses(bundle);
+ return testClasses.isEmpty() ? null : testClasses;
}
- }
- /** Called when a bundle changes state */
- public void bundleChanged(BundleEvent event) {
- // Only consider bundles which contain tests
- final Bundle b = event.getBundle();
- if(getSlingTestRegexp(b) == null) {
- log.debug("Bundle {} does not have {} header, ignored",
- b.getSymbolicName(), SLING_TEST_REGEXP);
- return;
- }
- synchronized (changedBundles) {
- log.debug("Got BundleEvent for Bundle {}, will rebuild its lists
of tests");
- changedBundles.add(b.getSymbolicName());
- }
- lastModified = System.currentTimeMillis();
- }
-
- private String getSlingTestRegexp(Bundle b) {
- return (String)b.getHeaders().get(SLING_TEST_REGEXP);
- }
-
- /** Get test classes that bundle b provides (as done in Felix/Sigil) */
- private List<String> getTestClasses(Bundle b) {
- final List<String> result = new ArrayList<String>();
- Pattern testClassRegexp = null;
- final String headerValue = getSlingTestRegexp(b);
- if (headerValue != null) {
- try {
- testClassRegexp = Pattern.compile(headerValue);
+ /** Get test classes that bundle b provides (as done in Felix/Sigil) */
+ @NotNull
+ private static Set<String> getTestClasses(Bundle bundle) {
+ final String headerValue = getSlingTestRegexp(bundle);
+ if (headerValue == null) {
+ LOG.debug("Bundle '{}' does not have {} header, not looking
for test classes",
+ bundle.getSymbolicName(), SLING_TEST_REGEXP);
+ return Collections.emptySet();
}
- catch (PatternSyntaxException pse) {
- log.warn("Invalid pattern '" + headerValue + "' for bundle "
- + b.getSymbolicName() + ", ignored", pse);
+
+ Predicate<String> isTestClass;
+ try {
+ isTestClass = Pattern.compile(headerValue).asPredicate();
+ } catch (PatternSyntaxException pse) {
+ LOG.warn("Bundle '{}' has an invalid pattern for {} header,
ignored: '{}'",
+ bundle.getSymbolicName(), SLING_TEST_REGEXP,
headerValue);
+ return Collections.emptySet();
}
- }
-
- if (testClassRegexp == null) {
- log.info("Bundle {} does not have {} header, not looking for test
classes", SLING_TEST_REGEXP);
- } else if (Bundle.ACTIVE != b.getState()) {
- log.info("Bundle {} is not active, no test classes considered",
b.getSymbolicName());
- } else {
- @SuppressWarnings("unchecked")
- Enumeration<URL> classUrls = b.findEntries("", "*.class", true);
+
+ Enumeration<URL> classUrls = bundle.findEntries("", "*.class",
true);
+ final Set<String> result = new LinkedHashSet<>();
while (classUrls.hasMoreElements()) {
URL url = classUrls.nextElement();
final String name = toClassName(url);
- if(testClassRegexp.matcher(name).matches()) {
+ if(isTestClass.test(name)) {
result.add(name);
} else {
- log.debug("Class {} does not match {} pattern {} of bundle
{}, ignored",
- new Object[] { name, SLING_TEST_REGEXP,
testClassRegexp, b.getSymbolicName() });
+ LOG.debug("Class '{}' does not match {} pattern '{}' of
bundle '{}', ignored",
+ name, SLING_TEST_REGEXP, headerValue,
bundle.getSymbolicName());
}
}
- log.info("{} test classes found in bundle {}", result.size(),
b.getSymbolicName());
- }
-
- return result;
- }
-
- /** Convert class URL to class name */
- private String toClassName(URL url) {
- final String f = url.getFile();
- final String cn = f.substring(1, f.length() - ".class".length());
- return cn.replace('/', '.');
- }
- /** Find bundle by symbolic name */
- private Bundle findBundle(String symbolicName) {
- for(Bundle b : bundleContext.getBundles()) {
- if(b.getSymbolicName().equals(symbolicName)) {
- return b;
- }
+ LOG.info("{} test classes found in bundle '{}'", result.size(),
bundle.getSymbolicName());
+ return result;
}
- return null;
- }
-
- public Class<?> createTestClass(String testName) throws
ClassNotFoundException {
- // Find the bundle to which the class belongs
- Bundle b = null;
- for(Map.Entry<String, List<String>> e : testClassesMap.entrySet()) {
- if(e.getValue().contains(testName)) {
- b = findBundle(e.getKey());
- break;
- }
- }
-
- if(b == null) {
- throw new IllegalArgumentException("No Bundle found that supplies
test class " + testName);
- }
- return b.loadClass(testName);
- }
- public long lastModified() {
- return lastModified;
- }
+ private static String getSlingTestRegexp(Bundle bundle) {
+ return bundle.getHeaders().get(SLING_TEST_REGEXP);
+ }
- public String getServicePid() {
- return componentName;
- }
+ /** Convert class URL to class name */
+ private static String toClassName(URL url) {
+ final String f = url.getFile();
+ final String cn = f.substring(1, f.length() - ".class".length());
+ return cn.replace('/', '.');
+ }
- public List<String> getTestNames() {
- maybeUpdateTestClasses();
- final List<String> result = new ArrayList<String>();
- for(List<String> list : testClassesMap.values()) {
- result.addAll(list);
+ private static boolean isFragment(final Bundle bundle) {
+ return bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null;
}
- return result;
}
}
diff --git
a/src/main/java/org/apache/sling/junit/impl/JUnit4TestExecutionStrategy.java
b/src/main/java/org/apache/sling/junit/impl/JUnit4TestExecutionStrategy.java
new file mode 100644
index 0000000..037e221
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/impl/JUnit4TestExecutionStrategy.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.impl;
+
+import org.apache.sling.junit.TestSelector;
+import org.junit.runner.JUnitCore;
+import org.junit.runner.Request;
+import org.junit.runner.notification.RunListener;
+
+public class JUnit4TestExecutionStrategy implements TestExecutionStrategy {
+
+ private final TestsManagerImpl testsManager;
+
+ public JUnit4TestExecutionStrategy(TestsManagerImpl testsManager) {
+ this.testsManager = testsManager;
+ }
+
+ @Override
+ public void execute(TestSelector selector, RunListener runListener) throws
Exception {
+ final JUnitCore junit = new JUnitCore();
+ junit.addListener(runListener);
+ final Request request = testsManager.createTestRequest(selector,
Request::method, Request::classes);
+ junit.run(request);
+ }
+
+ @Override
+ public void close() {
+ // nothing to do
+ }
+}
diff --git
a/src/main/java/org/apache/sling/junit/impl/TestContextRunListenerWrapper.java
b/src/main/java/org/apache/sling/junit/impl/TestContextRunListenerWrapper.java
index 0f264ee..544524b 100644
---
a/src/main/java/org/apache/sling/junit/impl/TestContextRunListenerWrapper.java
+++
b/src/main/java/org/apache/sling/junit/impl/TestContextRunListenerWrapper.java
@@ -28,8 +28,9 @@ public class TestContextRunListenerWrapper extends
RunListener {
private final RunListener wrapped;
private long testStartTime;
private static final Logger log =
LoggerFactory.getLogger(TestContextRunListenerWrapper.class);
-
- TestContextRunListenerWrapper(RunListener toWrap) {
+ private boolean createContext;
+
+ public TestContextRunListenerWrapper(RunListener toWrap) {
wrapped = toWrap;
}
@@ -58,13 +59,21 @@ public class TestContextRunListenerWrapper extends
RunListener {
}
@Override
- public void testRunFinished(Result result) throws Exception {
- wrapped.testRunFinished(result);
+ public void testRunStarted(Description description) throws Exception {
+ // Create a test context if we don't have one yet
+ createContext = !SlingTestContextProvider.hasContext();
+ if(createContext) {
+ SlingTestContextProvider.createContext();
+ }
+ wrapped.testRunStarted(description);
}
@Override
- public void testRunStarted(Description description) throws Exception {
- wrapped.testRunStarted(description);
+ public void testRunFinished(Result result) throws Exception {
+ wrapped.testRunFinished(result);
+ if (createContext) {
+ SlingTestContextProvider.deleteContext();
+ }
}
@Override
@@ -72,4 +81,18 @@ public class TestContextRunListenerWrapper extends
RunListener {
testStartTime = System.currentTimeMillis();
wrapped.testStarted(description);
}
+
+ @Override
+ public void testSuiteStarted(Description description) throws Exception {
+ // If we have a test context, clear its output metadata
+ if (SlingTestContextProvider.hasContext()) {
+ SlingTestContextProvider.getContext().output().clear();
+ }
+ wrapped.testSuiteStarted(description);
+ }
+
+ @Override
+ public void testSuiteFinished(Description description) throws Exception {
+ wrapped.testSuiteFinished(description);
+ }
}
diff --git
a/src/main/java/org/apache/sling/junit/impl/TestExecutionStrategy.java
b/src/main/java/org/apache/sling/junit/impl/TestExecutionStrategy.java
new file mode 100644
index 0000000..3ea034a
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/impl/TestExecutionStrategy.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.impl;
+
+import org.apache.sling.junit.TestSelector;
+import org.junit.runner.notification.RunListener;
+
+import java.io.Closeable;
+
+public interface TestExecutionStrategy extends Closeable {
+
+ void execute(TestSelector selector, RunListener runListener) throws
Exception;
+
+ @Override
+ void close();
+}
diff --git a/src/main/java/org/apache/sling/junit/impl/TestsManagerImpl.java
b/src/main/java/org/apache/sling/junit/impl/TestsManagerImpl.java
index 82b5ab8..80b8f7e 100644
--- a/src/main/java/org/apache/sling/junit/impl/TestsManagerImpl.java
+++ b/src/main/java/org/apache/sling/junit/impl/TestsManagerImpl.java
@@ -16,36 +16,35 @@
*/
package org.apache.sling.junit.impl;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sling.junit.Activator;
import org.apache.sling.junit.Renderer;
-import org.apache.sling.junit.SlingTestContextProvider;
+import org.apache.sling.junit.RequestParser;
import org.apache.sling.junit.TestSelector;
import org.apache.sling.junit.TestsManager;
import org.apache.sling.junit.TestsProvider;
-import org.junit.runner.JUnitCore;
-import org.junit.runner.Request;
+import org.apache.sling.junit.impl.servlet.junit5.JUnit5TestExecutionStrategy;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
-import org.osgi.framework.ServiceReference;
-import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
@Component
public class TestsManagerImpl implements TestsManager {
@@ -54,174 +53,136 @@ public class TestsManagerImpl implements TestsManager {
// Global Timeout up to which it stop waiting for bundles to be all
active, default to 40 seconds.
public static final String PROP_STARTUP_TIMEOUT_SECONDS =
"sling.junit.core.SystemStartupTimeoutSeconds";
- private static volatile int startupTimeoutSeconds =
Integer.parseInt(System.getProperty(PROP_STARTUP_TIMEOUT_SECONDS, "40"));
-
- private static volatile boolean waitForSystemStartup = true;
+ private final int startupTimeoutSeconds =
Integer.parseInt(System.getProperty(PROP_STARTUP_TIMEOUT_SECONDS, "40"));
- private ServiceTracker tracker;
+ private volatile boolean waitForSystemStartup = true;
- private int lastTrackingCount = -1;
+ boolean isReady() {
+ return !waitForSystemStartup;
+ }
private BundleContext bundleContext;
+
+ private ServiceTracker<TestsProvider, TestsProvider> testsProviderTracker;
- // List of providers
- private final List<TestsProvider> providers = new
ArrayList<TestsProvider>();
-
- // Map of test names to their provider's PID
- private Map<String, String> tests = new ConcurrentHashMap<String,
String>();
-
- // Last-modified values for each provider
- private Map<String, Long> lastModified = new HashMap<String, Long>();
-
- protected void activate(ComponentContext ctx) {
- bundleContext = ctx.getBundleContext();
- tracker = new ServiceTracker(bundleContext,
TestsProvider.class.getName(), null);
- tracker.open();
- }
+ private TestExecutionStrategy executionStrategy;
- protected void deactivate(ComponentContext ctx) {
- if(tracker != null) {
- tracker.close();
+ @Activate
+ protected void activate(BundleContext ctx) {
+ bundleContext = ctx;
+ testsProviderTracker = new ServiceTracker<>(bundleContext,
TestsProvider.class, null);
+ testsProviderTracker.open();
+ try {
+ executionStrategy = new JUnit5TestExecutionStrategy(this, ctx);
+ } catch (NoClassDefFoundError e) {
+ // (some) optional imports to org.junit.platform.* (JUnit5 API)
are missing
+ executionStrategy = new JUnit4TestExecutionStrategy(this);
}
- tracker = null;
- bundleContext = null;
}
-
- public void clearCaches() {
- log.debug("Clearing internal caches");
- lastModified.clear();
- lastTrackingCount = -1;
- }
-
- public Class<?> getTestClass(String testName) throws
ClassNotFoundException {
- maybeUpdateProviders();
- // find TestsProvider that can instantiate testName
- final String providerPid = tests.get(testName);
- if(providerPid == null) {
- throw new IllegalStateException("Provider PID not found for test "
+ testName);
+ @Deactivate
+ protected void deactivate() {
+ if(testsProviderTracker != null) {
+ testsProviderTracker.close();
+ testsProviderTracker = null;
}
- TestsProvider provider = null;
- for(TestsProvider p : providers) {
- if(p.getServicePid().equals(providerPid)) {
- provider = p;
- break;
- }
- }
-
- if(provider == null) {
- throw new IllegalStateException("No TestsProvider found for PID "
+ providerPid);
+
+ if (executionStrategy != null) {
+ executionStrategy.close();
+ executionStrategy = null;
}
+ bundleContext = null;
+ }
+
+ @NotNull
+ public Class<?> getTestClass(@NotNull String testName) throws
ClassNotFoundException {
+ final TestsProvider provider = getTestProviders()
+ .filter(p -> p.getTestNames().contains(testName))
+ .findFirst()
+ .orElseThrow(() -> new ClassNotFoundException("No
TestsProvider found for test '" + testName + "'"));
+
log.debug("Using provider {} to create test class {}", provider,
testName);
return provider.createTestClass(testName);
}
- public Collection<String> getTestNames(TestSelector selector) {
- maybeUpdateProviders();
-
- // If any provider has changes, reload the whole list
- // of test names (to keep things simple)
- boolean reload = false;
- for(TestsProvider p : providers) {
- final Long lastMod = lastModified.get(p.getServicePid());
- if(lastMod == null || lastMod != p.lastModified()) {
- reload = true;
- log.debug("{} updated, will reload test names from all
providers", p);
- break;
- }
- }
-
- if(reload) {
- tests.clear();
- for(TestsProvider p : providers) {
- final String pid = p.getServicePid();
- if(pid == null) {
- log.warn("{} has null PID, ignored", p);
- continue;
- }
- lastModified.put(pid, p.lastModified());
- final List<String> names = p.getTestNames();
- for(String name : names) {
- tests.put(name, pid);
- }
- log.debug("Added {} test names from provider {}",
names.size(), p);
- }
- log.info("Test names reloaded, total {} names from {} providers",
tests.size(), providers.size());
- }
-
- final Collection<String> allTests = tests.keySet();
+ @Override
+ public Collection<String> getTestNames(@Nullable TestSelector selector) {
+ final List<String> tests = getTestProviders()
+ .map(TestsProvider::getTestNames)
+ .flatMap(Collection::stream)
+ .sorted()
+ .collect(Collectors.toList());
+ final int allTestsCount = tests.size();
if(selector == null) {
- log.debug("No TestSelector supplied, returning all {} tests",
allTests.size());
- return allTests;
+ log.debug("No TestSelector supplied, returning all {} tests",
allTestsCount);
} else {
- final List<String> result = new LinkedList<String>();
- for(String test : allTests) {
- if(selector.acceptTestName(test)) {
- result.add(test);
- }
- }
- log.debug("{} selected {} tests out of {}", selector,
result.size(), allTests.size());
- return result;
+ tests.removeIf(testName -> !selector.acceptTestName(testName));
+ log.debug("{} selected {} tests out of {}", selector,
tests.size(), allTestsCount);
}
+ return tests;
}
-
- /** Update our list of providers if tracker changed */
- private void maybeUpdateProviders() {
- if(tracker.getTrackingCount() != lastTrackingCount) {
- // List of providers changed, need to reload everything
- lastModified.clear();
- List<TestsProvider> newList = new ArrayList<TestsProvider>();
- for(ServiceReference ref : tracker.getServiceReferences()) {
- newList.add((TestsProvider)bundleContext.getService(ref));
- }
- synchronized (providers) {
- providers.clear();
- providers.addAll(newList);
- }
- log.info("Updated list of TestsProvider: {}", providers);
+
+ private Stream<TestsProvider> getTestProviders() {
+ return testsProviderTracker.getTracked().values().stream();
+ }
+
+ @Override
+ public void executeTests(@Nullable Collection<String> testNames, @NotNull
Renderer renderer, @Nullable TestSelector selector) throws Exception {
+ if (selector != null) {
+ executeTests(renderer, selector);
+ } else if (testNames != null){
+ executeTests(renderer, new RequestParser(null) {
+ @Override
+ public boolean acceptTestName(String testName) {
+ return testNames.contains(testName);
+ }
+ });
+ } else {
+ executeTests(renderer, null);
}
- lastTrackingCount = tracker.getTrackingCount();
}
- public void executeTests(Collection<String> testNames, Renderer renderer,
TestSelector selector) throws Exception {
+ @Override
+ public void executeTests(@NotNull Renderer renderer, @Nullable
TestSelector selector) throws Exception {
renderer.title(2, "Running tests");
waitForSystemStartup();
- final JUnitCore junit = new JUnitCore();
-
- // Create a test context if we don't have one yet
- final boolean createContext = !SlingTestContextProvider.hasContext();
- if(createContext) {
- SlingTestContextProvider.createContext();
+ executionStrategy.execute(selector, new
TestContextRunListenerWrapper(renderer.getRunListener()));
+ }
+
+ public <T> T createTestRequest(TestSelector selector,
+ BiFunction<Class<?>, String, T>
methodRequestFactory,
+ Function<Class<?>[], T> classesRequestFactory)
throws ClassNotFoundException {
+ final T request;
+ final Collection<String> testNames = getTestNames(selector);
+ if (testNames.isEmpty()) {
+ throw new NoTestCasesFoundException();
}
-
- try {
- junit.addListener(new
TestContextRunListenerWrapper(renderer.getRunListener()));
- for(String className : testNames) {
- renderer.title(3, className);
-
- // If we have a test context, clear its output metadata
- if(SlingTestContextProvider.hasContext()) {
- SlingTestContextProvider.getContext().output().clear();
- }
-
- final String testMethodName = selector == null ? null :
selector.getSelectedTestMethodName();
- if(testMethodName != null && testMethodName.length() > 0) {
- log.debug("Running test method {} from test class {}",
testMethodName, className);
- junit.run(Request.method(getTestClass(className),
testMethodName));
- } else {
- log.debug("Running test class {}", className);
- junit.run(getTestClass(className));
- }
+ final String testMethodName = selector == null ? null :
selector.getSelectedTestMethodName();
+ if (testNames.size() == 1 && isNotBlank(testMethodName)) {
+ final String className = testNames.iterator().next();
+ log.debug("Running test method {} from test class {}",
testMethodName, className);
+ request = methodRequestFactory.apply(getTestClass(className),
testMethodName);
+ } else {
+ if (isNotBlank(testMethodName)) {
+ throw new IllegalStateException("A test method name is only
supported for a single test class");
}
- } finally {
- if(createContext) {
- SlingTestContextProvider.deleteContext();
+ final List<Class<?>> testClasses = new ArrayList<>();
+ for (String className : testNames) {
+ log.debug("Running test class {}", className);
+ testClasses.add(getTestClass(className));
}
+ request = classesRequestFactory.apply(testClasses.toArray(new
Class[0]));
}
+ return request;
+ }
+
+ private static boolean isNotBlank(String str) {
+ return str != null && str.length() > 0;
}
- public void listTests(Collection<String> testNames, Renderer renderer) {
+ @Override
+ public void listTests(@NotNull Collection<String> testNames, @NotNull
Renderer renderer) {
renderer.title(2, "Test classes");
final String note = "The test set can be restricted using partial test
names"
+ " as a suffix to this URL"
@@ -230,21 +191,21 @@ public class TestsManagerImpl implements TestsManager {
renderer.list("testNames", testNames);
}
+ @Override
+ public void clearCaches() {
+ // deprecated method kept for backwards compatibility
+ }
/** Wait for all bundles to be started
* @return number of msec taken by this method to execute
*/
- public static long waitForSystemStartup() {
+ long waitForSystemStartup() {
long elapsedMsec = -1;
if (waitForSystemStartup) {
waitForSystemStartup = false;
- final BundleContext bundleContext = Activator.getBundleContext();
- final Set<Bundle> bundlesToWaitFor = new HashSet<Bundle>();
- for (final Bundle bundle : bundleContext.getBundles()) {
- if (bundle.getState() != Bundle.ACTIVE && !isFragment(bundle))
{
- bundlesToWaitFor.add(bundle);
- }
- }
+ final Set<Bundle> bundlesToWaitFor =
Stream.of(bundleContext.getBundles())
+
.filter(not(TestsManagerImpl::isActive).and(not(TestsManagerImpl::isFragment)))
+ .collect(Collectors.toSet());
// wait max inactivityTimeout after the last bundle became active
before giving up
final long startTime = System.currentTimeMillis();
@@ -256,14 +217,7 @@ public class TestsManagerImpl implements TestsManager {
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
- Iterator<Bundle> bundles = bundlesToWaitFor.iterator();
- while (bundles.hasNext()) {
- Bundle bundle = bundles.next();
- if (bundle.getState() == Bundle.ACTIVE) {
- bundles.remove();
- log.debug("Bundle {} is now active",
bundle.getSymbolicName());
- }
- }
+ bundlesToWaitFor.removeIf(TestsManagerImpl::isActive);
}
elapsedMsec = System.currentTimeMillis() - startTime;
@@ -280,11 +234,19 @@ public class TestsManagerImpl implements TestsManager {
return elapsedMsec;
}
- private static boolean needToWait(final long startupTimeout, final
Collection<Bundle> bundlesToWaitFor) {
+ static boolean needToWait(final long startupTimeout, final
Collection<Bundle> bundlesToWaitFor) {
return startupTimeout > System.currentTimeMillis() &&
!bundlesToWaitFor.isEmpty();
}
+ private static <T> Predicate<T> not(Predicate<T> predicate) {
+ return predicate.negate();
+ }
+
private static boolean isFragment(final Bundle bundle) {
return bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null;
}
+
+ private static boolean isActive(Bundle bundle) {
+ return bundle.getState() == Bundle.ACTIVE;
+ }
}
diff --git
a/src/main/java/org/apache/sling/junit/impl/servlet/HtmlRenderer.java
b/src/main/java/org/apache/sling/junit/impl/servlet/HtmlRenderer.java
index c0b2ce0..fc00239 100644
--- a/src/main/java/org/apache/sling/junit/impl/servlet/HtmlRenderer.java
+++ b/src/main/java/org/apache/sling/junit/impl/servlet/HtmlRenderer.java
@@ -168,14 +168,6 @@ public class HtmlRenderer extends RunListener implements
Renderer,RendererFactor
@Override
public void testRunFinished(Result result) throws Exception {
super.testRunFinished(result);
- String cssClass = "testRun ";
- if(result.getFailureCount() > 0) {
- cssClass += "failure";
- } else if(result.getIgnoreCount() > 0) {
- cssClass += "ignored";
- } else {
- cssClass += "success";
- }
output.println("<p class='testRun'>");
output.print("TEST RUN FINISHED: ");
@@ -188,9 +180,11 @@ public class HtmlRenderer extends RunListener implements
Renderer,RendererFactor
}
@Override
- public void testRunStarted(Description description)
- throws Exception {
- super.testRunStarted(description);
+ public void testSuiteStarted(Description description) throws Exception {
+ super.testSuiteStarted(description);
+ if (description.getTestClass() != null) {
+ title(3, description.getClassName());
+ }
}
@Override
diff --git
a/src/main/java/org/apache/sling/junit/impl/servlet/PlainTextRenderer.java
b/src/main/java/org/apache/sling/junit/impl/servlet/PlainTextRenderer.java
index 937bc72..d881597 100644
--- a/src/main/java/org/apache/sling/junit/impl/servlet/PlainTextRenderer.java
+++ b/src/main/java/org/apache/sling/junit/impl/servlet/PlainTextRenderer.java
@@ -37,7 +37,7 @@ import org.osgi.service.component.annotations.Component;
public class PlainTextRenderer extends RunListener implements Renderer,
RendererFactory {
public static final String EXTENSION = "txt";
private PrintWriter output;
-
+
public Renderer createRenderer() {
return new PlainTextRenderer();
}
@@ -122,13 +122,10 @@ public class PlainTextRenderer extends RunListener
implements Renderer, Renderer
}
@Override
- public void testRunStarted(Description description)
- throws Exception {
- super.testRunStarted(description);
- }
-
- @Override
- public void testStarted(Description description) throws Exception {
- super.testStarted(description);
+ public void testSuiteStarted(Description description) throws Exception {
+ super.testSuiteStarted(description);
+ if (description.getTestClass() != null) {
+ title(3, description.getClassName());
+ }
}
}
diff --git
a/src/main/java/org/apache/sling/junit/impl/servlet/ServletProcessor.java
b/src/main/java/org/apache/sling/junit/impl/servlet/ServletProcessor.java
index adc3ce3..170ae60 100644
--- a/src/main/java/org/apache/sling/junit/impl/servlet/ServletProcessor.java
+++ b/src/main/java/org/apache/sling/junit/impl/servlet/ServletProcessor.java
@@ -19,9 +19,7 @@ package org.apache.sling.junit.impl.servlet;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
+import java.util.Collection;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@@ -52,19 +50,6 @@ public class ServletProcessor {
this.rendererSelector = rendererSelector;
}
- /** Return sorted list of available tests
- * @param prefix optionally select only names that match this prefix
- */
- private List<String> getTestNames(TestSelector selector, boolean
forceReload) {
- final List<String> result = new LinkedList<String>();
- if(forceReload) {
- log.debug("{} is true, clearing TestsManager caches",
FORCE_RELOAD_PARAM);
- }
- result.addAll(testsManager.getTestNames(selector));
- Collections.sort(result);
- return result;
- }
-
private void sendCss(HttpServletResponse response) throws IOException {
final InputStream str = getClass().getResourceAsStream("/" + CSS);
if(str == null) {
@@ -81,16 +66,17 @@ public class ServletProcessor {
}
}
- private boolean getForceReloadOption(HttpServletRequest request) {
- final boolean forceReload =
"true".equalsIgnoreCase(request.getParameter(FORCE_RELOAD_PARAM));
- log.debug("{} option is set to {}", FORCE_RELOAD_PARAM, forceReload);
- return forceReload;
+ private void logForceReloadOptionDeprecation(HttpServletRequest request) {
+ final String forceReloadParam =
request.getParameter(FORCE_RELOAD_PARAM);
+ if (forceReloadParam != null) {
+ log.info("{} option is no longer necessary and its use is
therefore deprecated", FORCE_RELOAD_PARAM);
+ }
}
/** GET request lists available tests */
public void doGet(final HttpServletRequest request, final
HttpServletResponse response, final String servletPath)
throws ServletException, IOException {
- final boolean forceReload = getForceReloadOption(request);
+ logForceReloadOptionDeprecation(request);
// Redirect to / if called without it, and serve CSS if requested
{
@@ -104,8 +90,8 @@ public class ServletProcessor {
}
final TestSelector selector = getTestSelector(request);
- final List<String> testNames = getTestNames(selector, forceReload);
-
+ final Collection<String> testNames =
testsManager.getTestNames(selector);
+
// 404 if no tests found
if(testNames.isEmpty()) {
final String msg =
@@ -137,10 +123,10 @@ public class ServletProcessor {
/** POST request executes tests */
public void doPost(HttpServletRequest request, HttpServletResponse
response)
throws ServletException, IOException {
+ logForceReloadOptionDeprecation(request);
+
final TestSelector selector = getTestSelector(request);
- final boolean forceReload = getForceReloadOption(request);
- log.info("POST request, executing tests: {}, {}={}",
- new Object[] { selector, FORCE_RELOAD_PARAM, forceReload});
+ log.info("POST request, executing tests: {}", selector);
final Renderer renderer = rendererSelector.getRenderer(selector);
if(renderer == null) {
@@ -148,15 +134,14 @@ public class ServletProcessor {
}
renderer.setup(response, getClass().getSimpleName());
- final List<String> testNames = getTestNames(selector, forceReload);
- if(testNames.isEmpty()) {
+ try {
+ testsManager.executeTests(renderer, selector);
+ } catch(TestsManager.NoTestCasesFoundException e) {
response.sendError(
HttpServletResponse.SC_NOT_FOUND,
"No tests found for " + selector);
- }
- try {
- testsManager.executeTests(testNames, renderer, selector);
- } catch(Exception e) {
+ return;
+ } catch (Exception e) {
throw new ServletException(e);
}
diff --git a/src/main/java/org/apache/sling/junit/impl/servlet/XmlRenderer.java
b/src/main/java/org/apache/sling/junit/impl/servlet/XmlRenderer.java
index 50a297b..52eaa4e 100644
--- a/src/main/java/org/apache/sling/junit/impl/servlet/XmlRenderer.java
+++ b/src/main/java/org/apache/sling/junit/impl/servlet/XmlRenderer.java
@@ -250,6 +250,14 @@ public class XmlRenderer extends RunListener implements
Renderer, RendererFactor
}
@Override
+ public void testSuiteStarted(Description description) throws Exception {
+ super.testSuiteStarted(description);
+ if (description.getTestClass() != null) {
+ title(3, description.getClassName());
+ }
+ }
+
+ @Override
public void testStarted(Description description) throws Exception {
super.testStarted(description);
tests.put(description, new Long(System.currentTimeMillis()));
diff --git
a/src/main/java/org/apache/sling/junit/impl/servlet/junit5/DescriptionGenerator.java
b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/DescriptionGenerator.java
new file mode 100644
index 0000000..bf3a56b
--- /dev/null
+++
b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/DescriptionGenerator.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.impl.servlet.junit5;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.platform.engine.TestSource;
+import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.engine.support.descriptor.MethodSource;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.runner.Description;
+
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.function.Function;
+
+public enum DescriptionGenerator {
+
+ CLASS_SOURCE(ClassSource.class, src ->
Description.createSuiteDescription(src.getJavaClass())),
+
+ METHOD_SOURCE(MethodSource.class, src ->
Description.createTestDescription(src.getClassName(), src.getMethodName()))
+
+ ;
+
+ private final Class<? extends TestSource> clazz;
+
+ private final Function<? super TestSource, Description> generator;
+
+ @SuppressWarnings("unchecked")
+ <T extends TestSource> DescriptionGenerator(Class<T> clazz, Function<?
super T, Description> generator) {
+ this.clazz = clazz;
+ this.generator = (Function<? super TestSource, Description>) generator;
+ }
+
+ @NotNull
+ public static Optional<Description> toDescription(TestIdentifier
testIdentifier) {
+ return
testIdentifier.getSource().map(DescriptionGenerator::createDescription);
+ }
+
+ static Description createDescription(TestSource testSource) {
+ if (testSource != null) {
+ return Arrays.stream(values())
+ .filter(v -> v.clazz.isInstance(testSource))
+ .map(v -> v.generator.apply(testSource))
+ .findFirst()
+ .orElse(null);
+ }
+ return null;
+ }
+}
diff --git
a/src/main/java/org/apache/sling/junit/impl/servlet/junit5/FailureHelper.java
b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/FailureHelper.java
new file mode 100644
index 0000000..e562e91
--- /dev/null
+++
b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/FailureHelper.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.impl.servlet.junit5;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+import org.junit.runner.notification.Failure;
+
+import static
org.apache.sling.junit.impl.servlet.junit5.DescriptionGenerator.toDescription;
+
+public final class FailureHelper {
+
+ @Nullable
+ public static Failure convert(TestIdentifier testIdentifier, Throwable
throwable) {
+ return toDescription(testIdentifier)
+ .map(d -> new Failure(d, throwable))
+ .orElse(null);
+ }
+
+ @Nullable
+ public static Failure convert(@NotNull TestExecutionSummary.Failure f) {
+ return convert(f.getTestIdentifier(), f.getException());
+ }
+
+ private FailureHelper() {}
+}
diff --git
a/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnit5TestExecutionStrategy.java
b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnit5TestExecutionStrategy.java
new file mode 100644
index 0000000..89a6a1e
--- /dev/null
+++
b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnit5TestExecutionStrategy.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.impl.servlet.junit5;
+
+import org.apache.sling.junit.TestSelector;
+import org.apache.sling.junit.impl.TestExecutionStrategy;
+import org.apache.sling.junit.impl.TestsManagerImpl;
+import org.junit.platform.engine.DiscoverySelector;
+import org.junit.platform.engine.discovery.DiscoverySelectors;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.core.LauncherConfig;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import org.junit.platform.launcher.core.LauncherFactory;
+import org.junit.runner.notification.RunListener;
+import org.osgi.framework.BundleContext;
+
+import java.util.stream.Stream;
+
+import static
org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
+
+public class JUnit5TestExecutionStrategy implements TestExecutionStrategy {
+
+ private final TestsManagerImpl testsManager;
+
+ private final TestEngineTracker testEngineTracker;
+
+ public JUnit5TestExecutionStrategy(TestsManagerImpl testsManager,
BundleContext ctx) {
+ this.testsManager = testsManager;
+ testEngineTracker = new TestEngineTracker(ctx);
+ }
+
+ @Override
+ public void close() {
+ testEngineTracker.close();
+ }
+
+ @Override
+ public void execute(TestSelector selector, RunListener runListener) throws
Exception {
+ Launcher launcher = LauncherFactory.create(
+ LauncherConfig.builder()
+
.addTestEngines(testEngineTracker.getAvailableTestEngines())
+ .addTestExecutionListeners(new
RunListenerAdapter(runListener))
+ .enableTestEngineAutoRegistration(false)
+ .enableTestExecutionListenerAutoRegistration(false)
+ .build()
+ );
+
+ final LauncherDiscoveryRequest request =
+ testsManager.createTestRequest(selector, this::methodRequest,
this::classesRequest);
+ launcher.execute(request);
+ }
+
+ private LauncherDiscoveryRequest methodRequest(Class<?> testClass, String
testMethodName) {
+ return LauncherDiscoveryRequestBuilder.request()
+ .selectors(selectMethod(testClass, testMethodName))
+ .build();
+ }
+
+ private LauncherDiscoveryRequest classesRequest(Class<?>[] testClasses) {
+ final DiscoverySelector[] selectors = Stream.of(testClasses)
+ .map(DiscoverySelectors::selectClass)
+ .toArray(DiscoverySelector[]::new);
+ return LauncherDiscoveryRequestBuilder.request()
+ .selectors(selectors)
+ .build();
+ }
+}
diff --git
a/src/main/java/org/apache/sling/junit/impl/servlet/junit5/ResultAdapter.java
b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/ResultAdapter.java
new file mode 100644
index 0000000..8119527
--- /dev/null
+++
b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/ResultAdapter.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.impl.servlet.junit5;
+
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+import org.junit.runner.Result;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class ResultAdapter extends Result {
+
+ private final transient TestExecutionSummary summary;
+
+ public ResultAdapter(TestExecutionSummary summary) {
+ this.summary = summary;
+ }
+
+ @Override
+ public int getRunCount() {
+ return (int) summary.getTestsStartedCount();
+ }
+
+ @Override
+ public int getFailureCount() {
+ return (int) summary.getTestsFailedCount();
+ }
+
+ @Override
+ public long getRunTime() {
+ return summary.getTimeFinished() - summary.getTimeStarted();
+ }
+
+ @Override
+ public List<Failure> getFailures() {
+ return summary.getFailures().stream()
+ .map(FailureHelper::convert)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public int getIgnoreCount() {
+ return (int) summary.getTestsSkippedCount();
+ }
+
+ @Override
+ public int getAssumptionFailureCount() {
+ return 0;
+ }
+
+ @Override
+ public boolean wasSuccessful() {
+ return summary.getTestsFailedCount() == 0;
+ }
+
+ @Override
+ public RunListener createListener() {
+ throw new UnsupportedOperationException("createListener is not
implemented");
+ }
+}
diff --git
a/src/main/java/org/apache/sling/junit/impl/servlet/junit5/RunListenerAdapter.java
b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/RunListenerAdapter.java
new file mode 100644
index 0000000..67c73fc
--- /dev/null
+++
b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/RunListenerAdapter.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.impl.servlet.junit5;
+
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.engine.reporting.ReportEntry;
+import org.junit.platform.launcher.TestExecutionListener;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+import org.junit.runner.notification.RunListener;
+
+import java.util.function.Consumer;
+
+import static
org.apache.sling.junit.impl.servlet.junit5.DescriptionGenerator.toDescription;
+
+public class RunListenerAdapter implements TestExecutionListener {
+
+ private final RunListener runListener;
+
+ private final SummaryGeneratingListener summarizer;
+
+ public RunListenerAdapter(RunListener runListener) {
+ this.runListener = runListener;
+ this.summarizer = new SummaryGeneratingListener();
+ }
+
+ @Override
+ public void testPlanExecutionStarted(TestPlan testPlan) {
+ summarizer.testPlanExecutionStarted(testPlan);
+ try {
+
runListener.testRunStarted(Description.createSuiteDescription("classes"));
+ } catch (Exception exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+
+ @Override
+ public void testPlanExecutionFinished(TestPlan testPlan) {
+ summarizer.testPlanExecutionFinished(testPlan);
+
+ final TestExecutionSummary summary = summarizer.getSummary();
+
+ final Result result = new ResultAdapter(summary);
+
+ try {
+ runListener.testRunFinished(result);
+ } catch (Exception exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+
+ @Override
+ public void executionStarted(TestIdentifier testIdentifier) {
+ summarizer.executionStarted(testIdentifier);
+ if (testIdentifier.isTest()) {
+ withDescription(testIdentifier, runListener::testStarted);
+ } else {
+ withDescription(testIdentifier, runListener::testSuiteStarted);
+ }
+ }
+
+ @Override
+ public void executionSkipped(TestIdentifier testIdentifier, String reason)
{
+ summarizer.executionSkipped(testIdentifier, reason);
+ if (testIdentifier.isTest()) {
+ withDescription(testIdentifier, runListener::testIgnored);
+ }
+ }
+
+ @Override
+ public void executionFinished(TestIdentifier testIdentifier,
TestExecutionResult testExecutionResult) {
+ summarizer.executionFinished(testIdentifier, testExecutionResult);
+ if (testIdentifier.isTest()) {
+ if (testExecutionResult.getStatus() !=
TestExecutionResult.Status.SUCCESSFUL) {
+ try {
+
runListener.testFailure(FailureHelper.convert(testIdentifier,
testExecutionResult.getThrowable().orElse(null)));
+ } catch (Exception exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+ withDescription(testIdentifier, runListener::testFinished);
+ } else {
+ withDescription(testIdentifier, runListener::testSuiteFinished);
+
+ }
+ }
+
+ @Override
+ public void dynamicTestRegistered(TestIdentifier testIdentifier) {
+ summarizer.dynamicTestRegistered(testIdentifier);
+ }
+
+ @Override
+ public void reportingEntryPublished(TestIdentifier testIdentifier,
ReportEntry entry) {
+ summarizer.reportingEntryPublished(testIdentifier, entry);
+ }
+
+ private static void withDescription(TestIdentifier testIdentifier,
ExceptionHandlingConsumer<Description, Exception> action) {
+ toDescription(testIdentifier).ifPresent(action);
+ }
+
+ private interface ExceptionHandlingConsumer<S, E extends Exception>
extends Consumer<S> {
+ @Override
+ default void accept(S s) {
+ try {
+ acceptAndThrow(s);
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+
+ void acceptAndThrow(S s) throws E;
+ }
+}
diff --git
a/src/main/java/org/apache/sling/junit/impl/servlet/junit5/TestEngineTracker.java
b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/TestEngineTracker.java
new file mode 100644
index 0000000..c2ddad6
--- /dev/null
+++
b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/TestEngineTracker.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.impl.servlet.junit5;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.platform.engine.TestEngine;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.wiring.BundleWiring;
+import org.osgi.util.tracker.BundleTracker;
+import org.osgi.util.tracker.BundleTrackerCustomizer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.ServiceLoader;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class TestEngineTracker implements Closeable {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(TestEngineTracker.class);
+
+ private final BundleTracker<AtomicReference<Set<TestEngine>>> tracker;
+
+ public TestEngineTracker(BundleContext bundleContext) {
+ tracker = new BundleTracker<>(bundleContext, Bundle.ACTIVE, new
Customizer());
+ tracker.open();
+ }
+
+ public TestEngine[] getAvailableTestEngines() {
+ return tracker.getTracked().values().stream()
+ .map(AtomicReference::get)
+ .flatMap(Collection::stream)
+ .toArray(TestEngine[]::new);
+ }
+
+ @Override
+ public void close() {
+ tracker.close();
+ }
+
+ private static class Customizer implements
BundleTrackerCustomizer<AtomicReference<Set<TestEngine>>> {
+
+ @Override
+ public AtomicReference<Set<TestEngine>> addingBundle(Bundle bundle,
BundleEvent event) {
+ return new AtomicReference<>(getTestEnginesForBundle(bundle));
+ }
+
+ @Override
+ public void modifiedBundle(Bundle bundle, BundleEvent event,
AtomicReference<Set<TestEngine>> testEngines) {
+ testEngines.set(getTestEnginesForBundle(bundle));
+ }
+
+ @Override
+ public void removedBundle(Bundle bundle, BundleEvent event,
AtomicReference<Set<TestEngine>> testEngines) {
+ testEngines.set(Collections.emptySet());
+ }
+
+ @NotNull
+ private static Set<TestEngine> getTestEnginesForBundle(Bundle bundle) {
+ final Set<TestEngine> testEngines = new HashSet<>();
+ ServiceLoader
+ .load(TestEngine.class,
bundle.adapt(BundleWiring.class).getClassLoader())
+ .forEach(testEngine -> {
+ LOG.info("Found TestEngine '{}' in bundle '{}'",
testEngine.getId(), bundle.getSymbolicName());
+ testEngines.add(testEngine);
+ });
+ return testEngines;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/junit/package-info.java
b/src/main/java/org/apache/sling/junit/package-info.java
index 374ef48..64790d9 100644
--- a/src/main/java/org/apache/sling/junit/package-info.java
+++ b/src/main/java/org/apache/sling/junit/package-info.java
@@ -16,7 +16,7 @@
~ specific language governing permissions and limitations
~ under the License.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
-@Version("1.1.1")
+@Version("1.2.0")
package org.apache.sling.junit;
import org.osgi.annotation.versioning.Version;
diff --git a/src/main/resources/junit.css b/src/main/resources/junit.css
index 74c6987..9986af3 100644
--- a/src/main/resources/junit.css
+++ b/src/main/resources/junit.css
@@ -43,6 +43,9 @@ h2,h3,h4 {
padding: 1em;
border: solid red 1px;
background-color: #FFFFCC;
+ white-space: pre;
+ max-height: 20.5em;
+ overflow-y: scroll;
}
.failure h3 {
diff --git
a/src/test/java/org/apache/sling/junit/impl/JUnit4TestExecutionStrategyTest.java
b/src/test/java/org/apache/sling/junit/impl/JUnit4TestExecutionStrategyTest.java
new file mode 100644
index 0000000..d4ff5da
--- /dev/null
+++
b/src/test/java/org/apache/sling/junit/impl/JUnit4TestExecutionStrategyTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.impl;
+
+import org.apache.sling.junit.TestSelector;
+import org.apache.sling.junit.sampletests.JUnit4SlingJUnit;
+import org.junit.Test;
+import org.junit.runner.Request;
+import org.junit.runner.Result;
+import org.junit.runner.notification.RunListener;
+
+import java.util.Objects;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class JUnit4TestExecutionStrategyTest {
+
+ @Test
+ public void testExecution() throws Exception {
+ final Request request = Request.method(JUnit4SlingJUnit.class,
"testSuccessful");
+ final TestsManagerImpl testsManager = mock(TestsManagerImpl.class);
+ when(testsManager.createTestRequest(any(), any(),
any())).thenReturn(request);
+ final JUnit4TestExecutionStrategy strategy = new
JUnit4TestExecutionStrategy(testsManager);
+ final RunListener runListener = mock(RunListener.class);
+ strategy.execute(mock(TestSelector.class), runListener);
+ verify(runListener, times(1))
+ .testRunStarted(any());
+ verify(runListener, times(1))
+ .testSuiteStarted(argThat(desc ->
Objects.equals(desc.getClassName(), JUnit4SlingJUnit.class.getName())));
+ verify(runListener, times(1))
+ .testStarted(argThat(desc ->
Objects.equals(desc.getMethodName(), "testSuccessful")));
+ verify(runListener, times(1))
+ .testFinished(argThat(desc ->
Objects.equals(desc.getMethodName(), "testSuccessful")));
+ verify(runListener, times(1))
+ .testSuiteFinished(argThat(desc ->
Objects.equals(desc.getClassName(), JUnit4SlingJUnit.class.getName())));
+ verify(runListener, times(1))
+ .testRunFinished(argThat(Result::wasSuccessful));
+ verifyNoMoreInteractions(runListener);
+
+ strategy.close();
+ }
+}
\ No newline at end of file
diff --git
a/src/test/java/org/apache/sling/junit/impl/TestsManagerImplTest.java
b/src/test/java/org/apache/sling/junit/impl/TestsManagerImplTest.java
index 8b394f4..9cb9ee1 100644
--- a/src/test/java/org/apache/sling/junit/impl/TestsManagerImplTest.java
+++ b/src/test/java/org/apache/sling/junit/impl/TestsManagerImplTest.java
@@ -16,98 +16,370 @@
*/
package org.apache.sling.junit.impl;
-import static junit.framework.TestCase.assertFalse;
-import static junit.framework.TestCase.assertTrue;
-import static org.powermock.api.mockito.PowerMockito.mock;
-import static org.powermock.api.mockito.PowerMockito.when;
+import net.bytebuddy.ByteBuddy;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.dynamic.scaffold.InstrumentedType;
+import net.bytebuddy.implementation.DefaultMethodCall;
+import net.bytebuddy.implementation.Implementation;
+import net.bytebuddy.implementation.MethodCall;
+import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
+import net.bytebuddy.matcher.ElementMatchers;
+import org.apache.sling.junit.Renderer;
+import org.apache.sling.junit.RequestParser;
+import org.apache.sling.junit.TestsManager;
+import org.apache.sling.junit.TestsProvider;
+import org.apache.sling.junit.impl.servlet.PlainTextRenderer;
+import org.apache.sling.junit.sampletests.JUnit4SlingJUnit;
+import org.hamcrest.Matchers;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.runner.notification.RunListener;
+import org.junit.vintage.engine.VintageTestEngine;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.wiring.BundleWiring;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Dictionary;
+import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
+import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
-import org.apache.sling.junit.Activator;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mockito;
-import org.osgi.framework.Bundle;
-import org.osgi.framework.BundleContext;
-import org.powermock.api.mockito.PowerMockito;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
-import org.powermock.reflect.Whitebox;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singletonList;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
/**
* Validate waitForSystemStartup method, along with private some
implementations.
*/
-@RunWith(PowerMockRunner.class)
-@PrepareForTest({ Activator.class, TestsManagerImpl.class })
public class TestsManagerImplTest {
- private static final String WAIT_METHOD_NAME = "needToWait";
- private static final int SYSTEM_STARTUP_SECONDS = 2;
-
- static {
- // Set a short timeout so our tests can run faster
- System.setProperty("sling.junit.core.SystemStartupTimeoutSeconds",
String.valueOf(SYSTEM_STARTUP_SECONDS));
- }
-
- /**
- * case if needToWait should return true, mainly it still have some bundles
in the list to wait, and global timeout didn't pass.
- */
- @Test
- public void needToWaitPositiveNotEmptyListNotGloballyTimeout() throws
Exception {
- long startupTimeout = System.currentTimeMillis() +
TimeUnit.SECONDS.toMillis(5 * SYSTEM_STARTUP_SECONDS);
- final Set<Bundle> bundlesToWaitFor = new HashSet<Bundle>();
- bundlesToWaitFor.add(Mockito.mock(Bundle.class));
- assertTrue((Boolean)Whitebox.invokeMethod(TestsManagerImpl.class,
WAIT_METHOD_NAME, startupTimeout, bundlesToWaitFor));
- }
-
- /**
- * case if needToWait should return false, when for example it reached the
global timeout limit.
- */
- @Test
- public void needToWaitNegativeForstartupTimeout() throws Exception {
- long lastChange = System.currentTimeMillis() -
TimeUnit.SECONDS.toMillis(SYSTEM_STARTUP_SECONDS / 2);
- long startupTimeout = lastChange - TimeUnit.SECONDS.toMillis(1);
- assertFalse((Boolean)Whitebox.invokeMethod(TestsManagerImpl.class,
WAIT_METHOD_NAME, startupTimeout, new HashSet<Bundle>()));
- }
-
- /**
- * case if needToWait should return false, when for example it reached the
global timeout limit.
- */
- @Test
- public void needToWaitNegativeForEmptyList() throws Exception {
- long lastChange = System.currentTimeMillis() -
TimeUnit.SECONDS.toMillis(SYSTEM_STARTUP_SECONDS / 2);
- long startupTimeout = lastChange + TimeUnit.SECONDS.toMillis(10);
- assertFalse((Boolean)Whitebox.invokeMethod(TestsManagerImpl.class,
WAIT_METHOD_NAME, startupTimeout, new HashSet<Bundle>()));
- }
-
- @Test
- public void waitForSystemStartupTimeout() {
- setupBundleContextMock(Bundle.INSTALLED);
- final long elapsed = TestsManagerImpl.waitForSystemStartup();
- assertTrue(elapsed > TimeUnit.SECONDS.toMillis(SYSTEM_STARTUP_SECONDS));
- assertTrue(elapsed < TimeUnit.SECONDS.toMillis(SYSTEM_STARTUP_SECONDS +
1));
- assertFalse((Boolean) Whitebox.getInternalState(TestsManagerImpl.class,
"waitForSystemStartup"));
- }
-
- @Test
- public void waitForSystemStartupAllActiveBundles() {
- setupBundleContextMock(Bundle.ACTIVE);
- final long elapsed = TestsManagerImpl.waitForSystemStartup();
- assertTrue(elapsed < TimeUnit.SECONDS.toMillis(SYSTEM_STARTUP_SECONDS));
- assertFalse((Boolean) Whitebox.getInternalState(TestsManagerImpl.class,
"waitForSystemStartup"));
- }
-
- private void setupBundleContextMock(final int bundleState) {
- PowerMockito.mockStatic(Activator.class);
- BundleContext mockedBundleContext = mock(BundleContext.class);
- Bundle mockedBundle = mock(Bundle.class);
- Hashtable<String, String> bundleHeaders = new Hashtable<String, String>();
- when(mockedBundle.getState()).thenReturn(bundleState);
- when(mockedBundle.getHeaders()).thenReturn(bundleHeaders);
- when(mockedBundleContext.getBundles()).thenReturn(new Bundle[] {
mockedBundle });
- when(Activator.getBundleContext()).thenReturn(mockedBundleContext);
- Whitebox.setInternalState(TestsManagerImpl.class, "waitForSystemStartup",
true);
- }
+ private static final int SYSTEM_STARTUP_SECONDS = 2;
+
+ private Set<Bundle> mockBundles = new HashSet<>();
+
+ static {
+ // Set a short timeout so our tests can run faster
+ System.setProperty("sling.junit.core.SystemStartupTimeoutSeconds",
String.valueOf(SYSTEM_STARTUP_SECONDS));
+ }
+
+ /**
+ * case if needToWait should return true, mainly it still have some
bundles in the list to wait, and global timeout didn't pass.
+ */
+ @Test
+ public void needToWaitPositiveNotEmptyListNotGloballyTimeout() {
+ long startupTimeout = System.currentTimeMillis() +
TimeUnit.SECONDS.toMillis(5 * SYSTEM_STARTUP_SECONDS);
+ final Set<Bundle> bundlesToWaitFor = new
HashSet<>(singletonList(mock(Bundle.class)));
+ assertTrue(TestsManagerImpl.needToWait(startupTimeout,
bundlesToWaitFor));
+ }
+
+ /**
+ * case if needToWait should return false, when for example it reached the
global timeout limit.
+ */
+ @Test
+ public void needToWaitNegativeForstartupTimeout() {
+ long lastChange = System.currentTimeMillis() -
TimeUnit.SECONDS.toMillis(SYSTEM_STARTUP_SECONDS / 2);
+ long startupTimeout = lastChange - TimeUnit.SECONDS.toMillis(1);
+ assertFalse(TestsManagerImpl.needToWait(startupTimeout, emptySet()));
+ }
+
+ /**
+ * case if needToWait should return false, when for example it reached the
global timeout limit.
+ */
+ @Test
+ public void needToWaitNegativeForEmptyList() {
+ long lastChange = System.currentTimeMillis() -
TimeUnit.SECONDS.toMillis(SYSTEM_STARTUP_SECONDS / 2);
+ long startupTimeout = lastChange + TimeUnit.SECONDS.toMillis(10);
+ assertFalse(TestsManagerImpl.needToWait(startupTimeout, emptySet()));
+ }
+
+ @Test
+ public void waitForSystemStartupTimeout() {
+ BundleContext bundleContext = setupBundleContext(Bundle.INSTALLED);
+ TestsManagerImpl testsManager = new TestsManagerImpl();
+ testsManager.activate(bundleContext);
+
+ assertFalse(testsManager.isReady());
+
+ final long elapsed = testsManager.waitForSystemStartup();
+ assertTrue(elapsed >
TimeUnit.SECONDS.toMillis(SYSTEM_STARTUP_SECONDS));
+ assertTrue(elapsed < TimeUnit.SECONDS.toMillis(SYSTEM_STARTUP_SECONDS
+ 1));
+ assertTrue(testsManager.isReady());
+
+ // second call is instantaneous
+ assertTrue(10 > testsManager.waitForSystemStartup());
+
+ testsManager.deactivate();
+ }
+
+ @Test
+ public void waitForSystemStartupAllActiveBundles() {
+ BundleContext bundleContext = setupBundleContext(Bundle.ACTIVE);
+ TestsManagerImpl testsManager = new TestsManagerImpl();
+ testsManager.activate(bundleContext);
+
+ assertFalse(testsManager.isReady());
+
+ final long elapsed = testsManager.waitForSystemStartup();
+ assertTrue(elapsed <
TimeUnit.SECONDS.toMillis(SYSTEM_STARTUP_SECONDS));
+ assertTrue(testsManager.isReady());
+
+ testsManager.deactivate();
+ }
+
+ @Test
+ public void testDeactivateBeforeActivateIgnored() {
+ try {
+ new TestsManagerImpl().deactivate();
+ } catch (Exception e) {
+ fail("deactivate before activate should be a no-op");
+ }
+ }
+
+ private BundleContext setupBundleContext(int state) {
+ final Bundle bundle = mock(Bundle.class);
+ when(bundle.getSymbolicName()).thenReturn("mocked-bundle");
+ when(bundle.getState()).thenReturn(state);
+
when(bundle.adapt(BundleWiring.class)).thenReturn(mock(BundleWiring.class));
+ when(bundle.getHeaders()).thenReturn(new Hashtable<>());
+
+ final BundleContext bundleContext = mock(BundleContext.class);
+ when(bundleContext.getBundle()).thenReturn(bundle);
+ when(bundleContext.getBundles()).thenAnswer(m -> new Bundle[]{bundle});
+
+ when(bundle.getBundleContext()).thenReturn(bundleContext);
+ return bundleContext;
+ }
+
+
+ @Test
+ public void testGettingTestNamesAndClassesAndExecution() throws Exception {
+
+ final ArrayList<String> allTestClasses = new ArrayList<>();
+
+ for (int i = 0; i < 5; i++) {
+
+ final List<String> testClasses = asList(
+ "org.apache.sling.junit.testbundle" + i + ".ASlingJUnit",
+ "org.apache.sling.junit.testbundle" + i +
".impl.ANestedSlingJUnit"
+ );
+
+ allTestClasses.addAll(testClasses);
+
+ final List<String> nonTestClasses = asList(
+ "org.apache.sling.junit.testbundle" + i + ".NotATest",
+ "org.apache.sling.junit.testbundle" + i +
".impl.AlsoNotATest",
+ "org.apache.sling.junit.testbundle" + i +
".CompletelyUnrelated"
+ );
+
+ final List<String> classes = new ArrayList<>();
+ classes.addAll(testClasses);
+ classes.addAll(nonTestClasses);
+ classes.sort(Comparator.naturalOrder());
+
+ createTestBundle(
+ "test-bundle-" + i,
+ "org.apache.sling.junit.testbundle" + i + ".*SlingJUnit",
+ classes
+ );
+ }
+
+ createTestBundle("test-bundle-no-tests",
"org.apache.sling.junit.notests.*SlingJUnit", emptyList());
+ createTestBundle("test-bundle-invalid-regexp", "[a-z", emptyList());
+ createTestBundle("test-bundle-no-regexp", null, emptyList());
+ createFragmentBundle("fragment");
+
+ final Bundle junitBundle = createMockBundle("junit-bundle",
Bundle.ACTIVE);
+ addBundleWiring(junitBundle, VintageTestEngine.class.getClassLoader());
+ final BundleContext bundleContext = junitBundle.getBundleContext();
+ final BundleTestsProvider bundleTestsProvider =
+ activateAndRegister(bundleContext, TestsProvider.class, new
BundleTestsProvider(), BundleTestsProvider::activate);
+ final TestsManagerImpl testsManager =
+ activateAndRegister(bundleContext, TestsManager.class, new
TestsManagerImpl(), TestsManagerImpl::activate);
+
+ final RequestParser selector = new RequestParser(null);
+ final Collection<String> testNames =
testsManager.getTestNames(selector);
+
+ assertThat("should find all tests", testNames,
Matchers.containsInAnyOrder(allTestClasses.toArray(new String[0])));
+
+ for (String testName : testNames) {
+ assertThat("should be able to load class " + testName,
testsManager.getTestClass(testName), Matchers.isA(Class.class));
+ }
+
+ try {
+ testsManager.getTestClass("a.class.that.does.not.Exist");
+ fail("should not load non-existant test class");
+ } catch (ClassNotFoundException e) {
+ // expected
+ }
+
+ testsManager.executeTests(createRenderer(), null);
+ testsManager.executeTests(createRenderer(), new
RequestParser("org.apache.sling.junit.testbundle0.ASlingJUnit/.html"));
+ testsManager.executeTests(createRenderer(), new
RequestParser("org.apache.sling.junit.testbundle0.ASlingJUnit/testSuccessful.html"));
+
+ {
+ final Renderer renderer = createRenderer();
+ final RequestParser requestParser = new
RequestParser("org.apache.sling.junit.testbundle0/testSuccessful.html");
+ try {
+ testsManager.executeTests(renderer, requestParser);
+ fail("IllegalStateException expected when selecting method for
multiple tests");
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ }
+
+ {
+ final Renderer renderer = createRenderer();
+ final RequestParser requestParser = new
RequestParser("no.test.Available.html");
+ try {
+ testsManager.executeTests(renderer, requestParser);
+ fail("NoTestCasesFoundException expected when selecting
non-existing test class");
+ } catch (TestsManager.NoTestCasesFoundException e) {
+ // expected
+ }
+ }
+
+ testsManager.deactivate();
+ bundleTestsProvider.deactivate();
+ }
+
+ public void createFragmentBundle(String symbolicName) throws IOException {
+ final Bundle bundle = createMockBundle(symbolicName, Bundle.ACTIVE);
+
+ final Dictionary<String, String> fragmentHeaders = bundle.getHeaders();
+ fragmentHeaders.put(BundleTestsProvider.SLING_TEST_REGEXP,
"system.bundle.*SlingJUnit");
+ fragmentHeaders.put(Constants.FRAGMENT_HOST, "system.bundle");
+
+ when(bundle.findEntries("", "*.class", true))
+ .thenAnswer(m ->
classesAsResourceEnumeration(singletonList("system.bundle.FragmentSlingJUnit")));
+
+ addBundleWiring(bundle, emptyMockClassloader());
+ }
+
+ private <T> T activateAndRegister(BundleContext bundleContext, Class<?
super T> interfaze, T service, BiConsumer<T, BundleContext> activator)
+ throws InvalidSyntaxException {
+ activator.accept(service, bundleContext);
+ registerService(bundleContext, service, interfaze);
+ return service;
+ }
+
+ private static Renderer createRenderer() throws IOException {
+ final PlainTextRenderer renderer = new PlainTextRenderer() {
+ @Override
+ public RunListener getRunListener() {
+ return super.getRunListener();
+ }
+ };
+ final HttpServletResponse response = mock(HttpServletResponse.class);
+ when(response.getWriter()).thenReturn(mock(PrintWriter.class));
+ renderer.setup(response, "Test");
+ return renderer;
+ }
+
+ @NotNull
+ private Bundle createMockBundle(String symbolicName, int state) {
+ final Bundle bundle = mock(Bundle.class);
+ when(bundle.getSymbolicName()).thenReturn(symbolicName);
+ when(bundle.getState()).thenReturn(state);
+ when(bundle.getHeaders()).thenReturn(new Hashtable<>());
+
+ final BundleContext bundleContext = mock(BundleContext.class);
+ when(bundleContext.getBundle()).thenReturn(bundle);
+
+ when(bundle.getBundleContext()).thenReturn(bundleContext);
+
+ when(bundleContext.getBundles())
+ .thenAnswer(m -> mockBundles.toArray(new Bundle[0]));
+
+ mockBundles.add(bundle);
+
+ return bundle;
+ }
+
+ private void createTestBundle(String symbolicName, String testRegexp,
Collection<String> classes)
+ throws ClassNotFoundException, IOException {
+ final Bundle bundle = createMockBundle(symbolicName, Bundle.ACTIVE);
+
+ when(bundle.findEntries("", "*.class", true)).thenAnswer(m ->
classesAsResourceEnumeration(classes));
+
+ // we just return the Object.class instead of a real class - we're not
doing anything with it
+ when(bundle.loadClass(argThat(classes::contains))).then(m ->
JUnit4SlingJUnit.class);
+ assertThat("cannot load just any class",
bundle.loadClass("any.class.Name"), nullValue());
+
+ if (testRegexp != null) {
+ bundle.getHeaders().put(BundleTestsProvider.SLING_TEST_REGEXP,
testRegexp);
+ }
+
+ addBundleWiring(bundle, emptyMockClassloader());
+ }
+
+ @NotNull
+ private static ClassLoader emptyMockClassloader() throws IOException {
+ final ClassLoader classLoader = mock(ClassLoader.class);
+ when(classLoader.getResources(any())).thenAnswer(m ->
Collections.emptyEnumeration());
+ return classLoader;
+ }
+
+ private static <T> void registerService(BundleContext bundleContext, T
service, Class<? super T> interfaze) throws InvalidSyntaxException {
+ @SuppressWarnings("unchecked") final ServiceReference<T>
serviceReference = (ServiceReference<T>) mock(ServiceReference.class);
+ final Set<ServiceReference<T>> references =
Collections.singleton(serviceReference);
+ when(bundleContext.getServiceReferences(interfaze, null)).thenAnswer(m
-> references);
+ when(bundleContext.getServiceReferences(interfaze.getName(), null))
+ .thenAnswer(m -> references.toArray(new ServiceReference[0]));
+ when(bundleContext.getServiceReference(interfaze)).thenAnswer(m ->
serviceReference);
+
when(bundleContext.getServiceReference(interfaze.getName())).thenAnswer(m ->
serviceReference);
+ when(bundleContext.getService(serviceReference)).thenReturn(service);
+ }
+
+ private static void addBundleWiring(Bundle bundle, ClassLoader
classLoader) {
+ final BundleWiring bundleWiring = mock(BundleWiring.class);
+ when(bundleWiring.getClassLoader()).thenReturn(classLoader);
+ when(bundle.adapt(BundleWiring.class)).thenReturn(bundleWiring);
+ }
+
+ private static Enumeration<URL>
classesAsResourceEnumeration(Collection<String> classes) {
+ final List<URL> resources = classes.stream()
+ .map(clazz -> '/' + clazz.replace('.', '/') + ".class")
+ .map(file -> {
+ try {
+ // In Apache Felix URLs look like this:
+ //
bundle://<random-bundle-identifier>:0/org/path/to/ClassName.class
+ // However, the "bundle" protocol causes an exception
+ // and in any case, only the URL's file part is used.
+ return new URL("file://pseudo:80" + file);
+ } catch (MalformedURLException e) {
+ fail(e.getMessage());
+ }
+ // cannot be reached because "fail()" throws an
AssertionError
+ return null;
+ })
+ .collect(Collectors.toList());
+ return Collections.enumeration(resources);
+ }
}
\ No newline at end of file
diff --git
a/src/test/java/org/apache/sling/junit/impl/servlet/junit5/RunListenerAdapterTest.java
b/src/test/java/org/apache/sling/junit/impl/servlet/junit5/RunListenerAdapterTest.java
new file mode 100644
index 0000000..5e493f1
--- /dev/null
+++
b/src/test/java/org/apache/sling/junit/impl/servlet/junit5/RunListenerAdapterTest.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.impl.servlet.junit5;
+
+import org.apache.sling.junit.sampletests.JUnit4SlingJUnit;
+import org.junit.Test;
+import org.junit.platform.engine.TestDescriptor;
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.engine.UniqueId;
+import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.engine.support.descriptor.MethodSource;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+
+import java.util.LinkedHashSet;
+import java.util.Objects;
+import java.util.Optional;
+
+import static java.util.Arrays.asList;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class RunListenerAdapterTest {
+
+ @Test
+ public void testLifecycle() throws Exception {
+ RunListener runListener = mock(RunListener.class);
+ RunListenerAdapter runListenerAdapter = new
RunListenerAdapter(runListener);
+
+ // start test run
+ TestPlan testPlan = mock(TestPlan.class);
+ runListenerAdapter.testPlanExecutionStarted(testPlan);
+ verify(runListener, times(1))
+ .testRunStarted(argThat(desc ->
desc.getClassName().equals("classes")));
+
+ // start test class (which is a suite)
+ runListenerAdapter.executionStarted(getTestIdentifierForClass());
+ verify(runListener, times(1))
+ .testSuiteStarted(argThat(desc ->
desc.getClassName().equals(JUnit4SlingJUnit.class.getName())));
+
+ // start test method (success)
+
runListenerAdapter.executionStarted(getTestIdentifierForMethod("testSuccessful"));
+ verify(runListener, times(1)).testStarted(
+ argThat(desc -> desc.isTest() &&
Objects.equals(desc.getMethodName(), "testSuccessful")));
+
runListenerAdapter.executionFinished(getTestIdentifierForMethod("testSuccessful"),
getTestExecutionResult(TestExecutionResult.Status.SUCCESSFUL));
+ verify(runListener, times(1)).testFinished(
+ argThat(desc -> desc.isTest() &&
Objects.equals(desc.getMethodName(), "testSuccessful")));
+
+ // finish test method (success)
+
+ // start test method (failure)
+
runListenerAdapter.executionStarted(getTestIdentifierForMethod("testFailed"));
+ verify(runListener, times(1)).testStarted(
+ argThat(desc -> desc.isTest() &&
Objects.equals(desc.getMethodName(), "testFailed")));
+
runListenerAdapter.executionFinished(getTestIdentifierForMethod("testFailed"),
getTestExecutionResult(TestExecutionResult.Status.FAILED));
+ verify(runListener, times(1)).testFailure(any(Failure.class));
+ verify(runListener, times(1)).testFinished(
+ argThat(desc -> desc.isTest() &&
Objects.equals(desc.getMethodName(), "testFailed")));
+
+ // start test method (skipped)
+
runListenerAdapter.executionSkipped(getTestIdentifierForMethod("testSkipped"),
null);
+ verify(runListener, times(1)).testIgnored(
+ argThat(desc -> desc.isTest() &&
Objects.equals(desc.getMethodName(), "testSkipped")));
+
+ runListenerAdapter.executionFinished(getTestIdentifierForClass(),
getTestExecutionResult(TestExecutionResult.Status.FAILED));
+ verify(runListener, times(1)).testSuiteFinished(
+ argThat(desc -> Objects.equals(desc.getClassName(),
JUnit4SlingJUnit.class.getName())));
+
+ runListenerAdapter.testPlanExecutionFinished(testPlan);
+ verify(runListener, times(1)).testRunFinished(
+ argThat(r ->
+ !r.wasSuccessful()
+ && r.getRunCount() == 2
+ && r.getFailureCount() == 1
+ && r.getIgnoreCount() == 1
+ && r.getAssumptionFailureCount() == 0
+ && r.getRunTime() > 0));
+
+ verifyNoMoreInteractions(runListener);
+ }
+
+ public TestExecutionResult
getTestExecutionResult(TestExecutionResult.Status status) {
+ final TestExecutionResult result = mock(TestExecutionResult.class);
+ when(result.getStatus()).thenReturn(status);
+ return result;
+ }
+
+ public TestIdentifier getTestIdentifierForClass() {
+ TestDescriptor testDescriptor = mock(TestDescriptor.class);
+ when(testDescriptor.getUniqueId()).thenReturn(mock(UniqueId.class));
+
when(testDescriptor.getType()).thenReturn(TestDescriptor.Type.CONTAINER);
+ when(testDescriptor.isTest()).thenReturn(false);
+
when(testDescriptor.getSource()).thenReturn(Optional.of(ClassSource.from(JUnit4SlingJUnit.class)));
+ when(testDescriptor.getChildren()).thenAnswer(m -> new
LinkedHashSet<>(asList(
+ getTestIdentifierForMethod("testSuccessful"),
+ getTestIdentifierForMethod("testFailed"),
+ getTestIdentifierForMethod("testSkipped"))));
+ return TestIdentifier.from(testDescriptor);
+ }
+
+ public TestIdentifier getTestIdentifierForMethod(String methodName) {
+ TestDescriptor testDescriptor = mock(TestDescriptor.class);
+ when(testDescriptor.getUniqueId()).thenReturn(mock(UniqueId.class));
+ when(testDescriptor.getType()).thenReturn(TestDescriptor.Type.TEST);
+ when(testDescriptor.isTest()).thenReturn(true);
+ when(testDescriptor.getSource()).thenReturn(Optional.of(
+ MethodSource.from(JUnit4SlingJUnit.class.getName(),
methodName)));
+ return TestIdentifier.from(testDescriptor);
+ }
+
+}
\ No newline at end of file
diff --git
a/src/test/java/org/apache/sling/junit/sampletests/JUnit4SlingJUnit.java
b/src/test/java/org/apache/sling/junit/sampletests/JUnit4SlingJUnit.java
new file mode 100644
index 0000000..dbc5765
--- /dev/null
+++ b/src/test/java/org/apache/sling/junit/sampletests/JUnit4SlingJUnit.java
@@ -0,0 +1,48 @@
+/*
+ * 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.sampletests;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Pseudo test-case class executed tests in {@link
org.apache.sling.junit.impl.TestsManagerImplTest}.
+ *
+ * Name does not match normal JUnit patterns in order NOT to be included in
the normal build's tests.
+ */
+public class JUnit4SlingJUnit {
+
+ @Test
+ public void testSuccessful() {
+ assertTrue(true);
+ }
+
+ @Test @Ignore("skipped for testing")
+ public void testSkipped() {
+ assertTrue(true);
+ }
+
+ @Test
+ public void testFailed() {
+ fail();
+ }
+}