This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class EbcdicTranslatorTest {
|
||||
|
||||
private final EbcdicTranslator translator = new EbcdicTranslator();
|
||||
|
||||
@Test
|
||||
public void testBasicAlphanumericTranslation() {
|
||||
assertEquals('A', translator.ebcdicToUnicode(0xC1));
|
||||
assertEquals('B', translator.ebcdicToUnicode(0xC2));
|
||||
assertEquals('Z', translator.ebcdicToUnicode(0xE9));
|
||||
assertEquals('a', translator.ebcdicToUnicode(0x81));
|
||||
assertEquals('z', translator.ebcdicToUnicode(0xA9));
|
||||
assertEquals('0', translator.ebcdicToUnicode(0xF0));
|
||||
assertEquals('9', translator.ebcdicToUnicode(0xF9));
|
||||
assertEquals(' ', translator.ebcdicToUnicode(0x40));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReverseTranslation() {
|
||||
assertEquals(0xC1, translator.unicodeToEbcdic('A'));
|
||||
assertEquals(0x81, translator.unicodeToEbcdic('a'));
|
||||
assertEquals(0xF0, translator.unicodeToEbcdic('0'));
|
||||
assertEquals(0x40, translator.unicodeToEbcdic(' '));
|
||||
assertEquals(0x4B, translator.unicodeToEbcdic('.'));
|
||||
assertEquals(0x6B, translator.unicodeToEbcdic(','));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSpecialCharactersCP037() {
|
||||
// [ and ]
|
||||
assertEquals(0xBA, translator.unicodeToEbcdic('['));
|
||||
assertEquals(0xBB, translator.unicodeToEbcdic(']'));
|
||||
assertEquals('[', translator.ebcdicToUnicode(0xBA));
|
||||
assertEquals(']', translator.ebcdicToUnicode(0xBB));
|
||||
|
||||
// { and }
|
||||
assertEquals(0xC0, translator.unicodeToEbcdic('{'));
|
||||
assertEquals(0xD0, translator.unicodeToEbcdic('}'));
|
||||
assertEquals('{', translator.ebcdicToUnicode(0xC0));
|
||||
assertEquals('}', translator.ebcdicToUnicode(0xD0));
|
||||
|
||||
// Backslash \
|
||||
assertEquals(0xE0, translator.unicodeToEbcdic('\\'));
|
||||
assertEquals('\\', translator.ebcdicToUnicode(0xE0));
|
||||
|
||||
// Caret ^
|
||||
assertEquals(0xB0, translator.unicodeToEbcdic('^'));
|
||||
assertEquals('^', translator.ebcdicToUnicode(0xB0));
|
||||
|
||||
// Tilde ~
|
||||
assertEquals(0xA1, translator.unicodeToEbcdic('~'));
|
||||
assertEquals('~', translator.ebcdicToUnicode(0xA1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStringRoundTrip() {
|
||||
String test = "Hello World! 123 [TEST] {VAR} \\";
|
||||
byte[] ebcdic = translator.stringToEbcdic(test);
|
||||
String restored = translator.ebcdicToString(ebcdic, 0, ebcdic.length);
|
||||
assertEquals(test, restored);
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
public class DataStreamProcessorTest {
|
||||
|
||||
private final EbcdicTranslator translator = new EbcdicTranslator();
|
||||
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
private final DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
|
||||
|
||||
@Test
|
||||
public void testEraseWriteAndSBAOrder() {
|
||||
// Build an EW record:
|
||||
// Byte 0: CMD_EW (0x05)
|
||||
// Byte 1: WCC (0xC3) - restore keyboard, reset MDT
|
||||
// Byte 2: ORDER_SBA (0x11)
|
||||
// Byte 3, 4: Encoded address for 0 (0x40, 0x40)
|
||||
// Byte 5: ORDER_SF (0x1D)
|
||||
// Byte 6: FA (0xC0) - unprotected
|
||||
// Byte 7..11: EBCDIC "HELLO" (0xC8, 0xC5, 0xD3, 0xD3, 0xD6)
|
||||
byte[] record = new byte[] {
|
||||
(byte) CMD_EW,
|
||||
(byte) 0xC3,
|
||||
(byte) ORDER_SBA,
|
||||
(byte) 0x40, (byte) 0x40,
|
||||
(byte) ORDER_SF,
|
||||
(byte) FA_PRINTABLE,
|
||||
(byte) 0xC8, (byte) 0xC5, (byte) 0xD3, (byte) 0xD3, (byte) 0xD6
|
||||
};
|
||||
|
||||
processor.processRecord(record, 0, record.length, true);
|
||||
|
||||
assertTrue(screen.isFormatted());
|
||||
assertTrue(screen.getCell(0).isFieldAttribute());
|
||||
|
||||
// Positions 1..5 should contain "HELLO"
|
||||
assertEquals('H', screen.getCell(1).ucs4);
|
||||
assertEquals('E', screen.getCell(2).ucs4);
|
||||
assertEquals('L', screen.getCell(3).ucs4);
|
||||
assertEquals('L', screen.getCell(4).ucs4);
|
||||
assertEquals('O', screen.getCell(5).ucs4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRepeatToAddressOrder() {
|
||||
// Write record with RA from 0 to 10 with character 'A' (0xC1)
|
||||
byte[] record = new byte[] {
|
||||
(byte) CMD_EW,
|
||||
(byte) 0xC3,
|
||||
(byte) ORDER_SBA,
|
||||
(byte) 0x40, (byte) 0x40, // Address 0
|
||||
(byte) ORDER_RA,
|
||||
(byte) 0x40, (byte) 0x4A, // Address 10 (0x40, 0x4A)
|
||||
(byte) 0xC1 // EBCDIC 'A'
|
||||
};
|
||||
|
||||
processor.processRecord(record, 0, record.length, true);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
assertEquals('A', screen.getCell(i).ucs4, "Cell " + i + " should be 'A'");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadBufferPropagatesLastAid() {
|
||||
haus.nightmare.lib3270j.input.InputProcessor input = new haus.nightmare.lib3270j.input.InputProcessor(screen, translator, null);
|
||||
processor.setInputProcessor(input);
|
||||
input.setLastAid(AID_ENTER);
|
||||
|
||||
java.util.concurrent.atomic.AtomicReference<byte[]> sentData = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
processor.setOutputSender(sentData::set);
|
||||
|
||||
byte[] rbRecord = new byte[] { (byte) CMD_RB };
|
||||
processor.processRecord(rbRecord, 0, rbRecord.length, true);
|
||||
|
||||
assertNotNull(sentData.get());
|
||||
assertEquals((byte) AID_ENTER, sentData.get()[0], "ReadBuffer must transmit operator's stored AID");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.graphics.GraphicsMode;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
public class QueryReplyBuilderTest {
|
||||
|
||||
private final EbcdicTranslator translator = new EbcdicTranslator();
|
||||
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen);
|
||||
|
||||
@Test
|
||||
public void testBuildAllQueryRepliesDefaultBoth() {
|
||||
assertEquals(GraphicsMode.BOTH, qrBuilder.getGraphicsMode());
|
||||
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||
assertNotNull(replies);
|
||||
assertTrue(replies.length > 0);
|
||||
assertEquals((byte) AID_SF, replies[0]);
|
||||
|
||||
// In GraphicsMode.BOTH, Vector Graphics QR 0xB0 must be present
|
||||
boolean hasB0 = false;
|
||||
for (int i = 0; i < replies.length - 3; i++) {
|
||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
||||
hasB0 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(hasB0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildAllQueryRepliesExplicitNone() {
|
||||
qrBuilder.setGraphicsMode(GraphicsMode.NONE);
|
||||
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||
assertNotNull(replies);
|
||||
assertTrue(replies.length > 0);
|
||||
assertEquals((byte) AID_SF, replies[0]);
|
||||
|
||||
// In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT be present
|
||||
boolean hasB0 = false;
|
||||
for (int i = 0; i < replies.length - 3; i++) {
|
||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
||||
hasB0 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertFalse(hasB0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildAllQueryRepliesWithVectorGraphics() {
|
||||
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
|
||||
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||
assertNotNull(replies);
|
||||
|
||||
// Vector Graphics QR 0xB0 and 0xB4 must be present
|
||||
boolean hasB0 = false;
|
||||
boolean hasB4 = false;
|
||||
for (int i = 0; i < replies.length - 3; i++) {
|
||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
||||
hasB0 = true;
|
||||
}
|
||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GCOLOR) {
|
||||
hasB4 = true;
|
||||
}
|
||||
}
|
||||
assertTrue(hasB0);
|
||||
assertTrue(hasB4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildAllQueryRepliesWithBoth() {
|
||||
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
|
||||
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||
assertNotNull(replies);
|
||||
|
||||
// Vector Graphics (0xB0) must be present and Charsets must have LoadPS (0x0A)
|
||||
boolean hasB0 = false;
|
||||
boolean hasCharsetsWithLoadPs = false;
|
||||
for (int i = 0; i < replies.length - 3; i++) {
|
||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
||||
hasB0 = true;
|
||||
}
|
||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
|
||||
if (i + 6 < replies.length && (replies[i + 6] & 0xFF) == 0x0A) {
|
||||
hasCharsetsWithLoadPs = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(hasB0);
|
||||
assertTrue(hasCharsetsWithLoadPs);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildAllQueryRepliesWithProgrammedSymbols() {
|
||||
qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS);
|
||||
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||
assertNotNull(replies);
|
||||
|
||||
// Charsets with LoadPS (0x0A) must be present, and QR_GRAPHICS must NOT be present
|
||||
boolean hasB0 = false;
|
||||
boolean hasCharsetsWithLoadPs = false;
|
||||
for (int i = 0; i < replies.length - 3; i++) {
|
||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
||||
hasB0 = true;
|
||||
}
|
||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
|
||||
if (i + 6 < replies.length && (replies[i + 6] & 0xFF) == 0x0A) {
|
||||
hasCharsetsWithLoadPs = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
assertFalse(hasB0);
|
||||
assertTrue(hasCharsetsWithLoadPs);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildFilteredQueryRepliesWithSupportedCodes() {
|
||||
byte[] requested = new byte[] { (byte) QR_DDM, (byte) QR_USABLE_AREA };
|
||||
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||
|
||||
assertNotNull(replies);
|
||||
assertEquals((byte) AID_SF, replies[0]);
|
||||
|
||||
// First SF should be DDM: length (2 bytes), SFID_QREPLY (0x81), QR_DDM (0x95)
|
||||
int len1 = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
|
||||
assertEquals(0x81, replies[3] & 0xFF);
|
||||
assertEquals(QR_DDM, replies[4] & 0xFF);
|
||||
|
||||
// Second SF should be Usable Area
|
||||
int pos2 = 1 + len1;
|
||||
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
|
||||
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
|
||||
assertEquals(QR_USABLE_AREA, replies[pos2 + 3] & 0xFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildFilteredQueryRepliesWithUnsupportedCode() {
|
||||
byte[] requested = new byte[] { (byte) 0x77 }; // Unsupported code
|
||||
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||
|
||||
assertNotNull(replies);
|
||||
assertEquals((byte) AID_SF, replies[0]);
|
||||
// Should emit QR_NULL (0xFF)
|
||||
assertEquals(0x81, replies[3] & 0xFF);
|
||||
assertEquals(QR_NULL, replies[4] & 0xFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImplicitPartitionModel4() {
|
||||
byte[] requested = new byte[] { (byte) QR_IMP_PART };
|
||||
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||
|
||||
assertNotNull(replies);
|
||||
assertEquals((byte) AID_SF, replies[0]);
|
||||
int len = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
|
||||
assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY
|
||||
assertEquals(QR_IMP_PART, replies[4] & 0xFF); // 0xA6
|
||||
|
||||
// Payload starts at index 5: 22 bytes total
|
||||
// Default size = 80 x 24
|
||||
int defCols = ((replies[10] & 0xFF) << 8) | (replies[11] & 0xFF);
|
||||
int defRows = ((replies[12] & 0xFF) << 8) | (replies[13] & 0xFF);
|
||||
assertEquals(80, defCols);
|
||||
assertEquals(24, defRows);
|
||||
|
||||
// Alt size = 80 x 43
|
||||
int altCols = ((replies[14] & 0xFF) << 8) | (replies[15] & 0xFF);
|
||||
int altRows = ((replies[16] & 0xFF) << 8) | (replies[17] & 0xFF);
|
||||
assertEquals(80, altCols);
|
||||
assertEquals(43, altRows);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProgrammedSymbolsDescriptorsSingleAndTriplePlane() {
|
||||
qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS);
|
||||
byte[] requested = new byte[] { (byte) QR_CHARSETS };
|
||||
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||
|
||||
assertNotNull(replies);
|
||||
int offset = -1;
|
||||
for (int i = 0; i < replies.length - 3; i++) {
|
||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
|
||||
offset = i + 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(offset >= 0, "QR_CHARSETS must be present");
|
||||
|
||||
// QR_CHARSETS payload structure:
|
||||
// Flags (2 bytes), SDW (1 byte), SDH (1 byte), Form (1 byte), DevType (2 bytes), Res (1 byte), DL (1 byte)
|
||||
// DL is at offset + 8, and is 7 bytes per descriptor.
|
||||
int dl = replies[offset + 8] & 0xFF;
|
||||
assertEquals(7, dl);
|
||||
|
||||
int descOffset = offset + 9;
|
||||
|
||||
// Descriptor 1: SET 0 (Base) -> flags 0x10
|
||||
assertEquals(0x00, replies[descOffset] & 0xFF);
|
||||
assertEquals(0x10, replies[descOffset + 1] & 0xFF);
|
||||
|
||||
// Descriptor 2: SET 1 (APL) -> flags 0x00
|
||||
assertEquals(0x01, replies[descOffset + 7] & 0xFF);
|
||||
assertEquals(0x00, replies[descOffset + 7 + 1] & 0xFF);
|
||||
|
||||
// Descriptor 3: PSA (Single plane) -> flags 0x80 (Loadable, single-plane)
|
||||
assertEquals(0x02, replies[descOffset + 14] & 0xFF);
|
||||
assertEquals(0x80, replies[descOffset + 14 + 1] & 0xFF);
|
||||
|
||||
// Descriptor 4: PSB (Single plane) -> flags 0x80 (Loadable, single-plane)
|
||||
assertEquals(0x03, replies[descOffset + 21] & 0xFF);
|
||||
assertEquals(0x80, replies[descOffset + 21 + 1] & 0xFF);
|
||||
|
||||
// Descriptor 5: PSC (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
|
||||
assertEquals(0x04, replies[descOffset + 28] & 0xFF);
|
||||
assertEquals(0xC0, replies[descOffset + 28 + 1] & 0xFF);
|
||||
|
||||
// Descriptor 6: PSD (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
|
||||
assertEquals(0x05, replies[descOffset + 35] & 0xFF);
|
||||
assertEquals(0xC0, replies[descOffset + 35 + 1] & 0xFF);
|
||||
|
||||
// Descriptor 7: PSE (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
|
||||
assertEquals(0x06, replies[descOffset + 42] & 0xFF);
|
||||
assertEquals(0xC0, replies[descOffset + 42 + 1] & 0xFF);
|
||||
|
||||
// Descriptor 8: PSF (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
|
||||
assertEquals(0x07, replies[descOffset + 49] & 0xFF);
|
||||
assertEquals(0xC0, replies[descOffset + 49 + 1] & 0xFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryReplyImageAndRpqNames() {
|
||||
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
|
||||
byte[] requested = new byte[] { (byte) QR_IMAGE, (byte) QR_RPQNAMES, (byte) QR_GIMAGE };
|
||||
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||
|
||||
assertNotNull(replies);
|
||||
assertEquals((byte) AID_SF, replies[0]);
|
||||
|
||||
// First SF should be QR_NULL (0xFF) because 0x82 is 3270 Image SF (not supported on 3179G)
|
||||
int len1 = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
|
||||
assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY
|
||||
assertEquals(QR_NULL, replies[4] & 0xFF); // 0xFF
|
||||
|
||||
// Second SF should be QR_RPQNAMES (0xA1) matching requested code
|
||||
int pos2 = 1 + len1;
|
||||
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
|
||||
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
|
||||
assertEquals(QR_RPQNAMES, replies[pos2 + 3] & 0xFF); // 0xA1
|
||||
|
||||
// Third SF should be QR_GIMAGE (0xB1)
|
||||
int pos3 = pos2 + len2;
|
||||
assertEquals(0x81, replies[pos3 + 2] & 0xFF);
|
||||
assertEquals(QR_GIMAGE, replies[pos3 + 3] & 0xFF); // 0xB1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package haus.nightmare.lib3270j.ft;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class FTConfigTest {
|
||||
|
||||
@Test
|
||||
public void testTsoReceiveAscii() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setHostType(FTConfig.HostType.TSO);
|
||||
config.setDirection(FTConfig.Direction.RECEIVE);
|
||||
config.setHostFilename("MY.DATASET");
|
||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
config.setCrAction(FTConfig.CrAction.REMOVE);
|
||||
|
||||
assertEquals("IND$FILE GET MY.DATASET ASCII CRLF", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTsoReceiveBinary() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setHostType(FTConfig.HostType.TSO);
|
||||
config.setDirection(FTConfig.Direction.RECEIVE);
|
||||
config.setHostFilename("'USER.BIN.DATA'");
|
||||
config.setTransferMode(FTConfig.TransferMode.BINARY);
|
||||
|
||||
assertEquals("IND$FILE GET 'USER.BIN.DATA'", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTsoSendFixedRecfm() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setHostType(FTConfig.HostType.TSO);
|
||||
config.setDirection(FTConfig.Direction.SEND);
|
||||
config.setHostFilename("MY.DATASET");
|
||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
config.setCrAction(FTConfig.CrAction.REMOVE);
|
||||
config.setRecfm("F");
|
||||
config.setLrecl("80");
|
||||
config.setBlksize("3120");
|
||||
|
||||
assertEquals("IND$FILE PUT MY.DATASET ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120)", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCmsReceiveBinaryNoNakedParen() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setHostType(FTConfig.HostType.CMS);
|
||||
config.setDirection(FTConfig.Direction.RECEIVE);
|
||||
config.setHostFilename("PROFILE EXEC A");
|
||||
config.setTransferMode(FTConfig.TransferMode.BINARY);
|
||||
|
||||
// In CMS, binary receive has no options, so NO open parenthesis '(' should be generated!
|
||||
assertEquals("IND$FILE GET PROFILE EXEC A", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCmsReceiveAscii() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setHostType(FTConfig.HostType.CMS);
|
||||
config.setDirection(FTConfig.Direction.RECEIVE);
|
||||
config.setHostFilename("TEST DATA A");
|
||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
config.setCrAction(FTConfig.CrAction.REMOVE);
|
||||
|
||||
assertEquals("IND$FILE GET TEST DATA A (ASCII CRLF", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCmsSendWithRecfmAndLrecl() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setHostType(FTConfig.HostType.CMS);
|
||||
config.setDirection(FTConfig.Direction.SEND);
|
||||
config.setHostFilename("TEST FILE A");
|
||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
config.setCrAction(FTConfig.CrAction.REMOVE);
|
||||
config.setRecfm("V");
|
||||
config.setLrecl("132");
|
||||
|
||||
assertEquals("IND$FILE PUT TEST FILE A (ASCII CRLF RECFM V LRECL 132", config.buildCommand());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class GocaDecoderTest {
|
||||
|
||||
@Test
|
||||
public void testBasicLineAndColorOrders() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
assertFalse(plane.hasContent());
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Set Color to Red (GOCA Color 2) - 2 bytes: order, color
|
||||
out.write(GocaConstants.G_GSCOL);
|
||||
out.write(0x02); // Red
|
||||
|
||||
// Line from (1000, 1000) to (2000, 2000) - long order: order, len, x1, y1, x2, y2
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x08); // 8 bytes: (x1, y1, x2, y2)
|
||||
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAreaAndPatternFill() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Set Pattern to Solid - 2 bytes: order, pattern
|
||||
out.write(GocaConstants.G_GSPT);
|
||||
out.write(GocaConstants.PT_SOLID);
|
||||
|
||||
// Begin Area - 2 bytes: order, flags
|
||||
out.write(GocaConstants.G_GBAR);
|
||||
out.write(0x00);
|
||||
|
||||
// Polyline forming a triangle: (500, 500) -> (1500, 500) -> (1000, 1500) -> (500, 500)
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x10); // 4 points * 4 bytes = 16 bytes
|
||||
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||
|
||||
out.write((1500 >> 8) & 0xFF); out.write(1500 & 0xFF);
|
||||
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||
|
||||
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||
out.write((1500 >> 8) & 0xFF); out.write(1500 & 0xFF);
|
||||
|
||||
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||
|
||||
// End Area - 1 byte
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMarkerAndVectorText() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Set Marker Type to Circle - 2 bytes: order, type
|
||||
out.write(GocaConstants.G_GSMT);
|
||||
out.write(GocaConstants.MK_CIRCLE);
|
||||
|
||||
// Draw Marker at (2000, 2000)
|
||||
out.write(GocaConstants.G_GMRK);
|
||||
out.write(0x04);
|
||||
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||
|
||||
// Draw Vector Stroked Character String: "IBM" (EBCDIC: 0xC9, 0xC2, 0xD4)
|
||||
out.write(GocaConstants.G_GCHST);
|
||||
out.write(0x07); // 4 bytes pos + 3 bytes chars
|
||||
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||
out.write(0xC9); out.write(0xC2); out.write(0xD4);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcedureOrdersAndErase() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// Draw something first
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x08);
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x64);
|
||||
out.write(0x00); out.write(0x64);
|
||||
byte[] drawStream = out.toByteArray();
|
||||
decoder.decodeStream(drawStream, 0, drawStream.length);
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
// Process procedure order 0x0A (Erase presentation space)
|
||||
byte[] procOrders = new byte[] { (byte) GocaConstants.P_ERASE };
|
||||
decoder.processProcedureOrders(procOrders, 0, procOrders.length);
|
||||
assertFalse(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSegmentAndRelativeLineOrders() {
|
||||
GraphicsPlane plane = new GraphicsPlane(720, 688);
|
||||
plane.setScreenDimensions(80, 43);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// 0x70: Begin Segment (14 bytes total)
|
||||
out.write(GocaConstants.G_BEGSEGM);
|
||||
out.write(0x0C);
|
||||
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x01); // Seg ID 1
|
||||
out.write(0x74); out.write(0x70);
|
||||
out.write(0x00); out.write(0x1E); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
|
||||
// 0x3E: End Prologue
|
||||
out.write(GocaConstants.G_ENDPROLOGUE);
|
||||
out.write(0x00);
|
||||
|
||||
// 0x0A: Color (7 = White)
|
||||
out.write(GocaConstants.G_GSCOL);
|
||||
out.write(0x07);
|
||||
|
||||
// 0xE1: Relative Line (Absolute Start (300, 200), deltas: (+10, -5), (-10, +5))
|
||||
out.write(GocaConstants.G_GRLINE);
|
||||
out.write(0x08); // 4 bytes start coord + 4 bytes (2 deltas) = 8 bytes
|
||||
out.write(0x01); out.write(0x2C); // Start X = 300
|
||||
out.write(0x00); out.write(0xC8); // Start Y = 200
|
||||
out.write(0x0A); out.write(0xFB); // dx = +10, dy = -5
|
||||
out.write(0xF6); out.write(0x05); // dx = -10, dy = +5
|
||||
|
||||
// 0x71: End Segment
|
||||
out.write(GocaConstants.G_ENDSEGM);
|
||||
out.write(0x00);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartialOrderBufferingAcrossStreams() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// First packet sends G_GLINE order code and half of its coordinates
|
||||
ByteArrayOutputStream chunk1 = new ByteArrayOutputStream();
|
||||
chunk1.write(GocaConstants.G_GLINE);
|
||||
chunk1.write(0x08); // 8 bytes: 2 points
|
||||
chunk1.write(0x00); chunk1.write(0x64); // x1 = 100
|
||||
chunk1.write(0x00); chunk1.write(0x64); // y1 = 100
|
||||
|
||||
byte[] b1 = chunk1.toByteArray();
|
||||
decoder.decodeStream(b1, 0, b1.length);
|
||||
// Order is not complete yet, so no line drawn yet
|
||||
assertFalse(plane.hasContent());
|
||||
|
||||
// Second packet sends the rest of the order: x2 = 200, y2 = 200
|
||||
ByteArrayOutputStream chunk2 = new ByteArrayOutputStream();
|
||||
chunk2.write(0x00); chunk2.write(0xC8); // x2 = 200
|
||||
chunk2.write(0x00); chunk2.write(0xC8); // y2 = 200
|
||||
|
||||
byte[] b2 = chunk2.toByteArray();
|
||||
decoder.decodeStream(b2, 0, b2.length);
|
||||
// Now the combined order executes!
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetCurrentPositionAndRelativeLine() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// 0x21: Set Current Position to (50, 50)
|
||||
out.write(GocaConstants.G_GSCP);
|
||||
out.write(0x04); // len = 4
|
||||
out.write(0x00); out.write(0x32); // x = 50
|
||||
out.write(0x00); out.write(0x32); // y = 50
|
||||
|
||||
// 0xA1: Relative Line from Current Position: (+10, +20), (-5, -10)
|
||||
out.write(GocaConstants.G_GCRLIN);
|
||||
out.write(0x04); // len = 4 (2 deltas * 2 bytes)
|
||||
out.write(0x0A); out.write(0x14); // dx = +10, dy = +20
|
||||
out.write(0xFB); out.write(0xF6); // dx = -5, dy = -10
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
assertEquals(55, decoder.getCurX());
|
||||
assertEquals(60, decoder.getCurY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test3179GCoordinateMapping() {
|
||||
GraphicsPlane plane = new GraphicsPlane(720, 516);
|
||||
plane.setScreenDimensions(80, 43);
|
||||
|
||||
// Screen center (0, 0) should map to canvas center (360, 257)
|
||||
assertEquals(360, plane.mapX(0));
|
||||
assertEquals(257, plane.mapY(0));
|
||||
|
||||
// Left edge (-360) should map to 0
|
||||
assertEquals(0, plane.mapX(-360));
|
||||
// Right edge (+359) should map to 719
|
||||
assertEquals(719, plane.mapX(359));
|
||||
|
||||
// Top edge (+257) should map to 0
|
||||
assertEquals(0, plane.mapY(257));
|
||||
// Bottom edge (-258) should map to 515
|
||||
assertEquals(515, plane.mapY(-258));
|
||||
|
||||
// Bidirectional roundtrip mapping must be exact
|
||||
assertEquals(0, plane.unmapX(plane.mapX(0)));
|
||||
assertEquals(0, plane.unmapY(plane.mapY(0)));
|
||||
assertEquals(100, plane.unmapY(plane.mapY(100)));
|
||||
assertEquals(-200, plane.unmapY(plane.mapY(-200)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImageOrdersGbimgGimdGeimg() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// G_GBIMG (0xD1): Begin Image: order, len=8, x=100, y=100, w=16, h=2
|
||||
out.write(GocaConstants.G_GBIMG);
|
||||
out.write(0x08);
|
||||
out.write(0x00); out.write(0x64); // x = 100
|
||||
out.write(0x00); out.write(0x64); // y = 100
|
||||
out.write(0x00); out.write(0x10); // w = 16
|
||||
out.write(0x00); out.write(0x02); // h = 2
|
||||
|
||||
// G_GIMD (0x92): Image Data: order, len=4, 4 bytes of bitmap (16x2 pixels = 32 bits = 4 bytes)
|
||||
out.write(GocaConstants.G_GIMD);
|
||||
out.write(0x04);
|
||||
out.write(0xFF); out.write(0x00);
|
||||
out.write(0xAA); out.write(0x55);
|
||||
|
||||
// G_GEIMG (0x91): End Image
|
||||
out.write(GocaConstants.G_GEIMG);
|
||||
out.write(0x00);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent(), "Expected plane to have content after image decoding");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDirectObjectControlSf24() {
|
||||
haus.nightmare.lib3270j.screen.ScreenBuffer sb = new haus.nightmare.lib3270j.screen.ScreenBuffer(
|
||||
haus.nightmare.lib3270j.TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
|
||||
haus.nightmare.lib3270j.datastream.DataStreamProcessor dsp = new haus.nightmare.lib3270j.datastream.DataStreamProcessor(
|
||||
sb, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
|
||||
|
||||
// First draw something on graphics plane
|
||||
dsp.getGraphicsPlane().setPixel(10, 10, 0xFFFFFFFF);
|
||||
assertTrue(dsp.getGraphicsPlane().hasContent());
|
||||
|
||||
// Send WSF with SF_OBJCNTL (0x24) containing P_ERASE (0x0A)
|
||||
byte[] wsf = new byte[] {
|
||||
(byte) haus.nightmare.lib3270j.protocol.DS3270Constants.CMD_WSF,
|
||||
0x00, 0x04, // Field length = 4
|
||||
(byte) haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL, // 0x24
|
||||
(byte) haus.nightmare.lib3270j.graphics.GocaConstants.P_ERASE // 0x0A
|
||||
};
|
||||
dsp.processRecord(wsf, 0, wsf.length, false);
|
||||
|
||||
assertFalse(dsp.getGraphicsPlane().hasContent(), "Expected graphics plane to be cleared after SF_OBJCNTL P_ERASE");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGcallAndGscsWithLengthBytes() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// Construct Segment 2: Begin Segment (id=2), Set Color, draw line, End Segment
|
||||
ByteArrayOutputStream s2 = new ByteArrayOutputStream();
|
||||
s2.write(GocaConstants.G_BEGSEGM);
|
||||
s2.write(0x0C);
|
||||
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x02); // id=2
|
||||
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x00);
|
||||
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x00);
|
||||
s2.write(GocaConstants.G_GSCS); s2.write(0x01); s2.write(0x40); // charSet = 0x40
|
||||
s2.write(GocaConstants.G_GSCOL); s2.write(0x02); // Color = Red
|
||||
s2.write(GocaConstants.G_GLINE); s2.write(0x08);
|
||||
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x00);
|
||||
s2.write(0x00); s2.write(0x32); s2.write(0x00); s2.write(0x32);
|
||||
s2.write(GocaConstants.G_ENDSEGM); s2.write(0x00);
|
||||
|
||||
byte[] s2Bytes = s2.toByteArray();
|
||||
decoder.decodeStream(s2Bytes, 0, s2Bytes.length);
|
||||
|
||||
// Segment 1 calls Segment 2 via GCALL (0x2A len=4 segId=2)
|
||||
ByteArrayOutputStream s1 = new ByteArrayOutputStream();
|
||||
s1.write(GocaConstants.G_BEGSEGM);
|
||||
s1.write(0x0C);
|
||||
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x01); // id=1
|
||||
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x00);
|
||||
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x00);
|
||||
// GCALL (0x2A len=4 segId=2)
|
||||
s1.write(GocaConstants.G_GCALL); s1.write(0x04);
|
||||
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x02);
|
||||
s1.write(GocaConstants.G_ENDSEGM); s1.write(0x00);
|
||||
|
||||
byte[] s1Bytes = s1.toByteArray();
|
||||
decoder.decodeStream(s1Bytes, 0, s1Bytes.length);
|
||||
|
||||
assertEquals(0x40, decoder.getCharSet());
|
||||
assertTrue(plane.hasContent(), "Expected plane to have content after GCALL segment execution");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAntialiasedLineRendering() {
|
||||
GraphicsPlane plane = new GraphicsPlane(100, 100);
|
||||
plane.clear();
|
||||
int red = 0xFFFF0000;
|
||||
|
||||
// Draw a diagonal line with Xiaolin Wu anti-aliasing
|
||||
plane.drawLine(10.0, 10.0, 50.0, 30.0, red, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
int[] buffer = plane.getRgbBuffer();
|
||||
boolean hasIntermediateAlpha = false;
|
||||
int nonZeroPixels = 0;
|
||||
|
||||
for (int y = 0; y < 100; y++) {
|
||||
for (int x = 0; x < 100; x++) {
|
||||
int pixel = buffer[y * 100 + x];
|
||||
if (pixel != 0) {
|
||||
nonZeroPixels++;
|
||||
int alpha = (pixel >>> 24) & 0xFF;
|
||||
int r = (pixel >>> 16) & 0xFF;
|
||||
assertEquals(255, r, "Red channel must be preserved");
|
||||
if (alpha > 0 && alpha < 255) {
|
||||
hasIntermediateAlpha = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(nonZeroPixels > 30, "Expected non-zero pixels along the line");
|
||||
assertTrue(hasIntermediateAlpha, "Expected Xiaolin Wu anti-aliasing to produce fractional alpha coverage");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubpixelArcAndAlphaBlending() {
|
||||
GraphicsPlane plane = new GraphicsPlane(100, 100);
|
||||
plane.clear();
|
||||
int green = 0xFF00FF00;
|
||||
|
||||
plane.drawArc(50.0, 50.0, 30.0, 30.0, 0.0, 360.0, green, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, true);
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
int[] buffer = plane.getRgbBuffer();
|
||||
boolean hasIntermediateAlpha = false;
|
||||
int nonZeroPixels = 0;
|
||||
|
||||
for (int p : buffer) {
|
||||
if (p != 0) {
|
||||
nonZeroPixels++;
|
||||
int alpha = (p >>> 24) & 0xFF;
|
||||
if (alpha > 0 && alpha < 255) {
|
||||
hasIntermediateAlpha = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(nonZeroPixels > 50, "Expected arc pixels");
|
||||
assertTrue(hasIntermediateAlpha, "Expected anti-aliased arc edges with smooth alpha");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVectorTextAntialiasedDrawing() {
|
||||
GraphicsPlane plane = new GraphicsPlane(200, 100);
|
||||
plane.clear();
|
||||
int yellow = 0xFFFFFF00;
|
||||
|
||||
plane.drawVectorText(10.0, 10.0, "8% 1985 TAX", yellow, 12.0, 20.0, GocaConstants.CD_LR, 0.0);
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
int[] buffer = plane.getRgbBuffer();
|
||||
boolean hasIntermediateAlpha = false;
|
||||
int nonZeroPixels = 0;
|
||||
|
||||
for (int p : buffer) {
|
||||
if (p != 0) {
|
||||
nonZeroPixels++;
|
||||
int alpha = (p >>> 24) & 0xFF;
|
||||
if (alpha > 0 && alpha < 255) {
|
||||
hasIntermediateAlpha = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(nonZeroPixels > 40, "Expected stroked vector text pixels");
|
||||
assertTrue(hasIntermediateAlpha, "Expected anti-aliased vector text strokes with fractional alpha");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTextRendererPrecisionSwitching() {
|
||||
GraphicsPlane plane = new GraphicsPlane(200, 100);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
final boolean[] textRendererCalled = new boolean[1];
|
||||
plane.setTextRenderer((p, x, y, text, color, cw, ch, dir, angle) -> {
|
||||
textRendererCalled[0] = true;
|
||||
});
|
||||
|
||||
// Test String Precision (Default): GCHST (0xC3) with "TAX"
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GCHST);
|
||||
out.write(0x07); // length = 7 (4 bytes pos + 3 bytes text)
|
||||
out.write(0x00); out.write(0x00); // x = 0
|
||||
out.write(0x00); out.write(0x00); // y = 0
|
||||
out.write(0xE3); // 'T' in EBCDIC
|
||||
out.write(0xC1); // 'A' in EBCDIC
|
||||
out.write(0xE7); // 'X' in EBCDIC
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(textRendererCalled[0], "Expected pluggable TextRenderer to be called for String precision");
|
||||
|
||||
// Now set Stroke precision (G_GSCC with 3):
|
||||
textRendererCalled[0] = false;
|
||||
ByteArrayOutputStream outStroke = new ByteArrayOutputStream();
|
||||
outStroke.write(GocaConstants.G_GSCC);
|
||||
outStroke.write(0x01);
|
||||
outStroke.write(GocaConstants.CP_STROKE); // 3
|
||||
outStroke.write(GocaConstants.G_GCHST);
|
||||
outStroke.write(0x07);
|
||||
outStroke.write(0x00); outStroke.write(0x00);
|
||||
outStroke.write(0x00); outStroke.write(0x00);
|
||||
outStroke.write(0xE3); outStroke.write(0xC1); outStroke.write(0xE7);
|
||||
|
||||
byte[] strokeStream = outStroke.toByteArray();
|
||||
decoder.decodeStream(strokeStream, 0, strokeStream.length);
|
||||
|
||||
assertFalse(textRendererCalled[0], "Expected drawVectorText (not textRenderer) when precision is CP_STROKE");
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThickLineRendering() {
|
||||
GraphicsPlane plane = new GraphicsPlane(100, 100);
|
||||
plane.clear();
|
||||
|
||||
plane.drawLine(10.0, 10.0, 50.0, 50.0, 0xFF00FF00, GocaConstants.LT_SOLID, GocaConstants.LW_THICK);
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
int[] buffer = plane.getRgbBuffer();
|
||||
int nonZero = 0;
|
||||
for (int p : buffer) {
|
||||
if (p != 0) nonZero++;
|
||||
}
|
||||
assertTrue(nonZero > 60, "Expected thick line to occupy more pixels than standard 1px line");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortFormAttributeOrderParsing() {
|
||||
GraphicsPlane plane = new GraphicsPlane(200, 200);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// Sequence: GBAR (0x68) short form with 0x80 flag (boundary=true, fill=false),
|
||||
// followed immediately by GLINE (0xC1 len=12 for 3 points) and GEAR (0x60)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GBAR);
|
||||
out.write(0x80); // Short 2-byte form with 0x80 flag! Must NOT be treated as length=128!
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x0C); // 3 points = 12 bytes
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x32);
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x32);
|
||||
out.write(0x00); out.write(0x32);
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
out.write(0x00);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent(), "Expected GLINE and GEAR inside GBAR short-form 0x80 to be decoded properly");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiPolygonAreaFilling() {
|
||||
GraphicsPlane plane = new GraphicsPlane(200, 200);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// Sequence: GBAR (0x68) short form 0x80 (bounded, always filled in GOCA)
|
||||
// Polygon 1 (e.g. Letter 'T' bar): (10,10) to (30,10) to (30,20) to (10,20) to (10,10)
|
||||
// Polygon 2 (e.g. Letter 'T' stem): (18,20) to (22,20) to (22,40) to (18,40) to (18,20)
|
||||
// GEAR (0x60)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GBAR);
|
||||
out.write(0x80);
|
||||
|
||||
// Polygon 1
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x14); // 5 points = 20 bytes
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(30); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(30); out.write(0x00); out.write(20);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(20);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||
|
||||
// Polygon 2 (disconnected start -> triggers new subpath)
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x14); // 5 points = 20 bytes
|
||||
out.write(0x00); out.write(18); out.write(0x00); out.write(20);
|
||||
out.write(0x00); out.write(22); out.write(0x00); out.write(20);
|
||||
out.write(0x00); out.write(22); out.write(0x00); out.write(40);
|
||||
out.write(0x00); out.write(18); out.write(0x00); out.write(40);
|
||||
out.write(0x00); out.write(18); out.write(0x00); out.write(20);
|
||||
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
out.write(0x00);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
int[] buffer = plane.getRgbBuffer();
|
||||
int nonZero = 0;
|
||||
for (int p : buffer) {
|
||||
if (p != 0) nonZero++;
|
||||
}
|
||||
assertTrue(nonZero > 50, "Expected both filled subpath polygons to render filled pixels");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class GraphicsModeTest {
|
||||
|
||||
@Test
|
||||
public void testDefaultsAndParsing() {
|
||||
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString(null));
|
||||
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString(""));
|
||||
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("NONE"));
|
||||
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("off"));
|
||||
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("false"));
|
||||
|
||||
assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("ps"));
|
||||
assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("programmed_symbols"));
|
||||
assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("apl"));
|
||||
|
||||
assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("vector"));
|
||||
assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("goca"));
|
||||
assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("vector_graphics"));
|
||||
|
||||
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("both"));
|
||||
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("all"));
|
||||
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("true"));
|
||||
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("on"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFlags() {
|
||||
assertFalse(GraphicsMode.NONE.isProgrammedSymbolsEnabled());
|
||||
assertFalse(GraphicsMode.NONE.isVectorGraphicsEnabled());
|
||||
|
||||
assertTrue(GraphicsMode.PROGRAMMED_SYMBOLS.isProgrammedSymbolsEnabled());
|
||||
assertFalse(GraphicsMode.PROGRAMMED_SYMBOLS.isVectorGraphicsEnabled());
|
||||
|
||||
assertFalse(GraphicsMode.VECTOR_GRAPHICS.isProgrammedSymbolsEnabled());
|
||||
assertTrue(GraphicsMode.VECTOR_GRAPHICS.isVectorGraphicsEnabled());
|
||||
|
||||
assertTrue(GraphicsMode.BOTH.isProgrammedSymbolsEnabled());
|
||||
assertTrue(GraphicsMode.BOTH.isVectorGraphicsEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectionConfigDefault() {
|
||||
ConnectionConfig config = new ConnectionConfig();
|
||||
assertEquals(GraphicsMode.BOTH, config.getGraphicsMode());
|
||||
|
||||
config.setGraphicsMode(GraphicsMode.NONE);
|
||||
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
|
||||
|
||||
config.setGraphicsMode(null);
|
||||
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ProgramSymbolManagerTest {
|
||||
|
||||
@Test
|
||||
public void testSetAllocationAndLookup() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
// Loadable set can be queried after loadps or clearAll
|
||||
assertNull(manager.getSymbolSet(0x40));
|
||||
assertNull(manager.getSymbol(0x40, 0x41));
|
||||
|
||||
manager.clearAll();
|
||||
assertNull(manager.getSymbolSet(0x40));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSinglePlaneLoadPsFormat1() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
|
||||
// Build Format 1 Load PS payload for LCID 0x40, code point 0x41 ('A')
|
||||
// Format 1 header:
|
||||
// byte 0: flags (0x01: format 1, single plane)
|
||||
// byte 1: LCID (0x40)
|
||||
// byte 2: Start code point (0x41)
|
||||
// byte 3: RWS (0x02: loadable slot 2)
|
||||
// followed by 18 bytes per symbol for 9x16 cell:
|
||||
// Byte 0-1: Col 0 for rows 0..15 (0x80 = bit for row 0 col 0)
|
||||
// Bytes 2-17: Cols 1..8 for rows 0..15 (0xFF = bits for row 0 cols 1..8)
|
||||
byte[] payload = new byte[4 + 18];
|
||||
payload[0] = 0x01; // Format 1
|
||||
payload[1] = 0x40; // LCID 0x40
|
||||
payload[2] = 0x41; // Code point 0x41
|
||||
payload[3] = 0x02; // RWS 2
|
||||
|
||||
payload[4] = (byte) 0x80; // Row 0 Col 0 bit
|
||||
payload[5] = (byte) 0x00; // Rows 8..15 Col 0
|
||||
for (int r = 0; r < 16; r++) {
|
||||
payload[6 + r] = (byte) 0xFF; // Cols 1..8 on all rows
|
||||
}
|
||||
|
||||
manager.loadps(payload);
|
||||
|
||||
ProgramSymbolSet set = manager.getSymbolSet(0x40);
|
||||
assertNotNull(set);
|
||||
assertEquals(0x40, set.getLcid());
|
||||
assertFalse(set.isTriplePlane());
|
||||
|
||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
|
||||
assertNotNull(slot);
|
||||
assertEquals(9, slot.getWidth());
|
||||
assertEquals(12, slot.getHeight());
|
||||
assertFalse(slot.isTriplePlane());
|
||||
|
||||
// Verify pixel data (Row 0 Col 0 is 1)
|
||||
byte[] pixels = slot.getPixelData();
|
||||
assertNotNull(pixels);
|
||||
assertEquals(1, pixels[0]); // Row 0 Col 0 is 1
|
||||
|
||||
// Test rendering into 32-bit ARGB pixels
|
||||
int fg = 0xFF00FF00; // Green
|
||||
int bg = 0xFF000000; // Black
|
||||
int[] rgb = slot.getRgbPixels(fg, bg);
|
||||
assertNotNull(rgb);
|
||||
assertEquals(9 * 12, rgb.length);
|
||||
assertEquals(fg, rgb[0]); // Row 0 Col 0 pixel should be foreground Green
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExplicitCellDimensions() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
manager.setDefaultCellDimensions(9, 16);
|
||||
|
||||
byte[] payload = new byte[4 + 18];
|
||||
payload[0] = 0x01; // Format 1
|
||||
payload[1] = 0x40; // LCID 0x40
|
||||
payload[2] = 0x41; // Code point 0x41
|
||||
payload[3] = 0x02; // RWS 2
|
||||
payload[4] = (byte) 0x80;
|
||||
payload[5] = (byte) 0x00;
|
||||
for (int r = 0; r < 16; r++) {
|
||||
payload[6 + r] = (byte) 0xFF;
|
||||
}
|
||||
|
||||
manager.loadps(payload);
|
||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
|
||||
assertNotNull(slot);
|
||||
assertEquals(9, slot.getWidth());
|
||||
assertEquals(16, slot.getHeight());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTriplePlaneMultiColorComposite() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
|
||||
// Build Format 1 Load PS payload for LCID 0x42, triple plane
|
||||
// flags = 0x01 (Format 1)
|
||||
// RWS = 4 (Triple Plane)
|
||||
byte[] payload = new byte[4 + 18];
|
||||
payload[0] = 0x01; // Format 1
|
||||
payload[1] = 0x42; // LCID 0x42
|
||||
payload[2] = 0x45; // Code point 0x45
|
||||
payload[3] = 0x04; // RWS 4 (Triple Plane)
|
||||
|
||||
// Set pixel at row 0 col 0
|
||||
payload[4] = (byte) 0x80; // Row 0 Col 0
|
||||
payload[5] = (byte) 0x00;
|
||||
for (int r = 0; r < 16; r++) {
|
||||
payload[6 + r] = (byte) 0x00;
|
||||
}
|
||||
|
||||
manager.loadps(payload);
|
||||
|
||||
ProgramSymbolSet set = manager.getSymbolSet(0x42);
|
||||
assertNotNull(set);
|
||||
assertTrue(set.isTriplePlane());
|
||||
|
||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x42, 0x45);
|
||||
assertNotNull(slot);
|
||||
assertTrue(slot.isTriplePlane());
|
||||
|
||||
// When loaded with colorPlane=0 (default), all 3 planes are set (val 7 = white composite)
|
||||
byte[] pixels = slot.getPixelData();
|
||||
assertEquals(7, pixels[0] & 0xFF);
|
||||
|
||||
// Render ARGB pixels
|
||||
int[] rgb = slot.getRgbPixels(0xFFFFFFFF, 0xFF000000);
|
||||
assertNotNull(rgb);
|
||||
|
||||
// Pixel (0,0) should be white composite (0xFFFFFFFF)
|
||||
int pixelArgb = rgb[0];
|
||||
int r = (pixelArgb >> 16) & 0xFF;
|
||||
int g = (pixelArgb >> 8) & 0xFF;
|
||||
int b = pixelArgb & 0xFF;
|
||||
assertEquals(255, r);
|
||||
assertEquals(255, g);
|
||||
assertEquals(255, b);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormat3Bitstream() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
|
||||
// Format 3 payload
|
||||
// byte 0: 0x03 (Format 3)
|
||||
// byte 1: LCID (0x44)
|
||||
// byte 2: code point (0x43)
|
||||
// byte 3: RWS (2)
|
||||
// followed by (9 * 16 + 7) / 8 = 18 bytes
|
||||
byte[] payload = new byte[4 + 18];
|
||||
payload[0] = 0x03; // Format 3
|
||||
payload[1] = 0x44; // LCID 0x44
|
||||
payload[2] = 0x43; // Code point 0x43
|
||||
payload[3] = 0x02; // RWS 2
|
||||
payload[4] = (byte) 0x80; // First pixel is set
|
||||
|
||||
manager.loadps(payload);
|
||||
|
||||
ProgramSymbolSet set = manager.getSymbolSet(0x44);
|
||||
assertNotNull(set);
|
||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x44, 0x43);
|
||||
assertNotNull(slot);
|
||||
assertEquals(1, slot.getPixelData()[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package haus.nightmare.lib3270j.input;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
public class InputProcessorTest {
|
||||
|
||||
private final EbcdicTranslator translator = new EbcdicTranslator();
|
||||
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
|
||||
@Test
|
||||
public void testKybdPrimeOnUnformattedScreen() {
|
||||
InputProcessor input = new InputProcessor(screen, translator, null);
|
||||
int cap = input.kybdPrime();
|
||||
assertEquals(screen.getRows() * screen.getCols(), cap);
|
||||
assertEquals(0, screen.getCursorAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKybdPrimeOnFormattedScreen() {
|
||||
InputProcessor input = new InputProcessor(screen, translator, null);
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) FA_PRINTABLE); // Unprotected field at 0 (length = 19)
|
||||
screen.setCellFA(20, (byte) (FA_PRINTABLE | FA_PROTECT)); // Protected field at 20
|
||||
|
||||
int cap = input.kybdPrime();
|
||||
assertEquals(19, cap);
|
||||
assertEquals(1, screen.getCursorAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEraseInput() {
|
||||
InputProcessor input = new InputProcessor(screen, translator, null);
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) FA_PRINTABLE);
|
||||
screen.setCursorAddress(1);
|
||||
input.typeCharacter('A');
|
||||
input.typeCharacter('B');
|
||||
|
||||
assertEquals('A', screen.getCell(1).ucs4);
|
||||
assertEquals('B', screen.getCell(2).ucs4);
|
||||
assertEquals('A', screen.getDisplayCell(1).ucs4);
|
||||
assertEquals('B', screen.getDisplayCell(2).ucs4);
|
||||
|
||||
input.eraseInput();
|
||||
|
||||
assertEquals(0, screen.getCell(1).ec);
|
||||
assertEquals(0, screen.getCell(2).ec);
|
||||
assertEquals(0, screen.getDisplayCell(1).ec);
|
||||
assertEquals(0, screen.getDisplayCell(2).ec);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTypeCharacterUpdatesDisplaySnapshot() {
|
||||
InputProcessor input = new InputProcessor(screen, translator, null);
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) FA_PRINTABLE); // Unprotected field starting at 1
|
||||
screen.setCursorAddress(1);
|
||||
screen.updateDisplaySnapshot(); // Initial snapshot
|
||||
|
||||
// Type "logon"
|
||||
input.typeCharacter('l');
|
||||
input.typeCharacter('o');
|
||||
input.typeCharacter('g');
|
||||
input.typeCharacter('o');
|
||||
input.typeCharacter('n');
|
||||
|
||||
// Verify underlying buffer
|
||||
assertEquals('l', screen.getCell(1).ucs4);
|
||||
assertEquals('o', screen.getCell(2).ucs4);
|
||||
assertEquals('g', screen.getCell(3).ucs4);
|
||||
assertEquals('o', screen.getCell(4).ucs4);
|
||||
assertEquals('n', screen.getCell(5).ucs4);
|
||||
|
||||
// Verify display snapshot (what TerminalPanel renders)
|
||||
assertEquals('l', screen.getDisplayCell(1).ucs4);
|
||||
assertEquals('o', screen.getDisplayCell(2).ucs4);
|
||||
assertEquals('g', screen.getDisplayCell(3).ucs4);
|
||||
assertEquals('o', screen.getDisplayCell(4).ucs4);
|
||||
assertEquals('n', screen.getDisplayCell(5).ucs4);
|
||||
assertEquals(6, screen.getDisplayCursorAddress());
|
||||
assertEquals(6, screen.getCursorAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCursorMovementUpdatesDisplayCursorAddress() {
|
||||
InputProcessor input = new InputProcessor(screen, translator, null);
|
||||
screen.erase(false);
|
||||
screen.setCursorAddress(10);
|
||||
assertEquals(10, screen.getDisplayCursorAddress());
|
||||
|
||||
input.cursorRight();
|
||||
assertEquals(11, screen.getDisplayCursorAddress());
|
||||
|
||||
input.cursorLeft();
|
||||
assertEquals(10, screen.getDisplayCursorAddress());
|
||||
|
||||
screen.setCursorAddress(45);
|
||||
assertEquals(45, screen.getDisplayCursorAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteAndEraseEofUpdateDisplaySnapshot() {
|
||||
InputProcessor input = new InputProcessor(screen, translator, null);
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) FA_PRINTABLE);
|
||||
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
screen.setCursorAddress(1);
|
||||
|
||||
input.typeCharacter('A');
|
||||
input.typeCharacter('B');
|
||||
input.typeCharacter('C');
|
||||
assertEquals('A', screen.getDisplayCell(1).ucs4);
|
||||
assertEquals('B', screen.getDisplayCell(2).ucs4);
|
||||
assertEquals('C', screen.getDisplayCell(3).ucs4);
|
||||
|
||||
// Backspace over 'C'
|
||||
input.backspace();
|
||||
assertEquals(3, screen.getCursorAddress());
|
||||
assertEquals(3, screen.getDisplayCursorAddress());
|
||||
assertEquals(0, screen.getDisplayCell(3).ucs4);
|
||||
|
||||
// Erase EOF from position 2 ('B')
|
||||
screen.setCursorAddress(2);
|
||||
input.eraseEof();
|
||||
assertEquals(0, screen.getDisplayCell(2).ucs4);
|
||||
assertEquals('A', screen.getDisplayCell(1).ucs4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGraphicInputBuilderStructure() {
|
||||
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(100, -50, AID_ENTER, false, false, false);
|
||||
assertEquals(56, sf.length);
|
||||
assertEquals(0x00, sf[0]);
|
||||
assertEquals(0x34, sf[1]); // Length = 52
|
||||
assertEquals(0x0F, sf[2]); // SF ID = 0x0F
|
||||
assertEquals(0x0F, sf[3]); // SF ID = 0x0F
|
||||
|
||||
// Coordinates
|
||||
int x = (sf[24] << 8) | (sf[25] & 0xFF);
|
||||
int y = (sf[26] << 8) | (sf[27] & 0xFF);
|
||||
assertEquals(100, (short) x);
|
||||
assertEquals(-50, (short) y);
|
||||
|
||||
// Keyboard AID
|
||||
assertEquals(0x07, sf[31]);
|
||||
assertEquals(0x07, sf[33]);
|
||||
assertEquals((byte) 0xFF, sf[34]);
|
||||
assertEquals((byte) AID_ENTER, sf[35]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendAidSuppressesNullsInModifiedField() {
|
||||
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||
@Override
|
||||
public void sendAid(int aidCode) {
|
||||
// Call super logic by hooking via fake FSM or overriding sendAid
|
||||
super.sendAid(aidCode);
|
||||
}
|
||||
};
|
||||
// Setup screen: field attribute at 0 (unprotected, modified)
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
|
||||
// Pos 1 = null (0x00)
|
||||
screen.getCell(1).ec = 0x00;
|
||||
// Pos 2 = 'Q' (0xD8)
|
||||
screen.getCell(2).ec = (byte) 0xD8;
|
||||
// Pos 3 = 'U' (0xE4)
|
||||
screen.getCell(3).ec = (byte) 0xE4;
|
||||
// Pos 4 = 'I' (0xC9)
|
||||
screen.getCell(4).ec = (byte) 0xC9;
|
||||
// Pos 5 = 'T' (0xE3)
|
||||
screen.getCell(5).ec = (byte) 0xE3;
|
||||
// Next field at 10 (protected)
|
||||
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
// Use custom callback or inspection
|
||||
// Let's verify DataStreamProcessor ReadModified behavior with the same buffer
|
||||
DataStreamProcessor dsp = new DataStreamProcessor(screen, translator);
|
||||
dsp.setInputProcessor(input);
|
||||
dsp.setOutputSender(sent::set);
|
||||
|
||||
byte[] rmRecord = new byte[] { (byte) CMD_RM };
|
||||
dsp.processRecord(rmRecord, 0, rmRecord.length, true);
|
||||
|
||||
byte[] result = sent.get();
|
||||
assertNotNull(result);
|
||||
// Result format: AID (1 byte), Cursor (2 bytes), SBA (1 byte), Addr (2 bytes), Data ('Q', 'U', 'I', 'T' - 4 bytes)
|
||||
assertEquals(10, result.length, "Result should be AID(1) + Cursor(2) + SBA(1) + Addr(2) + Data(4)");
|
||||
assertEquals((byte) ORDER_SBA, result[3]);
|
||||
assertEquals((byte) 0xD8, result[6]); // 'Q'
|
||||
assertEquals((byte) 0xE4, result[7]); // 'U'
|
||||
assertEquals((byte) 0xC9, result[8]); // 'I'
|
||||
assertEquals((byte) 0xE3, result[9]); // 'T'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package haus.nightmare.lib3270j.protocol;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
public class DS3270ConstantsTest {
|
||||
|
||||
@Test
|
||||
public void test12BitAddressRoundTrip() {
|
||||
int rows = 24;
|
||||
int cols = 80;
|
||||
int maxAddr = rows * cols; // 1920
|
||||
|
||||
for (int addr = 0; addr < maxAddr; addr += 17) {
|
||||
byte[] encoded = encodeAddress(addr, rows, cols);
|
||||
assertEquals(2, encoded.length);
|
||||
int decoded = decodeAddress(encoded[0] & 0xFF, encoded[1] & 0xFF);
|
||||
assertEquals(addr, decoded, "Address " + addr + " failed round trip");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test14BitAddressRoundTrip() {
|
||||
int rows = 60;
|
||||
int cols = 80; // 4800 (> 4096, triggers 14-bit format)
|
||||
int maxAddr = rows * cols;
|
||||
|
||||
for (int addr = 0; addr < maxAddr; addr += 123) {
|
||||
byte[] encoded = encodeAddress(addr, rows, cols);
|
||||
assertEquals(2, encoded.length);
|
||||
int decoded = decodeAddress(encoded[0] & 0xFF, encoded[1] & 0xFF);
|
||||
assertEquals(addr, decoded, "14-bit address " + addr + " failed round trip");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFieldAttributePredicates() {
|
||||
int protectedHighMod = FA_PRINTABLE | FA_PROTECT | FA_INT_HIGH_SEL | FA_MODIFY;
|
||||
assertTrue(faIsProtected(protectedHighMod));
|
||||
assertTrue(faIsHigh(protectedHighMod));
|
||||
assertTrue(faIsModified(protectedHighMod));
|
||||
assertFalse(faIsNumeric(protectedHighMod));
|
||||
assertFalse(faIsSkip(protectedHighMod));
|
||||
|
||||
int skipField = FA_PRINTABLE | FA_PROTECT | FA_NUMERIC;
|
||||
assertTrue(faIsSkip(skipField));
|
||||
assertTrue(faIsProtected(skipField));
|
||||
assertTrue(faIsNumeric(skipField));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package haus.nightmare.lib3270j.screen;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
public class ScreenBufferTest {
|
||||
|
||||
private final EbcdicTranslator translator = new EbcdicTranslator();
|
||||
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
|
||||
@Test
|
||||
public void testScreenInitialization() {
|
||||
assertEquals(24, screen.getRows());
|
||||
assertEquals(80, screen.getCols());
|
||||
assertEquals(43, screen.getMaxRows());
|
||||
assertEquals(80, screen.getMaxCols());
|
||||
assertFalse(screen.isFormatted());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScreenEraseAlternate() {
|
||||
screen.erase(true);
|
||||
assertTrue(screen.isScreenAlt());
|
||||
assertEquals(43, screen.getRows());
|
||||
assertEquals(80, screen.getCols());
|
||||
|
||||
screen.erase(false);
|
||||
assertFalse(screen.isScreenAlt());
|
||||
assertEquals(24, screen.getRows());
|
||||
assertEquals(80, screen.getCols());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFieldNavigationAndFindAttribute() {
|
||||
screen.erase(false);
|
||||
// Place field attribute at position 0 (unprotected)
|
||||
screen.setCellFA(0, (byte) FA_PRINTABLE);
|
||||
// Place field attribute at position 20 (protected)
|
||||
screen.setCellFA(20, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
assertTrue(screen.isFormatted());
|
||||
assertEquals(0, screen.findFieldAttribute(5));
|
||||
assertEquals(20, screen.findFieldAttribute(25));
|
||||
|
||||
// findNextUnprotected
|
||||
int nextUnprot = screen.findNextUnprotected(0);
|
||||
assertEquals(1, nextUnprot);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGraphicEscapeTranslation() {
|
||||
screen.erase(false);
|
||||
// Set cell at position 10 to horizontal line (0xA2) with CS_GE (0x04)
|
||||
ExtendedAttribute ea = screen.getCell(10);
|
||||
ea.ec = (byte) 0xA2;
|
||||
ea.cs = (byte) CS_GE;
|
||||
|
||||
screen.translateToUnicode();
|
||||
assertEquals('\u2500', ea.ucs4); // '─'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package haus.nightmare.lib3270j.telnet;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
|
||||
import haus.nightmare.lib3270j.protocol.TelnetConstants;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class TelnetFSMTest {
|
||||
|
||||
private ConnectionConfig config;
|
||||
private ScreenBuffer screenBuffer;
|
||||
private DataStreamProcessor dsProcessor;
|
||||
private TelnetFSM fsm;
|
||||
private MockConnection connection;
|
||||
|
||||
private static class MockConnection extends TelnetConnection {
|
||||
final List<byte[]> sentData = new ArrayList<>();
|
||||
|
||||
MockConnection(ConnectionConfig config, TelnetFSM fsm) {
|
||||
super(config, fsm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void sendRaw(byte[] data) {
|
||||
sentData.add(data.clone());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void sendRaw(byte[] data, int offset, int length) {
|
||||
byte[] b = new byte[length];
|
||||
System.arraycopy(data, offset, b, 0, length);
|
||||
sentData.add(b);
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, new EbcdicTranslator());
|
||||
dsProcessor = new DataStreamProcessor(screenBuffer, new EbcdicTranslator());
|
||||
fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||
connection = new MockConnection(config, fsm);
|
||||
fsm.setConnection(connection);
|
||||
}
|
||||
|
||||
private void feedBytes(int... bytes) {
|
||||
for (int b : bytes) {
|
||||
fsm.feedByte(b & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHostPrefixParsingPlainTn3270() {
|
||||
ConnectionConfig c1 = ConnectionConfig.parseHostString("P:mainframe.example.com", 23, TerminalModel.IBM_3279_4);
|
||||
assertFalse(c1.isTn3270eEnabled(), "P: prefix should disable TN3270E");
|
||||
assertFalse(c1.isUseTls());
|
||||
assertEquals("mainframe.example.com", c1.getHost());
|
||||
|
||||
ConnectionConfig c2 = ConnectionConfig.parseHostString("plain:zos.net:2323", 23, TerminalModel.IBM_3279_4);
|
||||
assertFalse(c2.isTn3270eEnabled());
|
||||
assertEquals("zos.net", c2.getHost());
|
||||
assertEquals(2323, c2.getPort());
|
||||
|
||||
ConnectionConfig c3 = ConnectionConfig.parseHostString("L:P:secure.mainframe.com:992", 0, TerminalModel.IBM_3279_4);
|
||||
assertTrue(c3.isUseTls(), "L: should enable TLS");
|
||||
assertFalse(c3.isTn3270eEnabled(), "P: should disable TN3270E");
|
||||
assertEquals("secure.mainframe.com", c3.getHost());
|
||||
assertEquals(992, c3.getPort());
|
||||
|
||||
ConnectionConfig c4 = ConnectionConfig.parseHostString("non-e:vm.ibm.com", 23, TerminalModel.IBM_3279_4);
|
||||
assertFalse(c4.isTn3270eEnabled());
|
||||
assertEquals("vm.ibm.com", c4.getHost());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlainTn3270NegotiationRejectsTn3270E() {
|
||||
config.setTn3270eEnabled(false);
|
||||
fsm.onConnected();
|
||||
assertEquals(ConnectionState.TELNET_PENDING, fsm.getConnectionState());
|
||||
|
||||
// Host offers DO TN3270E -> client must reply WONT TN3270E
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
|
||||
assertFalse(fsm.getMyOpts()[TelnetConstants.TELOPT_TN3270E]);
|
||||
|
||||
// Host offers WILL TN3270E -> client must reply DONT TN3270E
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TN3270E);
|
||||
assertFalse(fsm.getHisOpts()[TelnetConstants.TELOPT_TN3270E]);
|
||||
|
||||
// Host negotiates standard 3270 options: DO BINARY, WILL BINARY, DO EOR, WILL EOR, DO TTYPE
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_BINARY);
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_BINARY);
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_EOR);
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_EOR);
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TTYPE);
|
||||
|
||||
// Client should have transitioned to CONNECTED_3270
|
||||
assertEquals(ConnectionState.CONNECTED_3270, fsm.getConnectionState());
|
||||
|
||||
// Host asks for TTYPE
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
|
||||
TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE);
|
||||
|
||||
// Verify sent packets contain WONT TN3270E, DONT TN3270E, and TTYPE IS
|
||||
boolean foundWontTn3270e = false;
|
||||
boolean foundTtypeIs = false;
|
||||
for (byte[] pkt : connection.sentData) {
|
||||
if (pkt.length == 3 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
|
||||
(pkt[1] & 0xFF) == TelnetConstants.WONT && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TN3270E) {
|
||||
foundWontTn3270e = true;
|
||||
}
|
||||
if (pkt.length >= 4 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
|
||||
(pkt[1] & 0xFF) == TelnetConstants.SB && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TTYPE) {
|
||||
foundTtypeIs = true;
|
||||
}
|
||||
}
|
||||
assertTrue(foundWontTn3270e, "Expected WONT TN3270E response");
|
||||
assertTrue(foundTtypeIs, "Expected TTYPE IS response");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTn3270eFunctionsNegotiationWithoutBindImageTransitionsToConnected() {
|
||||
config.setTn3270eEnabled(true);
|
||||
fsm.onConnected();
|
||||
assertEquals(ConnectionState.TELNET_PENDING, fsm.getConnectionState());
|
||||
|
||||
// Host offers DO TN3270E -> client accepts with WILL TN3270E
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
|
||||
assertTrue(fsm.getMyOpts()[TelnetConstants.TELOPT_TN3270E]);
|
||||
|
||||
// Host responds with DEVICE-TYPE IS IBM-3279-4-E CONNECT LU01
|
||||
byte[] devTypeIs = new byte[]{
|
||||
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
|
||||
0x02, 0x04, // OP_DEVICE_TYPE, OP_IS
|
||||
'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
|
||||
0x01, // OP_CONNECT
|
||||
'L', 'U', '0', '1',
|
||||
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
|
||||
};
|
||||
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
|
||||
|
||||
// Host responds with FUNCTIONS REQUEST <empty> (e.g. pytn3270 / RFC 2355 server)
|
||||
byte[] funcsReq = new byte[]{
|
||||
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
|
||||
0x03, 0x07, // OP_FUNCTIONS, OP_REQUEST
|
||||
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
|
||||
};
|
||||
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
|
||||
|
||||
// Per RFC 2355: Client must reply with FUNCTIONS IS, and without BIND-IMAGE, session is bound immediately
|
||||
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState(),
|
||||
"Expected CONNECTED_TN3270E when BIND-IMAGE is not negotiated");
|
||||
|
||||
// Verify that client sent SB TN3270E FUNCTIONS IS SE
|
||||
boolean foundFunctionsIs = false;
|
||||
for (byte[] pkt : connection.sentData) {
|
||||
if (pkt.length >= 5 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
|
||||
(pkt[1] & 0xFF) == TelnetConstants.SB && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TN3270E &&
|
||||
(pkt[3] & 0xFF) == 0x03 && (pkt[4] & 0xFF) == 0x04) {
|
||||
foundFunctionsIs = true;
|
||||
}
|
||||
}
|
||||
assertTrue(foundFunctionsIs, "Expected FUNCTIONS IS reply from client upon receiving FUNCTIONS REQUEST");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlainDataStreamFallbackInTn3270eMode() {
|
||||
config.setTn3270eEnabled(true);
|
||||
fsm.onConnected();
|
||||
|
||||
// Negotiate TN3270E with empty functions
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
|
||||
byte[] devTypeIs = new byte[]{
|
||||
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
|
||||
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
|
||||
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
|
||||
};
|
||||
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
|
||||
|
||||
byte[] funcsReq = new byte[]{
|
||||
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
|
||||
0x03, 0x07, (byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
|
||||
};
|
||||
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
|
||||
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
|
||||
|
||||
// Now host sends raw 3270 data stream without 5-byte TN3270E header (e.g. EraseWrite 0xF5)
|
||||
// 0xF5 = EraseWrite, 0xC3 = WCC, 0x11 = SBA, 0x40 0x40 = Pos 0, "HELLO"
|
||||
EbcdicTranslator trans = new EbcdicTranslator();
|
||||
String msg = "HELLO";
|
||||
byte[] rawStream = new byte[3 + 2 + msg.length() + 2];
|
||||
rawStream[0] = (byte) 0xF5; // EraseWrite
|
||||
rawStream[1] = (byte) 0xC3; // WCC
|
||||
rawStream[2] = 0x11; // SBA
|
||||
rawStream[3] = 0x40; rawStream[4] = 0x40; // addr 0
|
||||
for (int i = 0; i < msg.length(); i++) {
|
||||
rawStream[5 + i] = (byte) trans.unicodeToEbcdic(msg.charAt(i));
|
||||
}
|
||||
rawStream[rawStream.length - 2] = (byte) TelnetConstants.IAC;
|
||||
rawStream[rawStream.length - 1] = (byte) TelnetConstants.EOR;
|
||||
|
||||
for (byte b : rawStream) fsm.feedByte(b & 0xFF);
|
||||
|
||||
// State must have automatically switched to CONNECTED_3270 and screen updated
|
||||
assertEquals(ConnectionState.CONNECTED_3270, fsm.getConnectionState());
|
||||
assertEquals('H', trans.ebcdicToUnicode(screenBuffer.getCellEC(0)));
|
||||
assertEquals('E', trans.ebcdicToUnicode(screenBuffer.getCellEC(1)));
|
||||
assertEquals('L', trans.ebcdicToUnicode(screenBuffer.getCellEC(2)));
|
||||
assertEquals('L', trans.ebcdicToUnicode(screenBuffer.getCellEC(3)));
|
||||
assertEquals('O', trans.ebcdicToUnicode(screenBuffer.getCellEC(4)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTelnetTimingMarkOption6() {
|
||||
fsm.onConnected();
|
||||
connection.sentData.clear();
|
||||
|
||||
// Host sends DO TELOPT_TM (DO 6) -> Client must reply WILL TELOPT_TM (WILL 6) per RFC 860
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TM);
|
||||
boolean foundWillTm = false;
|
||||
for (byte[] pkt : connection.sentData) {
|
||||
if (pkt.length == 3 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
|
||||
(pkt[1] & 0xFF) == TelnetConstants.WILL && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TM) {
|
||||
foundWillTm = true;
|
||||
}
|
||||
}
|
||||
assertTrue(foundWillTm, "Expected IAC WILL TELOPT_TM reply upon receiving IAC DO TELOPT_TM");
|
||||
|
||||
// Host sends WILL TELOPT_TM (WILL 6) -> Client must reply DO TELOPT_TM (DO 6)
|
||||
connection.sentData.clear();
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TM);
|
||||
boolean foundDoTm = false;
|
||||
for (byte[] pkt : connection.sentData) {
|
||||
if (pkt.length == 3 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
|
||||
(pkt[1] & 0xFF) == TelnetConstants.DO && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TM) {
|
||||
foundDoTm = true;
|
||||
}
|
||||
}
|
||||
assertTrue(foundDoTm, "Expected IAC DO TELOPT_TM reply upon receiving IAC WILL TELOPT_TM");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIACAYT() {
|
||||
fsm.onConnected();
|
||||
connection.sentData.clear();
|
||||
|
||||
// Host sends IAC AYT
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.AYT);
|
||||
boolean foundNop = false;
|
||||
for (byte[] pkt : connection.sentData) {
|
||||
if (pkt.length == 2 && (pkt[0] & 0xFF) == TelnetConstants.IAC && (pkt[1] & 0xFF) == TelnetConstants.NOP) {
|
||||
foundNop = true;
|
||||
}
|
||||
}
|
||||
assertTrue(foundNop, "Expected IAC NOP acknowledgment for IAC AYT");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTTypeCycling() {
|
||||
fsm.onConnected();
|
||||
connection.sentData.clear();
|
||||
|
||||
// Enable TTYPE
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TTYPE);
|
||||
|
||||
// First SEND request -> Expect configured type IBM-3279-4-E
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
|
||||
TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE);
|
||||
byte[] lastPkt = connection.sentData.get(connection.sentData.size() - 1);
|
||||
String resp1 = new String(lastPkt, 4, lastPkt.length - 6);
|
||||
assertEquals("IBM-3279-4-E", resp1);
|
||||
|
||||
// Second SEND request -> Expect fallback IBM-3279-4
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
|
||||
TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE);
|
||||
lastPkt = connection.sentData.get(connection.sentData.size() - 1);
|
||||
String resp2 = new String(lastPkt, 4, lastPkt.length - 6);
|
||||
assertEquals("IBM-3279-4", resp2);
|
||||
|
||||
// Third SEND request -> Expect monochrome fallback IBM-3278-4-E
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
|
||||
TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE);
|
||||
lastPkt = connection.sentData.get(connection.sentData.size() - 1);
|
||||
String resp3 = new String(lastPkt, 4, lastPkt.length - 6);
|
||||
assertEquals("IBM-3278-4-E", resp3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package haus.nightmare.lib3270j.tls;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class TlsConfigTest {
|
||||
|
||||
@Test
|
||||
public void testConnectionConfigDefaults() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23);
|
||||
assertFalse(config.isUseTls());
|
||||
assertTrue(config.isTlsVerifyCert());
|
||||
assertEquals("TLS", config.getSslProtocol());
|
||||
assertNull(config.getCertificateVerifier());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseHostStringTlsPrefixes() {
|
||||
// L: prefix
|
||||
ConnectionConfig c1 = ConnectionConfig.parseHostString("L:mainframe.example.com", 0, TerminalModel.IBM_3279_4);
|
||||
assertTrue(c1.isUseTls());
|
||||
assertEquals("mainframe.example.com", c1.getHost());
|
||||
assertEquals(992, c1.getPort());
|
||||
|
||||
// ssl: prefix with explicit port
|
||||
ConnectionConfig c2 = ConnectionConfig.parseHostString("ssl:zos.local:2323", 0, TerminalModel.IBM_3279_4);
|
||||
assertTrue(c2.isUseTls());
|
||||
assertEquals("zos.local", c2.getHost());
|
||||
assertEquals(2323, c2.getPort());
|
||||
|
||||
// y: prefix
|
||||
ConnectionConfig c3 = ConnectionConfig.parseHostString("y:vm370.net:992", 23, TerminalModel.IBM_3279_4);
|
||||
assertTrue(c3.isUseTls());
|
||||
assertEquals("vm370.net", c3.getHost());
|
||||
assertEquals(992, c3.getPort());
|
||||
|
||||
// Plain host
|
||||
ConnectionConfig c4 = ConnectionConfig.parseHostString("plain.host.com", 23, TerminalModel.IBM_3279_4);
|
||||
assertFalse(c4.isUseTls());
|
||||
assertEquals("plain.host.com", c4.getHost());
|
||||
assertEquals(23, c4.getPort());
|
||||
|
||||
// IPv6 host with brackets
|
||||
ConnectionConfig c5 = ConnectionConfig.parseHostString("L:[2001:db8::1]:992", 0, TerminalModel.IBM_3279_4);
|
||||
assertTrue(c5.isUseTls());
|
||||
assertEquals("2001:db8::1", c5.getHost());
|
||||
assertEquals(992, c5.getPort());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTlsTrustManagerBypass() {
|
||||
ConnectionConfig config = new ConnectionConfig("untrusted.host", 992);
|
||||
config.setUseTls(true);
|
||||
config.setTlsVerifyCert(false);
|
||||
|
||||
TlsTrustManager trustManager = new TlsTrustManager(config);
|
||||
// With tlsVerifyCert=false, checkServerTrusted must not throw even with null/empty chain
|
||||
assertDoesNotThrow(() -> trustManager.checkServerTrusted(new X509Certificate[0], "RSA"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTlsTrustManagerVerifierCallback() {
|
||||
ConnectionConfig config = new ConnectionConfig("selfsigned.host", 992);
|
||||
config.setUseTls(true);
|
||||
config.setTlsVerifyCert(true);
|
||||
|
||||
AtomicBoolean verifierInvoked = new AtomicBoolean(false);
|
||||
config.setCertificateVerifier((chain, authType, exception) -> {
|
||||
verifierInvoked.set(true);
|
||||
return true; // Accept certificate
|
||||
});
|
||||
|
||||
TlsTrustManager trustManager = new TlsTrustManager(config);
|
||||
// Standard verification will fail on empty cert chain, triggering our callback
|
||||
assertDoesNotThrow(() -> trustManager.checkServerTrusted(new X509Certificate[0], "RSA"));
|
||||
assertTrue(verifierInvoked.get(), "Expected verifier callback to be invoked");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTlsTrustManagerVerifierRejection() {
|
||||
ConnectionConfig config = new ConnectionConfig("badcert.host", 992);
|
||||
config.setUseTls(true);
|
||||
config.setTlsVerifyCert(true);
|
||||
|
||||
config.setCertificateVerifier((chain, authType, exception) -> false); // Reject certificate
|
||||
|
||||
TlsTrustManager trustManager = new TlsTrustManager(config);
|
||||
assertThrows(CertificateException.class, () -> trustManager.checkServerTrusted(new X509Certificate[0], "RSA"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateSSLContext() throws Exception {
|
||||
ConnectionConfig config = new ConnectionConfig("secure.host", 992);
|
||||
config.setUseTls(true);
|
||||
|
||||
SSLContext context = TlsTrustManager.createSSLContext(config);
|
||||
assertNotNull(context);
|
||||
assertNotNull(context.getSocketFactory());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user