332 lines
12 KiB
Java
332 lines
12 KiB
Java
package jmri.jmrix.openlcb;
|
|
|
|
import org.mockito.ArgumentMatcher;
|
|
import org.mockito.verification.VerificationMode;
|
|
import org.openlcb.AbstractConnection;
|
|
import org.openlcb.Connection;
|
|
import org.openlcb.InitializationCompleteMessage;
|
|
import org.openlcb.Message;
|
|
import org.openlcb.NodeID;
|
|
import org.openlcb.OlcbInterface;
|
|
import org.openlcb.can.AliasMap;
|
|
import org.openlcb.can.CanFrame;
|
|
import org.openlcb.can.GridConnect;
|
|
import org.openlcb.can.MessageBuilder;
|
|
import org.openlcb.can.OpenLcbCanFrame;
|
|
|
|
import java.util.List;
|
|
import java.util.concurrent.BlockingQueue;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
import java.util.concurrent.Semaphore;
|
|
|
|
import static org.mockito.Mockito.*;
|
|
|
|
/**
|
|
* Test helper class that instantiates an OlcbInterface and allows making expectations on what is sent to the bus, as
|
|
* well as allows injecting response messages from the bus.
|
|
* <p>
|
|
* Created by bracz on 1/9/16.
|
|
*/
|
|
public class OlcbTestHelper {
|
|
// This mock object is handed over to the interface to have all messages sent to it. Expectations on sent messages
|
|
// are on this object.
|
|
protected Connection outputConnectionMock = mock(AbstractConnection.class);
|
|
// Interface object that collects all critical OpenLCB components.
|
|
protected OlcbInterface iface = null;
|
|
protected AliasMap aliasMap = new AliasMap();
|
|
// If true, messages injected by the test script using sendMessage will be turned into CAN frames first, and
|
|
// then the respective frames will be sent to the OpenLCB stack. Note that these are the *incoming* messages
|
|
// from the bus.
|
|
protected boolean testWithCanFrameRendering = false;
|
|
// Prints injected frames from sendMessage.
|
|
private boolean debugFrames = false;
|
|
// Connection wrapper that allows capturing and printing the *outgoing* messages that are generated by us and sent
|
|
// to the bus.
|
|
private FakeConnection fakeConnection;
|
|
// Helper object for single-threaded operation.
|
|
FakeExecutionThread thread;
|
|
|
|
/* A Connection class that forwards all messages to a
|
|
* mock connection passed in at construction time.
|
|
* Supports printing the messages for debugging.
|
|
*/
|
|
public static class FakeConnection extends AbstractConnection {
|
|
private final Connection mock;
|
|
public boolean debugMessages = false;
|
|
|
|
public FakeConnection(Connection mock) {
|
|
this.mock = mock;
|
|
}
|
|
|
|
@Override
|
|
public synchronized void put(Message msg, Connection sender) {
|
|
if (debugMessages) {
|
|
System.err.println("Mock send: " + msg.toString());
|
|
}
|
|
mock.put(msg, sender);
|
|
}
|
|
|
|
}
|
|
|
|
public OlcbTestHelper() {
|
|
expectInit();
|
|
}
|
|
|
|
public void dispose() {
|
|
if (thread != null) {
|
|
thread.waitForEmpty();
|
|
}
|
|
expectNoMessages();
|
|
iface.dispose();
|
|
iface = null;
|
|
}
|
|
|
|
/**
|
|
* Initializes the interface objects, and clears the startup messages in the mock.
|
|
*/
|
|
private void expectInit() {
|
|
NodeID id = new NodeID(new byte[]{1, 2, 0, 0, 1, 1});
|
|
aliasMap.insert(0x333, id);
|
|
fakeConnection = new FakeConnection(outputConnectionMock);
|
|
iface = new OlcbInterface(id, fakeConnection);
|
|
expectMessage(new InitializationCompleteMessage(iface.getNodeId()));
|
|
}
|
|
|
|
/**
|
|
* If a test calls this function, all outgoing messages will be routed through a single send thread.
|
|
*/
|
|
public void enableSingleThreaded() {
|
|
thread = new FakeExecutionThread();
|
|
iface.setLoopbackThread(thread);
|
|
iface.runOnThreadPool(thread);
|
|
}
|
|
|
|
/**
|
|
* Helper class for the single-threaded operation. This class gets plugged into the OlcbInterface as the callback
|
|
* for executing outgoing message send operations.
|
|
*/
|
|
static class FakeExecutionThread implements OlcbInterface.SyncExecutor, Runnable {
|
|
private final BlockingQueue<QEntry> outputQueue = new LinkedBlockingQueue<>();
|
|
|
|
/**
|
|
* Blocks until the send thread has no more runnables.
|
|
*/
|
|
public void waitForEmpty() {
|
|
while (!outputQueue.isEmpty()) {
|
|
try {
|
|
Thread.sleep(20);
|
|
} catch (InterruptedException e) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void schedule(Runnable r) throws InterruptedException {
|
|
QEntry q = new QEntry(r);
|
|
outputQueue.add(q);
|
|
q.sem.acquire();
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
while (!Thread.currentThread().isInterrupted()) {
|
|
QEntry m;
|
|
try {
|
|
m = outputQueue.take();
|
|
m.callback.run();
|
|
m.sem.release();
|
|
} catch (InterruptedException e) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class QEntry {
|
|
QEntry(Runnable r) {
|
|
callback = r;
|
|
}
|
|
|
|
Runnable callback;
|
|
Semaphore sem = new Semaphore(0);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Defines an alias for a remote node. This is equivalent to sending an AMD frame for the given alias and node ID
|
|
* via sendFrame. It essentially simulates a remote node showing up on the bus.
|
|
*
|
|
* @param alias node alias
|
|
* @param nid node ID
|
|
*/
|
|
public void setRemoteAlias(int alias, NodeID nid) {
|
|
aliasMap.insert(alias, nid);
|
|
}
|
|
|
|
/** Prints all outgoing messages that get sent to the mock. For debugging purposes. */
|
|
public void printAllSentMessages() {
|
|
fakeConnection.debugMessages = true;
|
|
}
|
|
|
|
/**
|
|
* Sends one or more OpenLCB message, as represented by the given CAN frames, to the interface's inbound port. This
|
|
* represents traffic that a far away node is sending. The frame should be specified in the GridConnect protocol.
|
|
*
|
|
* @param frames is one or more CAN frames in the GridConnect protocol format.
|
|
*/
|
|
protected void sendFrame(String frames) {
|
|
List<CanFrame> parsedFrames = GridConnect.parse(frames);
|
|
MessageBuilder d = new MessageBuilder(aliasMap);
|
|
for (CanFrame f : parsedFrames) {
|
|
aliasMap.processFrame(new OpenLcbCanFrame(f));
|
|
List<Message> l = d.processFrame(f);
|
|
if (l != null) {
|
|
for (Message m : l) {
|
|
iface.getInputConnection().put(m, null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends an OpenLCB message to the interface's inbound port. This represents traffic that a far away node is
|
|
* sending.
|
|
*
|
|
* @param msg inbound message from a far node
|
|
*/
|
|
protected void sendMessage(Message msg) {
|
|
if (testWithCanFrameRendering) {
|
|
MessageBuilder d = new MessageBuilder(aliasMap);
|
|
List<? extends CanFrame> actualFrames = d.processMessage(msg);
|
|
StringBuilder b = new StringBuilder();
|
|
for (CanFrame f : actualFrames) {
|
|
b.append(GridConnect.format(f));
|
|
}
|
|
if (debugFrames)
|
|
System.err.println("Input frames: " + b);
|
|
sendFrame(b.toString());
|
|
} else {
|
|
iface.getInputConnection().put(msg, null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves all outgoing messages to the pending messages queue.
|
|
*/
|
|
protected void consumeMessages() {
|
|
iface.flushSendQueue();
|
|
}
|
|
|
|
/**
|
|
* Expects that the next outgoing message (not yet matched with an expectation) is the given CAN frame.
|
|
*
|
|
* @param expectedFrame GridConnect-formatted CAN frame.
|
|
* @param cardinality how many times to expect this frame, e.g. 'times(2)' or omit for once.
|
|
*/
|
|
protected void expectFrame(String expectedFrame, VerificationMode cardinality) {
|
|
class MessageMatchesFrame implements ArgumentMatcher<Message> {
|
|
private final String frame;
|
|
private String actual = "";
|
|
|
|
public MessageMatchesFrame(String frame) {
|
|
this.frame = frame;
|
|
}
|
|
|
|
@Override
|
|
public boolean matches(Message message) {
|
|
MessageBuilder d = new MessageBuilder(aliasMap);
|
|
List<? extends CanFrame> actualFrames = d.processMessage(message);
|
|
StringBuilder b = new StringBuilder();
|
|
for (CanFrame f : actualFrames) {
|
|
b.append(GridConnect.format(f));
|
|
}
|
|
String r = b.toString();
|
|
if (!frame.equals(r)) {
|
|
actual = actual + r;
|
|
}
|
|
return frame.equals(r);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
//printed in verification errors
|
|
return "[OpenLCB message with CAN rendering of " + frame + "][actual " + actual + "]";
|
|
}
|
|
|
|
}
|
|
|
|
consumeMessages();
|
|
verify(outputConnectionMock, cardinality).put(argThat(new MessageMatchesFrame(expectedFrame)), any());
|
|
}
|
|
|
|
/**
|
|
* Expects that the next outgoing message (not yet matched with an expectation) is the given CAN frame.
|
|
*
|
|
* @param expectedFrame GridConnect-formatted CAN frame.
|
|
*/
|
|
protected void expectFrame(String expectedFrame) {
|
|
expectFrame(expectedFrame, times(1));
|
|
}
|
|
|
|
|
|
/**
|
|
* Expects that the next outgoing message (not yet matched with an expectation) is the given message.
|
|
*
|
|
* @param expectedMessage message that should have been sent to the bus from the local stack.
|
|
*/
|
|
protected void expectMessage(Message expectedMessage) {
|
|
consumeMessages();
|
|
verify(outputConnectionMock).put(eq(expectedMessage), any());
|
|
}
|
|
|
|
/**
|
|
* Expects a single outgoing message and that no other outgoing messages are unmatched by expectations.
|
|
*
|
|
* @param expectedMessage message that should have been sent to the bus from the local stack.
|
|
*/
|
|
protected void expectMessageAndNoMore(Message expectedMessage) {
|
|
expectMessage(expectedMessage);
|
|
expectNoMessages();
|
|
}
|
|
|
|
/**
|
|
* Expects that there are no unconsumed outgoing messages.
|
|
*/
|
|
protected void expectNoFrames() {
|
|
consumeMessages();
|
|
verifyNoMoreInteractions(outputConnectionMock);
|
|
clearInvocations(outputConnectionMock);
|
|
}
|
|
|
|
/**
|
|
* Expects that there are no unconsumed outgoing messages.
|
|
*/
|
|
protected void expectNoMessages() {
|
|
expectNoFrames();
|
|
}
|
|
|
|
/**
|
|
* Simulates an interaction initiated by a remote node on the OpenLCB bus.
|
|
* @param send The remote node sends this frame to the bus (i.e., this frame will be injected as an inbound frame).
|
|
* @param expect The local stack is expected to react with this frame. (i.e., this frame will be consumed from the
|
|
* outbound frames generated by the local stack).
|
|
*/
|
|
protected void sendFrameAndExpectResult(String send, String expect) {
|
|
sendFrame(send);
|
|
expectFrame(expect);
|
|
expectNoFrames();
|
|
}
|
|
|
|
/**
|
|
* Simulates an interaction initiated by a remote node on the OpenLCB bus.
|
|
* @param send The remote node sends this frmessage to the bus (i.e., this message will be injected inbound).
|
|
* @param expect The local stack is expected to react with this message. (i.e., this message will be consumed from the
|
|
* outbound messages generated by the local stack).
|
|
*/
|
|
protected void sendMessageAndExpectResult(Message send, Message expect) {
|
|
sendMessage(send);
|
|
expectMessage(expect);
|
|
}
|
|
}
|