/**
* 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 java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.kaazing.k3po.junit.rules.ScriptRunner.BarrierState.INITIAL;
import static org.kaazing.k3po.junit.rules.ScriptRunner.BarrierState.NOTIFIED;
import static org.kaazing.k3po.junit.rules.ScriptRunner.BarrierState.NOTIFYING;
import java.lang.management.ManagementFactory;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import org.kaazing.k3po.control.internal.Control;
import org.kaazing.k3po.control.internal.command.AbortCommand;
import org.kaazing.k3po.control.internal.command.PrepareCommand;
import org.kaazing.k3po.control.internal.command.StartCommand;
import org.kaazing.k3po.control.internal.event.CommandEvent;
import org.kaazing.k3po.control.internal.event.ErrorEvent;
import org.kaazing.k3po.control.internal.event.FinishedEvent;
import org.kaazing.k3po.control.internal.event.NotifiedEvent;
import org.kaazing.k3po.control.internal.event.PreparedEvent;
import org.kaazing.k3po.junit.rules.internal.ScriptPair;
final class ScriptRunner implements Callable<ScriptPair> {
private final Control controller;
private final List<String> names;
private final Latch latch;
private volatile boolean abortScheduled;
private volatile Map<String, BarrierStateMachine> barriers;
private final List<String> overridenScriptProperties;
private static final int DISPOSE_TIMEOUT = isDebugging() ? 0: 5000;
ScriptRunner(URL controlURL, List<String> names, Latch latch, List<String> overridenScriptProperties) {
if (names == null) {
throw new NullPointerException("names");
}
if (latch == null) {
throw new NullPointerException("latch");
}
this.controller = new Control(controlURL);
this.names = names;
this.latch = latch;
this.barriers = new HashMap<String, ScriptRunner.BarrierStateMachine>();
this.overridenScriptProperties = overridenScriptProperties;
}
public void abort() {
// logging with system.out as I don't believe there is a standard junit logger, in the future
// we will send this on the wire to appear in the diff (https://github.com/k3po/k3po/issues/332)
System.out.println(
"K3po Script Runner is sending an abort!\n Aborts may cause K3po to falsely fail the test if K3po\n"
+ "is still processing a backlog of messages. This is often the case in junit tests that have low\n"
+ "timeout exceptions (less than 5 secs) and are running on somewhat limited hardware (travis CI and build"
+ " machines)\n"
+ "see https://github.com/k3po/k3po/issues/332 for more details");
this.abortScheduled = true;
latch.notifyAbort();
}
@Override
public ScriptPair call() throws Exception {
try {
// We are already done if abort before we start
if (abortScheduled) {
return new ScriptPair();
}
controller.connect();
// send PREPARE command
PrepareCommand prepare = new PrepareCommand();
prepare.setNames(names);
prepare.setOverriddenScriptProperties(overridenScriptProperties);
controller.writeCommand(prepare);
boolean abortWritten = false;
String expectedScript = null;
while (true) {
try {
// validate event name matches command name
CommandEvent event = controller.readEvent(200, MILLISECONDS);
// process event
switch (event.getKind()) {
case PREPARED:
PreparedEvent prepared = (PreparedEvent) event;
expectedScript = prepared.getScript();
for (String barrier : prepared.getBarriers()) {
barriers.put(barrier, new BarrierStateMachine());
}
// notify script is prepared
latch.notifyPrepared();
latch.awaitStartable();
// Send ABORT if we were asked to abort otherwise send start command
if (abortScheduled && !abortWritten) {
sendAbortCommand();
abortWritten = true;
} else {
// send START command
StartCommand start = new StartCommand();
controller.writeCommand(start);
}
break;
case STARTED:
break;
case NOTIFIED:
NotifiedEvent notifiedEvent = (NotifiedEvent) event;
String barrier = notifiedEvent.getBarrier();
BarrierStateMachine stateMachine = barriers.get(barrier);
stateMachine.notified();
break;
case ERROR:
ErrorEvent error = (ErrorEvent) event;
throw new SpecificationException(format("%s:%s", error.getSummary(), error.getDescription()));
case FINISHED:
FinishedEvent finished = (FinishedEvent) event;
// note: observed script is possibly incomplete
String observedScript = finished.getScript();
return new ScriptPair(expectedScript, observedScript);
default:
throw new IllegalArgumentException("Unrecognized event kind: " + event.getKind());
}
} catch (SocketTimeoutException e) {
if (abortScheduled && !abortWritten) {
sendAbortCommand();
abortWritten = true;
}
// defensive clean-up in case Robot control server stalls during response to ABORT
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
}
} catch (ConnectException e) {
Exception exception = new Exception("Failed to connect. Is K3PO ready?", e);
exception.fillInStackTrace();
latch.notifyException(exception);
throw e;
} catch (Exception e) {
latch.notifyException(e);
throw e;
} finally {
latch.notifyFinished();
}
}
private void sendAbortCommand() throws Exception {
AbortCommand abort = new AbortCommand();
controller.writeCommand(abort);
}
public void awaitBarrier(String barrierName) throws InterruptedException {
if (!barriers.keySet().contains(barrierName)) {
throw new IllegalArgumentException(String.format(
"Barrier with %s is not present in the script and thus can't be waited upon", barrierName));
}
final CountDownLatch notifiedLatch = new CountDownLatch(1);
final BarrierStateMachine barrierStateMachine = barriers.get(barrierName);
barrierStateMachine.addListener(new BarrierStateListener() {
@Override
public void initial() {
// NOOP
}
@Override
public void notifying() {
// NOOP
}
@Override
public void notified() {
notifiedLatch.countDown();
}
});
try {
controller.await(barrierName);
} catch (Exception e) {
latch.notifyException(e);
}
notifiedLatch.await();
}
public void notifyBarrier(final String barrierName) throws InterruptedException {
if (!barriers.keySet().contains(barrierName)) {
throw new IllegalArgumentException(String.format(
"Barrier with %s is not present in the script and thus can't be notified", barrierName));
}
final CountDownLatch notifiedLatch = new CountDownLatch(1);
final BarrierStateMachine barrierStateMachine = barriers.get(barrierName);
barrierStateMachine.addListener(new BarrierStateListener() {
@Override
public void initial() {
barrierStateMachine.notifying();
// Only write to wire once
try {
controller.notifyBarrier(barrierName);
} catch (Exception e) {
latch.notifyException(e);
}
}
@Override
public void notifying() {
// NOOP
}
@Override
public void notified() {
notifiedLatch.countDown();
}
});
notifiedLatch.await();
}
private interface BarrierStateListener {
void initial();
void notified();
void notifying();
}
enum BarrierState {
INITIAL, NOTIFYING, NOTIFIED;
}
private class BarrierStateMachine implements BarrierStateListener {
private BarrierState state = INITIAL;
private List<BarrierStateListener> stateListeners = new ArrayList<>();
@Override
public void initial() {
synchronized (this) {
this.state = NOTIFYING;
for (BarrierStateListener listener : stateListeners) {
listener.initial();
}
}
}
@Override
public void notifying() {
synchronized (this) {
this.state = NOTIFYING;
for (BarrierStateListener listener : stateListeners) {
listener.notifying();
}
}
}
@Override
public void notified() {
synchronized (this) {
this.state = NOTIFIED;
for (BarrierStateListener listener : stateListeners) {
listener.notified();
}
}
}
public void addListener(BarrierStateListener stateListener) {
synchronized (this) {
switch (this.state) {
// notify right away if waiting on state
case INITIAL:
stateListener.initial();
break;
case NOTIFYING:
stateListener.notify();
break;
case NOTIFIED:
stateListener.notified();
break;
default:
break;
}
stateListeners.add(stateListener);
}
}
}
public void dispose() throws Exception {
try {
controller.dispose();
CommandEvent event = controller.readEvent();
// ensure it is the correct event
switch (event.getKind()) {
case DISPOSED:
latch.notifyDisposed();
break;
default:
throw new IllegalArgumentException("Unrecognized event kind: " + event.getKind());
}
} catch (Exception e) {
// TODO log this when we get a logger added to Junit, or remove need for this which always clean
// shutdown of k3po channels
e.printStackTrace();
// NOOP swallow exception as this is a clean up task that may fail in case setup didn't complete,
// expressions didn't get resolved. Etc. This happens frequently when Junit Assume is used, as K3po
// will have inited the accept channels outside of the test method.
}
finally {
controller.disconnect();
}
}
private static boolean isDebugging() {
List<String> arguments = ManagementFactory.getRuntimeMXBean().getInputArguments();
for (final String argument : arguments) {
if ("-Xdebug".equals(argument)) {
return true;
} else if (argument.startsWith("-agentlib:jdwp")) {
return true;
}
}
return false;
}
}