262 lines
9.8 KiB
Java
262 lines
9.8 KiB
Java
package jmri.jmrix.dccpp;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.DataInputStream;
|
|
import java.io.IOException;
|
|
import java.nio.charset.StandardCharsets;
|
|
|
|
import jmri.util.JUnitAppender;
|
|
import jmri.util.JUnitUtil;
|
|
|
|
import org.junit.jupiter.api.*;
|
|
|
|
/**
|
|
* <p>
|
|
* Title: DCCppPacketizerTest </p>
|
|
* <p>
|
|
*
|
|
* @author Bob Jacobsen Copyright (C) 2002
|
|
* @author Mark Underwood Copyright(C) 2015
|
|
*/
|
|
public class DCCppPacketizerTest extends DCCppTrafficControllerTest {
|
|
|
|
/**
|
|
* Local test class to make DCCppPacketizer more felicitous to test
|
|
*/
|
|
private static class StoppingDCCppPacketizer extends DCCppPacketizer {
|
|
|
|
StoppingDCCppPacketizer(jmri.jmrix.dccpp.DCCppCommandStation p) {
|
|
super(p);
|
|
}
|
|
|
|
// methods removed for testing
|
|
@Override
|
|
protected void handleTimeout(jmri.jmrix.AbstractMRMessage msg, jmri.jmrix.AbstractMRListener l) {
|
|
} // don't care about timeout
|
|
|
|
@Override
|
|
protected void reportReceiveLoopException(Exception e) {
|
|
}
|
|
|
|
@Override
|
|
protected void portWarn(Exception e) {
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void testOutbound() throws IOException {
|
|
DCCppPacketizer c = (DCCppPacketizer) tc;
|
|
// connect to iostream via port controller scaffold
|
|
DCCppPortControllerScaffold p = new DCCppPortControllerScaffold();
|
|
c.connectPort(p);
|
|
//c.startThreads();
|
|
DCCppMessage m = DCCppMessage.makeTurnoutCommandMsg(22, true);
|
|
m.setTimeout(1); // don't want to wait a long time
|
|
c.sendDCCppMessage(m, null);
|
|
log.debug("Message = {} length = {}", m.toString(), m.getNumDataElements());
|
|
JUnitUtil.waitFor(JUnitUtil.WAITFOR_DEFAULT_DELAY); // Allow time for other threads to send 4 characters
|
|
//assertEquals("total length ", 8, p.tostream.available());
|
|
assertEquals( '<', p.tostream.readByte() & 0xff, "Char 0");
|
|
assertEquals( 'T', p.tostream.readByte() & 0xff, "Char 1");
|
|
assertEquals( ' ', p.tostream.readByte() & 0xff, "Char 2");
|
|
assertEquals( '2', p.tostream.readByte() & 0xff, "Char 3");
|
|
assertEquals( '2', p.tostream.readByte() & 0xff, "Char 4");
|
|
assertEquals( ' ', p.tostream.readByte() & 0xff, "Char 5");
|
|
assertEquals( '1', p.tostream.readByte() & 0xff, "Char 6");
|
|
assertEquals( '>', p.tostream.readByte() & 0xff, "Char 7");
|
|
assertEquals( 0, p.tostream.available(), "remaining ");
|
|
}
|
|
|
|
@Test
|
|
public void testInbound() throws IOException {
|
|
DCCppPacketizer c = (DCCppPacketizer) tc;
|
|
|
|
log.debug("Running testInbound() test");
|
|
|
|
// connect to iostream via port controller
|
|
DCCppPortControllerScaffold p = new DCCppPortControllerScaffold();
|
|
c.connectPort(p);
|
|
|
|
// object to receive reply
|
|
DCCppListenerScaffold l = new DCCppListenerScaffold();
|
|
c.addDCCppListener(~0, l);
|
|
|
|
// now send reply
|
|
// NOTE: The PortControllerScaffold doesn't model the real PortController, which will
|
|
// pre-strip the < > characters out of the stream before forwarding it.
|
|
// So we don't include them here.
|
|
p.tistream.write('<');
|
|
p.tistream.write('H');
|
|
p.tistream.write(' ');
|
|
p.tistream.write('2');
|
|
p.tistream.write('2');
|
|
p.tistream.write(' ');
|
|
p.tistream.write('1');
|
|
p.tistream.write('>');
|
|
|
|
// check that the message was picked up by the read thread.
|
|
assertTrue( waitForReply(l), "reply received");
|
|
log.debug("Reply string = {} length = {}", l.rcvdRply.toString(), l.rcvdRply.getNumDataElements());
|
|
assertEquals( 'H', l.rcvdRply.getElement(0) & 0xFF, "Char 0 ");
|
|
assertEquals( ' ', l.rcvdRply.getElement(1) & 0xFF, "Char 1 ");
|
|
assertEquals( '2', l.rcvdRply.getElement(2) & 0xFF, "Char 2 ");
|
|
assertEquals( '2', l.rcvdRply.getElement(3) & 0xFF, "Char 3 ");
|
|
assertEquals( ' ', l.rcvdRply.getElement(4) & 0xFF, "Char 4 ");
|
|
assertEquals( '1', l.rcvdRply.getElement(5) & 0xFF, "Char 5 ");
|
|
}
|
|
|
|
@Test
|
|
public void testInboundCaptionWithGreaterThan() throws IOException {
|
|
// A '>' inside the quoted caption must not terminate the message early.
|
|
checkAutomationCaption("Round > the bend");
|
|
}
|
|
|
|
@Test
|
|
public void testInboundCaptionWithLessThan() throws IOException {
|
|
// A '<' inside the quoted caption must pass through unchanged.
|
|
checkAutomationCaption("Less < than");
|
|
}
|
|
|
|
@Test
|
|
public void testInboundCaptionWithBothAngleBrackets() throws IOException {
|
|
// Both characters in the same caption.
|
|
checkAutomationCaption("Round > and < the bend");
|
|
}
|
|
|
|
/**
|
|
* Feed a <jA ...> automation-id reply with the given caption through the
|
|
* packetizer and assert the full body is delivered, the reply still matches
|
|
* AUTOMATION_ID_REPLY_REGEX, and the caption is preserved verbatim.
|
|
*/
|
|
private void checkAutomationCaption(String caption) throws IOException {
|
|
DCCppPacketizer c = (DCCppPacketizer) tc;
|
|
DCCppPortControllerScaffold p = new DCCppPortControllerScaffold();
|
|
c.connectPort(p);
|
|
|
|
DCCppListenerScaffold l = new DCCppListenerScaffold();
|
|
c.addDCCppListener(~0, l);
|
|
|
|
String body = "jA 4 A \"" + caption + "\"";
|
|
p.tistream.write('<');
|
|
for (byte b : body.getBytes(StandardCharsets.US_ASCII)) {
|
|
p.tistream.write(b);
|
|
}
|
|
p.tistream.write('>');
|
|
|
|
assertTrue( waitForReply(l), "reply received");
|
|
assertEquals( body, l.rcvdRply.toString(), "full body delivered");
|
|
assertTrue( l.rcvdRply.isAutomationIDReply(), "reply still matches AUTOMATION_ID_REPLY_REGEX");
|
|
assertEquals( caption, l.rcvdRply.getAutomationDescString(), "caption preserved verbatim");
|
|
}
|
|
|
|
private boolean waitForReply(DCCppListenerScaffold l) {
|
|
// wait for reply (normally, done by callback; will check that later)
|
|
int i = 0;
|
|
while (l.rcvdRply == null && i++ < 100) {
|
|
JUnitUtil.waitFor(JUnitUtil.WAITFOR_DEFAULT_DELAY);
|
|
}
|
|
if (log.isDebugEnabled()) {
|
|
log.debug("past loop, i={} reply={}", i, l.rcvdRply);
|
|
}
|
|
if (i == 0) {
|
|
log.warn("waitForReply saw an immediate return; is threading right?");
|
|
}
|
|
return i < 100;
|
|
}
|
|
|
|
// --- readFrameBody direct tests (no threading required) ---
|
|
|
|
@Test
|
|
public void testReadFrameBodyNormalMessage() throws IOException {
|
|
assertEquals("H 22 1", parseBody("H 22 1>"));
|
|
}
|
|
|
|
@Test
|
|
public void testReadFrameBodyQuotedGreaterThan() throws IOException {
|
|
assertEquals("jA 4 A \"Round > bend\"", parseBody("jA 4 A \"Round > bend\">"));
|
|
}
|
|
|
|
@Test
|
|
public void testReadFrameBodyBroadcastSimple() throws IOException {
|
|
assertEquals("* hello world *", parseBody("* hello world *>"));
|
|
}
|
|
|
|
@Test
|
|
public void testReadFrameBodyBroadcastWithGreaterThan() throws IOException {
|
|
assertEquals("* value > 16 *", parseBody("* value > 16 *>"));
|
|
}
|
|
|
|
@Test
|
|
public void testReadFrameBodyBroadcastWithNestedAngles() throws IOException {
|
|
// The actual failure case: DCC-EX rejects <V 1025 0> and embeds the
|
|
// command in the broadcast body — the '>' inside must not split the frame.
|
|
String body = "* Command format <V cv value> failed CHECK(cv 1 .. 1023)\n <V 1025 0> *";
|
|
assertEquals(body, parseBody(body + ">"));
|
|
}
|
|
|
|
@Test
|
|
public void testReadFrameBodyBroadcastWithCommandContainingLessThan() throws IOException {
|
|
// '<' in a quoted arg inside the broadcast body, e.g. echo of <m 0 1 "voltage < 16 ">.
|
|
String body = "* Command format <m 0 1 \"voltage < 16 \"> failed *";
|
|
assertEquals(body, parseBody(body + ">"));
|
|
}
|
|
|
|
@Test
|
|
public void testInboundBroadcastMessage() throws IOException {
|
|
// Full pipeline test: verify broadcast frames reach parseReply and are recognised as diag replies.
|
|
DCCppPacketizer c = (DCCppPacketizer) tc;
|
|
DCCppPortControllerScaffold p = new DCCppPortControllerScaffold();
|
|
c.connectPort(p);
|
|
DCCppListenerScaffold l = new DCCppListenerScaffold();
|
|
c.addDCCppListener(~0, l);
|
|
|
|
String payload = "* Command format <V cv value> failed CHECK(cv 1 .. 1023) <V 1025 0> *";
|
|
p.tistream.write('<');
|
|
for (byte b : payload.getBytes(StandardCharsets.US_ASCII)) {
|
|
p.tistream.write(b);
|
|
}
|
|
p.tistream.write('>');
|
|
|
|
assertTrue(waitForReply(l), "reply received");
|
|
assertEquals(payload, l.rcvdRply.toString(), "full body delivered");
|
|
assertTrue(l.rcvdRply.isDiagReply(), "reply parsed as diag");
|
|
}
|
|
|
|
/** Synchronous helper: feed raw bytes directly to readFrameBody and return the result. */
|
|
private String parseBody(String input) throws IOException {
|
|
DataInputStream is = new DataInputStream(
|
|
new ByteArrayInputStream(input.getBytes(StandardCharsets.US_ASCII)));
|
|
return ((DCCppPacketizer) tc).readFrameBody(is, 2048);
|
|
}
|
|
|
|
@Test
|
|
@Override
|
|
public void testPortReadyToSendNullController() {
|
|
super.testPortReadyToSendNullController();
|
|
JUnitAppender.suppressWarnMessageStartsWith("DCC-EX port not ready to send");
|
|
}
|
|
|
|
@BeforeEach
|
|
@Override
|
|
public void setUp() {
|
|
JUnitUtil.setUp();
|
|
DCCppCommandStation lcs = new DCCppCommandStation();
|
|
tc = new StoppingDCCppPacketizer(lcs);
|
|
}
|
|
|
|
@AfterEach
|
|
@Override
|
|
public void tearDown() {
|
|
tc.terminateThreads();
|
|
tc = null;
|
|
JUnitUtil.resetWindows(false, false);
|
|
JUnitUtil.tearDown();
|
|
}
|
|
|
|
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DCCppPacketizerTest.class);
|
|
|
|
}
|