You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by js...@apache.org on 2020/10/06 13:12:47 UTC
[sling-org-apache-sling-junit-core] 01/01: SLING-9795 - JUnit 5
support for server-side tests
This is an automated email from the ASF dual-hosted git repository.
jsedding pushed a commit to branch issues/SLING-9795-junit5-support-for-server-side-tests
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-junit-core.git
commit 3d13ef6d32521622bac88ed9bf8779efd171bd74
Author: Julian Sedding <js...@apache.org>
AuthorDate: Tue Oct 6 15:12:19 2020 +0200
SLING-9795 - JUnit 5 support for server-side tests
---
bnd.bnd | 14 +-
pom.xml | 45 +++-
.../java/org/apache/sling/junit/TestsManager.java | 15 +-
.../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 | 246 +++++++--------------
.../junit/impl/JUnit4TestExecutionStrategy.java | 68 ++++++
.../junit/impl/TestContextRunListenerWrapper.java | 2 +-
.../sling/junit/impl/TestExecutionStrategy.java | 33 +++
.../apache/sling/junit/impl/TestsManagerImpl.java | 217 ++++++------------
.../sling/junit/impl/servlet/ServletProcessor.java | 33 ++-
.../impl/servlet/junit5/DescriptionGenerator.java | 65 ++++++
.../junit/impl/servlet/junit5/FailureHelper.java | 50 +++++
.../junit5/JUnit5TestExecutionStrategy.java | 125 +++++++++++
.../junit/impl/servlet/junit5/ResultAdapter.java | 80 +++++++
.../impl/servlet/junit5/RunListenerAdapter.java | 126 +++++++++++
.../impl/servlet/junit5/TestEngineTracker.java | 91 ++++++++
src/main/resources/junit.css | 3 +
.../sling/junit/impl/TestsManagerImplTest.java | 74 ++++---
20 files changed, 945 insertions(+), 394 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..04be215 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>
@@ -60,6 +60,17 @@
<excludePackageNames>org.apache.sling.junit.impl;org.apache.sling.junit.impl.*</excludePackageNames>
</configuration>
</plugin>
+ <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>
</plugins>
</build>
@@ -117,6 +128,17 @@
<dependencies>
<dependency>
+ <groupId>biz.aQute.bnd</groupId>
+ <artifactId>bnd-baseline-maven-plugin</artifactId>
+ <version>5.0.0</version>
+ </dependency>
+ <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 +215,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.
-->
@@ -215,15 +246,15 @@
<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/TestsManager.java b/src/main/java/org/apache/sling/junit/TestsManager.java
index 497ad0b..5cdba5a 100644
--- a/src/main/java/org/apache/sling/junit/TestsManager.java
+++ b/src/main/java/org/apache/sling/junit/TestsManager.java
@@ -34,12 +34,6 @@ public interface TestsManager {
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();
-
- /**
* Instantiate test class for specified test
*
* @param testName the test class
@@ -66,4 +60,13 @@ public interface TestsManager {
* @throws Exception if any error occurs
*/
void executeTests(Collection<String> testNames, Renderer renderer, 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.
+ */
+ @Deprecated
+ void clearCaches();
}
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..5ce5489 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,24 @@
package org.apache.sling.junit.impl;
import java.net.URL;
-import java.util.ArrayList;
+import java.util.Collection;
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.regex.Pattern;
import java.util.regex.PatternSyntaxException;
+import java.util.stream.Collectors;
-import org.apache.sling.junit.TestsProvider;
+import org.jetbrains.annotations.Nullable;
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.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;
@@ -40,190 +43,99 @@ import org.slf4j.LoggerFactory;
* 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;
-
+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 IllegalArgumentException("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);
+ return getTestClasses(bundle);
}
}
- /** 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);
- }
- catch (PatternSyntaxException pse) {
- log.warn("Invalid pattern '" + headerValue + "' for bundle "
- + b.getSymbolicName() + ", ignored", pse);
- }
- }
-
- 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);
- while (classUrls.hasMoreElements()) {
- URL url = classUrls.nextElement();
- final String name = toClassName(url);
- if(testClassRegexp.matcher(name).matches()) {
- result.add(name);
- } else {
- log.debug("Class {} does not match {} pattern {} of bundle {}, ignored",
- new Object[] { name, SLING_TEST_REGEXP, testClassRegexp, b.getSymbolicName() });
- }
- }
- log.info("{} test classes found in bundle {}", result.size(), b.getSymbolicName());
+ @Nullable
+ 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 null;
}
-
- 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;
- }
+ Pattern testClassRegexp;
+ try {
+ testClassRegexp = Pattern.compile(headerValue);
+ } catch (PatternSyntaxException pse) {
+ LOG.warn("Bundle '{}' has an invalid pattern for {} header, ignored: '{}'",
+ bundle.getSymbolicName(), SLING_TEST_REGEXP, headerValue);
+ return null;
}
- 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;
+
+ 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()) {
+ result.add(name);
+ } else {
+ LOG.debug("Class '{}' does not match {} pattern '{}' of bundle '{}', ignored",
+ name, SLING_TEST_REGEXP, testClassRegexp, bundle.getSymbolicName());
}
}
-
- if(b == null) {
- throw new IllegalArgumentException("No Bundle found that supplies test class " + testName);
- }
- return b.loadClass(testName);
- }
- public long lastModified() {
- return lastModified;
+ LOG.info("{} test classes found in bundle '{}'", result.size(), bundle.getSymbolicName());
+ return result.isEmpty() ? null : result;
}
- public String getServicePid() {
- return componentName;
+ private static String getSlingTestRegexp(Bundle bundle) {
+ return bundle.getHeaders().get(SLING_TEST_REGEXP);
}
- public List<String> getTestNames() {
- maybeUpdateTestClasses();
- final List<String> result = new ArrayList<String>();
- for(List<String> list : testClassesMap.values()) {
- result.addAll(list);
- }
- return result;
+ /** 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('/', '.');
}
}
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..04cbaf6
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/impl/JUnit4TestExecutionStrategy.java
@@ -0,0 +1,68 @@
+/*
+ * 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.Renderer;
+import org.apache.sling.junit.SlingTestContextProvider;
+import org.apache.sling.junit.TestSelector;
+import org.junit.runner.JUnitCore;
+import org.junit.runner.Request;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+
+public class JUnit4TestExecutionStrategy implements TestExecutionStrategy {
+
+ private static final Logger LOG = LoggerFactory.getLogger(JUnit4TestExecutionStrategy.class);
+
+ private final TestsManagerImpl testsManager;
+
+ public JUnit4TestExecutionStrategy(TestsManagerImpl testsManager) {
+ this.testsManager = testsManager;
+ }
+
+ @Override
+ public void execute(Renderer renderer, Collection<String> testNames, TestSelector selector) throws Exception {
+ final JUnitCore junit = new JUnitCore();
+ 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(testsManager.getTestClass(className), testMethodName));
+ } else {
+ LOG.debug("Running test class {}", className);
+ junit.run(testsManager.getTestClass(className));
+ }
+ }
+ }
+
+ @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..2d5cee0 100644
--- a/src/main/java/org/apache/sling/junit/impl/TestContextRunListenerWrapper.java
+++ b/src/main/java/org/apache/sling/junit/impl/TestContextRunListenerWrapper.java
@@ -29,7 +29,7 @@ public class TestContextRunListenerWrapper extends RunListener {
private long testStartTime;
private static final Logger log = LoggerFactory.getLogger(TestContextRunListenerWrapper.class);
- TestContextRunListenerWrapper(RunListener toWrap) {
+ public TestContextRunListenerWrapper(RunListener toWrap) {
wrapped = toWrap;
}
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..454e182
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/impl/TestExecutionStrategy.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.impl;
+
+import org.apache.sling.junit.Renderer;
+import org.apache.sling.junit.TestSelector;
+
+import java.io.Closeable;
+import java.util.Collection;
+
+public interface TestExecutionStrategy extends Closeable {
+
+ void execute(Renderer renderer, Collection<String> testNames, TestSelector selector) 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..1f0d361 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,31 @@
*/
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.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.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.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
@Component
public class TestsManagerImpl implements TestsManager {
@@ -54,166 +49,91 @@ 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 final int startupTimeoutSeconds = Integer.parseInt(System.getProperty(PROP_STARTUP_TIMEOUT_SECONDS, "40"));
- private static volatile boolean waitForSystemStartup = true;
+ private volatile boolean waitForSystemStartup = true;
- private ServiceTracker tracker;
-
- 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;
+
+ @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);
+ }
}
- protected void deactivate(ComponentContext ctx) {
- if(tracker != null) {
- tracker.close();
+ @Deactivate
+ protected void deactivate() {
+ if(testsProviderTracker != null) {
+ testsProviderTracker.close();
+ testsProviderTracker = null;
+ }
+
+ if (executionStrategy != null) {
+ executionStrategy.close();
+ executionStrategy = null;
}
- 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);
- }
- 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);
- }
+ final TestsProvider provider = getTestProviders()
+ .filter(p -> p.getTestNames().contains(testName))
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("No TestsProvider found for test '" + testName + "'"));
log.debug("Using provider {} to create test class {}", provider, testName);
return provider.createTestClass(testName);
}
+ @Override
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();
+ final List<String> tests = getTestProviders()
+ .map(TestsProvider::getTestNames)
+ .flatMap(Collection::stream)
+ .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);
- }
- lastTrackingCount = tracker.getTrackingCount();
+
+ private Stream<TestsProvider> getTestProviders() {
+ return testsProviderTracker.getTracked().values().stream();
}
+ @Override
public void executeTests(Collection<String> testNames, Renderer renderer, 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();
}
-
+
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));
- }
- }
+ executionStrategy.execute(renderer, testNames, selector);
} finally {
if(createContext) {
SlingTestContextProvider.deleteContext();
@@ -221,6 +141,7 @@ public class TestsManagerImpl implements TestsManager {
}
}
+ @Override
public void listTests(Collection<String> testNames, Renderer renderer) {
renderer.title(2, "Test classes");
final String note = "The test set can be restricted using partial test names"
@@ -230,15 +151,17 @@ public class TestsManagerImpl implements TestsManager {
renderer.list("testNames", testNames);
}
+ @Override
+ public void clearCaches() {
+ }
/** 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)) {
@@ -280,7 +203,7 @@ 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();
}
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..25a654c 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,8 +19,8 @@ package org.apache.sling.junit.impl.servlet;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.ArrayList;
import java.util.Collections;
-import java.util.LinkedList;
import java.util.List;
import javax.servlet.ServletException;
@@ -53,14 +53,10 @@ public class ServletProcessor {
}
/** Return sorted list of available tests
- * @param prefix optionally select only names that match this prefix
+ * @param selector optionally select only names that match this selector
*/
- 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));
+ private List<String> getTestNames(TestSelector selector) {
+ final List<String> result = new ArrayList<>(testsManager.getTestNames(selector));
Collections.sort(result);
return result;
}
@@ -81,16 +77,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,7 +101,7 @@ public class ServletProcessor {
}
final TestSelector selector = getTestSelector(request);
- final List<String> testNames = getTestNames(selector, forceReload);
+ final List<String> testNames = getTestNames(selector);
// 404 if no tests found
if(testNames.isEmpty()) {
@@ -137,10 +134,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,7 +145,7 @@ public class ServletProcessor {
}
renderer.setup(response, getClass().getSimpleName());
- final List<String> testNames = getTestNames(selector, forceReload);
+ final List<String> testNames = getTestNames(selector);
if(testNames.isEmpty()) {
response.sendError(
HttpServletResponse.SC_NOT_FOUND,
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..b509e9b
--- /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;
+
+ <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..8241a06
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/FailureHelper.java
@@ -0,0 +1,50 @@
+/*
+ * 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.engine.TestExecutionResult;
+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, TestExecutionResult result) {
+ return convert(testIdentifier, result.getThrowable().orElse(null));
+ }
+
+ @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..75b2ff4
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnit5TestExecutionStrategy.java
@@ -0,0 +1,125 @@
+/*
+ * 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.Renderer;
+import org.apache.sling.junit.SlingTestContextProvider;
+import org.apache.sling.junit.TestSelector;
+import org.apache.sling.junit.impl.TestContextRunListenerWrapper;
+import org.apache.sling.junit.impl.TestExecutionStrategy;
+import org.apache.sling.junit.impl.TestsManagerImpl;
+import org.junit.platform.engine.discovery.ClassSelector;
+import org.junit.platform.engine.discovery.DiscoverySelectors;
+import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.TestExecutionListener;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.core.LauncherConfig;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import org.junit.platform.launcher.core.LauncherFactory;
+import org.osgi.framework.BundleContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
+
+public class JUnit5TestExecutionStrategy implements TestExecutionStrategy {
+
+ private static final Logger LOG = LoggerFactory.getLogger(JUnit5TestExecutionStrategy.class);
+
+ 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(Renderer renderer, Collection<String> testNames, TestSelector selector) throws Exception {
+ TestExecutionListener listener = new TestExecutionListener() {
+ @Override
+ public void executionStarted(TestIdentifier testIdentifier) {
+ testIdentifier.getSource().ifPresent(src -> {
+ if (src instanceof ClassSource) {
+ final String className = ((ClassSource) src).getClassName();
+ renderer.title(3, className);
+ }
+ });
+
+ // If we have a test context, clear its output metadata
+ if (SlingTestContextProvider.hasContext()) {
+ SlingTestContextProvider.getContext().output().clear();
+ }
+ }
+ };
+ Launcher launcher = LauncherFactory.create(
+ LauncherConfig.builder()
+ .addTestEngines(testEngineTracker.getAvailableTestEngines())
+ .addTestExecutionListeners(listener, new RunListenerAdapter(new TestContextRunListenerWrapper(renderer.getRunListener())))
+ .enableTestEngineAutoRegistration(false)
+ .enableTestExecutionListenerAutoRegistration(false)
+ .build()
+ );
+
+ final LauncherDiscoveryRequestBuilder requestBuilder = LauncherDiscoveryRequestBuilder.request();
+ if (testNames.size() == 1) {
+ final String className = testNames.iterator().next();
+ final Class<?> testClass = testsManager.getTestClass(className);
+ final String testMethodName = selector == null ? null : selector.getSelectedTestMethodName();
+ if (testMethodName != null && testMethodName.length() > 0) {
+ LOG.debug("Running test method {} from test class {}", testMethodName, className);
+ requestBuilder.selectors(selectMethod(testClass, testMethodName));
+ } else {
+ LOG.debug("Running test class {}", className);
+ requestBuilder.selectors(selectClass(testClass));
+ }
+ } else {
+ final List<ClassSelector> testSelectors = testNames.stream()
+ .map(className -> {
+ try {
+ return testsManager.getTestClass(className);
+ } catch (ClassNotFoundException e) {
+ LOG.warn("Failed to find test class '{}'", className);
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .map(DiscoverySelectors::selectClass)
+ .collect(Collectors.toList());
+
+ requestBuilder.selectors(testSelectors);
+ }
+
+ launcher.execute(requestBuilder.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..8c58f08
--- /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 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..b34ef0a
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/RunListenerAdapter.java
@@ -0,0 +1,126 @@
+/*
+ * 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);
+ }
+
+ @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::testRunStarted);
+ }
+ }
+
+ @Override
+ public void executionSkipped(TestIdentifier testIdentifier, String reason) {
+ summarizer.executionSkipped(testIdentifier, reason);
+ 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);
+ }
+ }
+
+ @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..6f19702
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/TestEngineTracker.java
@@ -0,0 +1,91 @@
+/*
+ * 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.IdentityHashMap;
+import java.util.ServiceLoader;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+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 Iterable<TestEngine> testEngines =
+ ServiceLoader.load(TestEngine.class, bundle.adapt(BundleWiring.class).getClassLoader());
+ return StreamSupport.stream(testEngines.spliterator(), false)
+ .peek(testEngine -> LOG.info("Found TestEngine '{}' in bundle '{}'", testEngine.getId(), bundle.getSymbolicName()))
+ .collect(Collectors.toCollection(() -> Collections.newSetFromMap(new IdentityHashMap<>())));
+ }
+ }
+}
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/TestsManagerImplTest.java b/src/test/java/org/apache/sling/junit/impl/TestsManagerImplTest.java
index 8b394f4..889233c 100644
--- a/src/test/java/org/apache/sling/junit/impl/TestsManagerImplTest.java
+++ b/src/test/java/org/apache/sling/junit/impl/TestsManagerImplTest.java
@@ -16,34 +16,28 @@
*/
package org.apache.sling.junit.impl;
+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.powermock.api.mockito.PowerMockito.mock;
-import static org.powermock.api.mockito.PowerMockito.when;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import java.util.HashSet;
import java.util.Hashtable;
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 org.osgi.framework.wiring.BundleWiring;
/**
* 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 {
@@ -55,59 +49,67 @@ public class TestsManagerImplTest {
* 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 {
+ public void needToWaitPositiveNotEmptyListNotGloballyTimeout() {
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));
+ 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() throws Exception {
+ public void needToWaitNegativeForstartupTimeout() {
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>()));
+ assertFalse(TestsManagerImpl.needToWait(startupTimeout, emptySet()));
}
/**
* case if needToWait should return false, when for example it reached the global timeout limit.
*/
@Test
- public void needToWaitNegativeForEmptyList() throws Exception {
+ public void needToWaitNegativeForEmptyList() {
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>()));
+ assertFalse(TestsManagerImpl.needToWait(startupTimeout, emptySet()));
}
@Test
public void waitForSystemStartupTimeout() {
- setupBundleContextMock(Bundle.INSTALLED);
- final long elapsed = TestsManagerImpl.waitForSystemStartup();
+ BundleContext bundleContext = setupBundleContext(Bundle.INSTALLED);
+ TestsManagerImpl testsManager = new TestsManagerImpl();
+ testsManager.activate(bundleContext);
+
+ final long elapsed = testsManager.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"));
+ assertTrue(testsManager.isReady());
}
@Test
public void waitForSystemStartupAllActiveBundles() {
- setupBundleContextMock(Bundle.ACTIVE);
- final long elapsed = TestsManagerImpl.waitForSystemStartup();
+ BundleContext bundleContext = setupBundleContext(Bundle.ACTIVE);
+ TestsManagerImpl testsManager = new TestsManagerImpl();
+ testsManager.activate(bundleContext);
+
+ final long elapsed = testsManager.waitForSystemStartup();
assertTrue(elapsed < TimeUnit.SECONDS.toMillis(SYSTEM_STARTUP_SECONDS));
- assertFalse((Boolean) Whitebox.getInternalState(TestsManagerImpl.class, "waitForSystemStartup"));
+ assertTrue(testsManager.isReady());
}
- 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 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;
}
}
\ No newline at end of file