/**
* Copyright 2007-2015, Kaazing Corporation. All rights reserved.
*
* Licensed 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.kaazing.k3po.junit.rules;
import static java.lang.String.format;
import static org.junit.Assert.assertTrue;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.rules.Verifier;
import org.junit.runner.Description;
import org.junit.runner.JUnitCore;
import org.junit.runners.model.Statement;
import org.kaazing.k3po.junit.annotation.ScriptProperty;
import org.kaazing.k3po.junit.annotation.Specification;
import org.kaazing.net.URLFactory;
/**
* A K3poRule specifies how a Test using k3po is executed.
*
*/
public class K3poRule extends Verifier {
private static final Pattern NAMED_PACKAGE_PATH_PATTERN = Pattern.compile("\\$\\{([A-Za-z]+)\\}(.+)");
/*
* For some reason earlier versions of JUnit will cause tests to either hang or succeed incorrectly without ever
* talking to the K3PO. I'm not sure why but the apply method does not seem to be called. So we need to require
* version 4.10 (I know 4.7 has the problem ... not sure about 4.8 and 4.9 but 4.10 works).
*/
static {
JUnitCore core = new JUnitCore();
String version = core.getVersion();
String[] versionTokens = version.split("\\.");
Integer[] versionsInt = new Integer[versionTokens.length];
for (int i = 0; i < versionTokens.length; i++) {
String versionToken = versionTokens[i];
if (versionToken.contains("-")) {
versionToken = versionToken.substring(0, versionToken.indexOf("-"));
}
versionsInt[i] = Integer.parseInt(versionToken);
}
if (versionsInt[0] < 5) {
if (versionsInt.length == 1 || versionsInt[0] < 4 || versionsInt[1] < 10) {
throw new AssertionError("JUnit library 4.10+ required. Found version " + version);
}
}
}
private final Latch latch;
private String scriptRoot;
private URL controlURL;
private SpecificationStatement statement;
private List<String> classOverriddenProperties;
private Map<String, String> packagePathsByName;
/**
* Allocates a new K3poRule.
*/
public K3poRule() {
latch = new Latch();
classOverriddenProperties = new ArrayList<>();
packagePathsByName = new HashMap<>();
}
/**
* Sets the ClassPath root of where to look for scripts when resolving them.
*
* @param packagePath is a package path used resolve relative script names
*
* @return an instance of K3poRule for convenience
*/
public K3poRule setScriptRoot(String packagePath) {
this.scriptRoot = packagePath;
return this;
}
/**
* Adds a named ClassPath root of where to look for scripts when resolving them.
* Specifications should reference the short name using {@code "${shortName}/..." } in script names.
*
* @param shortName the short name used to refer to the package path
* @param packagePath a package path used resolve relative script names
*
* @return an instance of K3poRule for convenience
*/
public K3poRule addScriptRoot(String shortName, String packagePath) {
packagePathsByName.put(shortName, packagePath);
return this;
}
/**
* Sets the URI on which to communicate to the k3po driver.
* @param controlURI the URI on which to connect
* @return an instance of K3poRule for convenience
*/
public K3poRule setControlURI(URI controlURI) {
this.controlURL = createURL(controlURI.toString());
return this;
}
@Override
public Statement apply(Statement statement, final Description description) {
Specification specification = description.getAnnotation(Specification.class);
String[] scripts = (specification != null) ? specification.value() : null;
ScriptProperty overriddenProperty = description.getAnnotation(ScriptProperty.class);
String[] overriddenProperties = (overriddenProperty != null) ? overriddenProperty.value() : null;
List<String> methodOverridenScriptProperties = new ArrayList<>();
if (overriddenProperties != null) {
for (String prop : overriddenProperties) {
methodOverridenScriptProperties.add(prop);
}
}
if (scripts != null) {
// decorate with K3PO behavior only if @Specification annotation is present
List<String> scriptNames = new LinkedList<>();
for (String script : scripts) {
// strict compatibility (relax to support fully qualified paths later)
if (script.startsWith("/")) {
throw new IllegalArgumentException("Script path must be relative");
}
Matcher matcher = NAMED_PACKAGE_PATH_PATTERN.matcher(script);
if (matcher.matches()) {
String shortName = matcher.group(1);
String relativePath = matcher.group(2);
String packagePath = packagePathsByName.get(shortName);
if (packagePath == null) {
throw new IllegalArgumentException("Script short name not found: " + shortName);
}
String scriptName = format("%s/%s", packagePath, relativePath);
scriptNames.add(scriptName);
}
else {
String packagePath = getScriptRoot(description);
String scriptName = format("%s/%s", packagePath, script);
scriptNames.add(scriptName);
}
}
URL controlURL = this.controlURL;
if (controlURL == null) {
// lazy dependency on TCP scheme
controlURL = createURL("tcp://localhost:11642");
}
methodOverridenScriptProperties.addAll(classOverriddenProperties);
this.statement =
new SpecificationStatement(statement, controlURL, scriptNames, latch, methodOverridenScriptProperties);
statement = this.statement;
}
return super.apply(statement, description);
}
/**
* Starts the connects in the robot scripts. The accepts are implicitly started just prior to the test logic in the
* Specification.
*/
public void start() {
// script should already be prepared before annotated test can execute
assertTrue(format("Did you call start() from outside @%s test?", Specification.class.getSimpleName()),
latch.isPrepared());
// notify script to start
latch.notifyStartable();
}
/**
* Blocking call to await for the K3po threads to stop executing. If the connects have not already been initiated via the
* start() method, they will be implicitly called.
* @throws Exception if an error has occurred in the execution of the tests.
*/
public void finish() throws Exception {
assertTrue(format("Did you call finish() from outside @%s test?", Specification.class.getSimpleName()),
!latch.isInInitState());
// wait for script to finish
latch.notifyStartable();
latch.awaitFinished();
}
private static URL createURL(String location) {
try {
return URLFactory.createURL("tcp://localhost:11642");
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
/**
* Wait for barrier to fire.
* @param barrierName is the name of the barrier to await
* @throws InterruptedException if await is interrupted
*/
public void awaitBarrier(String barrierName) throws InterruptedException {
statement.awaitBarrier(barrierName);
}
/**
* Overrides a script property.
* @param property of script
* @return K3po rule for convenience
*/
public K3poRule scriptProperty(String property) {
this.classOverriddenProperties.add(property);
return this;
}
/**
* Notify barrier to fire.
* @param barrierName is the name for the barrier to notify
* @throws InterruptedException if notify is interrupted (note: waits for confirm that is notified)
*/
public void notifyBarrier(String barrierName) throws InterruptedException {
statement.notifyBarrier(barrierName);
}
private String getScriptRoot(
Description description)
{
if (scriptRoot == null) {
Class<?> testClass = description.getTestClass();
String packageName = testClass.getPackage().getName();
return packageName.replaceAll("\\.", "/");
}
return scriptRoot;
}
}