Files
JIMRI/java/test/jmri/jmrix/dccpp/DCCppPacketizerTest.java
2026-06-17 14:00:51 +02:00

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