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. *

* 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 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 parsedFrames = GridConnect.parse(frames); MessageBuilder d = new MessageBuilder(aliasMap); for (CanFrame f : parsedFrames) { aliasMap.processFrame(new OpenLcbCanFrame(f)); List 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 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 { 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 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); } }