SLING-1984 - support for running individual tests remotely from an IDE. Based on a contribution by Pooja Kothari, thanks!
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1074632 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
index 2e06348..81102e1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,7 @@
<artifactId>org.apache.sling.junit.remote</artifactId>
<version>0.1.1-SNAPSHOT</version>
- <packaging>jar</packaging>
+ <packaging>bundle</packaging>
<name>Apache Sling JUnit Remote Tests Runners</name>
<description>Utilities to run server-side JUnit tests remotely</description>
@@ -39,13 +39,49 @@
<url>http://svn.apache.org/viewvc/sling/trunk/testing/junit/remote</url>
</scm>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-scr-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Export-Package>org.apache.sling.junit.remote.exported.*</Export-Package>
+ <Import-Package>
+ org.apache.http.*; resolution:=optional,
+ org.apache.sling.testing.tools.http; resolution:=optional,
+ org.junit.internal.*; resolution:=optional,
+ *
+ </Import-Package>
+ <Sling-Test-Regexp>.*Test</Sling-Test-Regexp>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
<dependencies>
<dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ </dependency>
+ <dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.junit.core</artifactId>
<version>0.1.1-SNAPSHOT</version>
</dependency>
<dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.scr.annotations</artifactId>
+ <version>1.4.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
@@ -57,6 +93,12 @@
<version>1.5.11</version>
</dependency>
<dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <version>1.5.11</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.testing.tools</artifactId>
<version>0.1.1-SNAPSHOT</version>
@@ -66,5 +108,9 @@
<artifactId>org.apache.sling.commons.json</artifactId>
<version>2.0.6</version>
</dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>servlet-api</artifactId>
+ </dependency>
</dependencies>
</project>
diff --git a/src/main/java/org/apache/sling/junit/remote/exported/ExampleRemoteTest.java b/src/main/java/org/apache/sling/junit/remote/exported/ExampleRemoteTest.java
new file mode 100644
index 0000000..063a4f4
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/remote/exported/ExampleRemoteTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.remote.exported;
+
+import static org.junit.Assert.fail;
+import org.apache.sling.junit.remote.ide.SlingRemoteExecutionRule;
+import org.junit.Rule;
+import org.junit.Test;
+
+/** Test that can be run remotely on a Sling instance from an IDE, by
+ * setting the {@link SlingRemoteExecutionRule.SLING_REMOTE_TEST_URL}
+ * system property in the IDE setup, to the URL of
+ * the Sling JUnit servlet (like http://localhost:8080/system/sling/junit)
+ */
+public class ExampleRemoteTest {
+
+ @Rule
+ public SlingRemoteExecutionRule execRule = new SlingRemoteExecutionRule();
+
+ @Test
+ public void testAlwaysPasses() {
+ }
+
+ @Test
+ public void testAlwaysFails() {
+ fail("This test always fails");
+ }
+
+ @Test
+ public void testFailsSometimes() {
+ if(Math.random() < 0.5) {
+ fail("This test fails sometimes");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/junit/remote/httpclient/RemoteTestHttpClient.java b/src/main/java/org/apache/sling/junit/remote/httpclient/RemoteTestHttpClient.java
new file mode 100644
index 0000000..007109a
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/remote/httpclient/RemoteTestHttpClient.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.remote.httpclient;
+
+import java.io.IOException;
+
+import org.apache.http.ParseException;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.sling.testing.tools.http.Request;
+import org.apache.sling.testing.tools.http.RequestBuilder;
+import org.apache.sling.testing.tools.http.RequestExecutor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** HTTP client that executes tests remotely */
+public class RemoteTestHttpClient {
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ private final String junitServletUrl;
+ private StringBuilder subpath;
+ private boolean consumeContent;
+ private static final String SLASH = "/";
+ private static final String DOT = ".";
+
+ public RemoteTestHttpClient(String junitServletUrl, boolean consumeContent) {
+ if(junitServletUrl == null) {
+ throw new IllegalArgumentException("JUnit servlet URL is null, cannot run tests");
+ }
+ this.junitServletUrl = junitServletUrl;
+ this.consumeContent = consumeContent;
+ }
+
+ public RequestExecutor runTests(String testClassesSelector, String testMethodSelector, String extension)
+ throws ClientProtocolException, IOException {
+ final RequestBuilder builder = new RequestBuilder(junitServletUrl);
+
+ // Optionally let the client to consume the response entity
+ final RequestExecutor executor = new RequestExecutor(new DefaultHttpClient()) {
+ @Override
+ protected void consumeEntity() throws ParseException, IOException {
+ if(consumeContent) {
+ super.consumeEntity();
+ }
+ }
+ };
+
+ // POST request executes the tests
+ subpath = new StringBuilder();
+ if(!junitServletUrl.endsWith(SLASH)) {
+ subpath.append(SLASH);
+ }
+ subpath.append(testClassesSelector);
+ if(!extension.startsWith(DOT)) {
+ subpath.append(DOT);
+ }
+ subpath.append(extension);
+ if(testMethodSelector != null && testMethodSelector.length() > 0) {
+ subpath.append("/");
+ subpath.append(testMethodSelector);
+ }
+
+ log.info("Executing test remotely, path={} JUnit servlet URL={}",
+ subpath, junitServletUrl);
+ final Request r = builder.buildPostRequest(subpath.toString());
+ executor.execute(r).assertStatus(200);
+
+ return executor;
+ }
+
+ /** If called after runTests, returns the path used to
+ * run tests on the remote JUnit servlet
+ */
+ public String getTestExecutionPath() {
+ return subpath == null ? null : subpath.toString();
+ }
+}
diff --git a/src/main/java/org/apache/sling/junit/remote/ide/ExecutionResult.java b/src/main/java/org/apache/sling/junit/remote/ide/ExecutionResult.java
new file mode 100644
index 0000000..19cf76d
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/remote/ide/ExecutionResult.java
@@ -0,0 +1,53 @@
+/*
+ * 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.remote.ide;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.runner.Result;
+import org.junit.runner.notification.Failure;
+
+public class ExecutionResult implements Serializable {
+ private static final long serialVersionUID = 7935484811381524530L;
+ private final Throwable throwable;
+
+ public ExecutionResult(Result result) {
+ if (result.getFailureCount() > 0) {
+ final List<Throwable> failures = new ArrayList<Throwable>(result.getFailureCount());
+ for (Failure f : result.getFailures()) {
+ failures.add(f.getException());
+ }
+
+ // TODO MultipleFailureException is an internal JUnit class -
+ // we don't have it when running server-side in Sling
+ // throwable = new MultipleFailureException(failures);
+ throwable = failures.get(0);
+ } else {
+ throwable = null;
+ }
+ }
+
+ public Throwable getException() {
+ return throwable;
+ }
+
+ public boolean isFailure() {
+ return throwable != null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/junit/remote/ide/SerializedRenderer.java b/src/main/java/org/apache/sling/junit/remote/ide/SerializedRenderer.java
new file mode 100644
index 0000000..ddb0dc3
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/remote/ide/SerializedRenderer.java
@@ -0,0 +1,97 @@
+/*
+ * 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.remote.ide;
+
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Collection;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.junit.Renderer;
+import org.apache.sling.junit.RequestParser;
+import org.junit.runner.Result;
+import org.junit.runner.notification.RunListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Renderer for Sling JUnit server-side testing, which
+ * renders test results in binary form.
+ * Used to send results, and especially Exceptions, as
+ * is to a remote IDE.
+ */
+@Component(immediate=false)
+@Service
+public class SerializedRenderer extends RunListener implements Renderer {
+
+ public static final String EXTENSION = "serialized";
+ private ObjectOutputStream outputStream;
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ /** @inheritDoc */
+ public boolean appliesTo(RequestParser p) {
+ return EXTENSION.equals(p.getExtension());
+ }
+
+ /** @inheritDoc */
+ public void setup(HttpServletResponse response, String pageTitle)
+ throws IOException, UnsupportedEncodingException {
+ response.setContentType("application/x-java-serialized-object");
+ outputStream = new ObjectOutputStream(response.getOutputStream());
+ }
+
+ /** @inheritDoc */
+ public void cleanup() {
+ try {
+ outputStream.flush();
+ outputStream.close();
+ } catch (IOException e) {
+ log.warn("Exception in cleanup()", e);
+ }
+ outputStream = null;
+ }
+
+ /** @inheritDoc */
+ public RunListener getRunListener() {
+ return this;
+ }
+
+ /** @inheritDoc */
+ public void info(String role, String info) {
+ }
+
+ /** @inheritDoc */
+ public void link(String info, String url, String method) {
+ }
+
+ /** @inheritDoc */
+ public void list(String role, Collection<String> data) {
+ }
+
+ /** @inheritDoc */
+ public void title(int level, String title) {
+ }
+
+ @Override
+ public void testRunFinished(Result result) throws IOException {
+ final ExecutionResult er = new ExecutionResult(result);
+ outputStream.writeObject(er);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/junit/remote/ide/SlingRemoteExecutionRule.java b/src/main/java/org/apache/sling/junit/remote/ide/SlingRemoteExecutionRule.java
new file mode 100644
index 0000000..9590ab8
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/remote/ide/SlingRemoteExecutionRule.java
@@ -0,0 +1,96 @@
+/*
+ * 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.remote.ide;
+
+import java.io.ObjectInputStream;
+
+import org.apache.http.HttpEntity;
+import org.apache.sling.junit.remote.httpclient.RemoteTestHttpClient;
+import org.apache.sling.testing.tools.http.RequestExecutor;
+import org.junit.rules.MethodRule;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.Statement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SlingRemoteExecutionRule implements MethodRule {
+ private static final Logger log =
+ LoggerFactory.getLogger(SlingRemoteExecutionRule.class);
+
+ /** Name of the system property that activates remote test execution */
+ public static final String SLING_REMOTE_TEST_URL = "sling.remote.test.url";
+
+ public Statement apply(final Statement base, final FrameworkMethod method, Object target) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ if (tryRemoteEvaluation(method)) {
+ return;
+ }
+ base.evaluate();
+ }
+ };
+ }
+
+ /**
+ * Execute test remotely if the corresponding system property is set
+ *
+ * @return <code>true</code> if the method was executed remotely and passed.
+ * If the test was <b>not</b> executed remotely then
+ * <code>false</code> is returned to indicate that test should be
+ * executed locally
+ */
+ private boolean tryRemoteEvaluation(FrameworkMethod method) throws Throwable {
+ String remoteUrl = System.getProperty(SLING_REMOTE_TEST_URL);
+ if(remoteUrl != null) {
+ remoteUrl = remoteUrl.trim();
+ if(remoteUrl.length() > 0) {
+ invokeRemote(remoteUrl, method);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void invokeRemote(String remoteUrl, FrameworkMethod method) throws Throwable {
+ final String testClassesSelector = method.getMethod().getDeclaringClass().getName();
+ final String methodName = method.getMethod().getName();
+
+ final RemoteTestHttpClient testHttpClient = new RemoteTestHttpClient(remoteUrl, false);
+ final RequestExecutor executor = testHttpClient.runTests(
+ testClassesSelector, methodName, "serialized"
+ );
+ log.debug("Ran test {} method {} at URL {}",
+ new Object[] { testClassesSelector, methodName, remoteUrl });
+
+ final HttpEntity entity = executor.getResponse().getEntity();
+ if (entity != null) {
+ try {
+ final Object o = new ObjectInputStream(entity.getContent()).readObject();
+ if( !(o instanceof ExecutionResult) ) {
+ throw new IllegalStateException("Expected an ExecutionResult, got a " + o.getClass().getName());
+ }
+ final ExecutionResult result = (ExecutionResult)o;
+ if (result.isFailure()) {
+ throw result.getException();
+ }
+ } finally {
+ entity.consumeContent();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/junit/remote/testrunner/SlingRemoteTestRunner.java b/src/main/java/org/apache/sling/junit/remote/testrunner/SlingRemoteTestRunner.java
index 428f090..d8e9118 100644
--- a/src/main/java/org/apache/sling/junit/remote/testrunner/SlingRemoteTestRunner.java
+++ b/src/main/java/org/apache/sling/junit/remote/testrunner/SlingRemoteTestRunner.java
@@ -21,12 +21,10 @@
import java.util.LinkedList;
import java.util.List;
-import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONObject;
import org.apache.sling.commons.json.JSONTokener;
-import org.apache.sling.testing.tools.http.Request;
-import org.apache.sling.testing.tools.http.RequestBuilder;
+import org.apache.sling.junit.remote.httpclient.RemoteTestHttpClient;
import org.apache.sling.testing.tools.http.RequestExecutor;
import org.junit.internal.AssumptionViolatedException;
import org.junit.internal.runners.model.EachTestNotifier;
@@ -47,8 +45,7 @@
public class SlingRemoteTestRunner extends ParentRunner<SlingRemoteTest> {
private static final Logger log = LoggerFactory.getLogger(SlingRemoteTestRunner.class);
private final SlingRemoteTestParameters testParameters;
- private RequestExecutor executor;
- private RequestBuilder builder;
+ private RemoteTestHttpClient testHttpClient;
private final Class<?> testClass;
private final List<SlingRemoteTest> children = new LinkedList<SlingRemoteTest>();
@@ -72,32 +69,18 @@
}
private void maybeExecuteTests() throws Exception {
- if(executor != null) {
+ if(testHttpClient != null) {
+ // Tests already ran
return;
}
- // Setup request execution
- executor = new RequestExecutor(new DefaultHttpClient());
- if(testParameters.getJunitServletUrl() == null) {
- throw new IllegalStateException("Server base URL is null, cannot run tests");
- }
- builder = new RequestBuilder(testParameters.getJunitServletUrl());
-
- // POST request executes the tests
- final StringBuilder subpath = new StringBuilder();
- subpath.append("/");
- subpath.append(testParameters.getTestClassesSelector());
- subpath.append(".json");
- final String testMethodSelector = testParameters.getTestMethodSelector();
- if(testMethodSelector != null && testMethodSelector.length() > 0) {
- subpath.append("/");
- subpath.append(testMethodSelector);
- }
- final Request r = builder.buildPostRequest(subpath.toString());
- executor.execute(r)
- .assertStatus(200)
- .assertContentType("application/json");
-
+ testHttpClient = new RemoteTestHttpClient(testParameters.getJunitServletUrl(), true);
+ final RequestExecutor executor = testHttpClient.runTests(
+ testParameters.getTestClassesSelector(),
+ testParameters.getTestMethodSelector(),
+ "json"
+ );
+ executor.assertContentType("application/json");
final JSONArray json = new JSONArray(new JSONTokener((executor.getContent())));
// Response contains an array of objects identified by
@@ -111,7 +94,7 @@
}
log.info("Server-side tests executed at {} with path {}",
- testParameters.getJunitServletUrl(), subpath);
+ testParameters.getJunitServletUrl(), testHttpClient.getTestExecutionPath());
// Check that number of tests is as expected
assertEquals("Expecting " + testParameters.getExpectedNumberOfTests() + " tests",