Files
2026-06-17 14:00:51 +02:00

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);
}
}