More NVT stuff
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m24s

This commit is contained in:
2026-08-31 15:54:26 -04:00
parent 7b5bc7ec11
commit a56aa5f059
82 changed files with 7319 additions and 203 deletions
@@ -0,0 +1,105 @@
package haus.nightmare.lib3270j.charset;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Phase 8 IBM 3270 APL / Graphic Escape (GA23-0059) character set translation.
*/
public class APLTranslationPhase8Test {
@Test
public void testAPLBoxDrawingGlyphs() {
EbcdicTranslator translator = new EbcdicTranslator();
// Box lines and corners
assertEquals('─', translator.mapAPL(0xA2)); // Horizontal line
assertEquals('│', translator.mapAPL(0x85)); // Vertical line
assertEquals('┌', translator.mapAPL(0xC5)); // Top Left corner
assertEquals('┐', translator.mapAPL(0xD5)); // Top Right corner
assertEquals('└', translator.mapAPL(0xC4)); // Bottom Left corner
assertEquals('┘', translator.mapAPL(0xD4)); // Bottom Right corner
assertEquals('├', translator.mapAPL(0xC6)); // Left T
assertEquals('┤', translator.mapAPL(0xD6)); // Right T
assertEquals('┬', translator.mapAPL(0xC7)); // Top T
assertEquals('┴', translator.mapAPL(0xD7)); // Bottom T
assertEquals('┼', translator.mapAPL(0xCB)); // Cross
}
@Test
public void testAPLMathAndSpecialSymbols() {
EbcdicTranslator translator = new EbcdicTranslator();
assertEquals('≤', translator.mapAPL(0x8C)); // Less than or equal
assertEquals('≥', translator.mapAPL(0xAE)); // Greater than or equal
assertEquals('≠', translator.mapAPL(0xBE)); // Not equal
assertEquals('[', translator.mapAPL(0xAD)); // Left bracket
assertEquals(']', translator.mapAPL(0xBD)); // Right bracket
assertEquals('{', translator.mapAPL(0x8D)); // Left brace
assertEquals('}', translator.mapAPL(0x9D)); // Right brace
assertEquals('°', translator.mapAPL(0xB0)); // Degree
assertEquals('±', translator.mapAPL(0xB1)); // Plus-minus
assertEquals('²', translator.mapAPL(0xB2)); // Superscript 2
assertEquals('³', translator.mapAPL(0xB3)); // Superscript 3
assertEquals('¯', translator.mapAPL(0xAF)); // Overbar / High Minus
assertEquals('Ω', translator.mapAPL(0xBA)); // Greek Omega
assertEquals('µ', translator.mapAPL(0xBF)); // Micro
assertEquals('¬', translator.mapAPL(0x5F)); // Not sign
}
@Test
public void testAPLOperatorsAndStructureGlyphs() {
EbcdicTranslator translator = new EbcdicTranslator();
assertEquals('⋄', translator.mapAPL(0x80)); // Diamond
assertEquals('⍺', translator.mapAPL(0x81)); // Alpha
assertEquals('⊥', translator.mapAPL(0x82)); // Up tack / decode
assertEquals('∩', translator.mapAPL(0x83)); // Intersection
assertEquals('⌊', translator.mapAPL(0x84)); // Floor
assertEquals('∇', translator.mapAPL(0x87)); // Del
assertEquals('∆', translator.mapAPL(0x88)); // Delta
assertEquals('⍳', translator.mapAPL(0x89)); // Iota
assertEquals('→', translator.mapAPL(0x8A)); // Right arrow
assertEquals('⍞', translator.mapAPL(0x8B)); // Quote quad
assertEquals('×', translator.mapAPL(0x8E)); // Multiply
assertEquals('÷', translator.mapAPL(0x8F)); // Divide
assertEquals('⍟', translator.mapAPL(0x90)); // Circle star
assertEquals('⌹', translator.mapAPL(0x91)); // Domino / quad divide
assertEquals('⊤', translator.mapAPL(0x92)); // Down tack / encode
assertEquals('∪', translator.mapAPL(0x93)); // Union
assertEquals('⌈', translator.mapAPL(0x94)); // Ceiling
assertEquals('⍴', translator.mapAPL(0x95)); // Rho / shape
assertEquals('⍵', translator.mapAPL(0x96)); // Omega
assertEquals('○', translator.mapAPL(0x99)); // Circle
assertEquals('←', translator.mapAPL(0x9A)); // Left arrow
assertEquals('⍋', translator.mapAPL(0x9C)); // Grade up
assertEquals('⍒', translator.mapAPL(0x9E)); // Grade down
assertEquals('⍝', translator.mapAPL(0x9F)); // Lamp / comment
assertEquals('⊂', translator.mapAPL(0xA6)); // Enclose / left shoe
assertEquals('⊃', translator.mapAPL(0xA7)); // Disclose / right shoe
assertEquals('↑', translator.mapAPL(0xA8)); // Take / up arrow
assertEquals('↓', translator.mapAPL(0xA9)); // Drop / down arrow
assertEquals('⎕', translator.mapAPL(0xAA)); // Quad
assertEquals('⍉', translator.mapAPL(0xAC)); // Transpose
assertEquals('⊖', translator.mapAPL(0xB4)); // Circle bar
}
@Test
public void testScreenBufferAPLCharacterSetIntegration() {
EbcdicTranslator translator = new EbcdicTranslator();
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
// Place box-drawing corner and horizontal line with cs=1 (CS_APL)
screen.setCellWithCS(0, 0xC5, 1); // Top left ┌
screen.setCellWithCS(1, 0xA2, 1); // Horizontal ─
screen.setCellWithCS(2, 0xD5, 1); // Top right ┐
screen.translateToUnicode();
assertEquals('┌', (char) screen.getCell(0).ucs4);
assertEquals('─', (char) screen.getCell(1).ucs4);
assertEquals('┐', (char) screen.getCell(2).ucs4);
}
}
@@ -0,0 +1,79 @@
package haus.nightmare.lib3270j.charset;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Phase 8 custom user-defined character translation override tables.
*/
public class CustomTranslationOverridePhase8Test {
@Test
public void testSingleOverride() {
EbcdicTranslator translator = new EbcdicTranslator("037");
assertFalse(translator.hasCustomOverrides());
// Default CP037 mapping for 0xC1 is 'A'
assertEquals('A', translator.ebcdicToUnicode(0xC1));
assertEquals(0xC1, translator.unicodeToEbcdic('A'));
// Override 0xC1 to '★' (\u2605)
translator.setCustomOverride(0xC1, '★');
assertTrue(translator.hasCustomOverrides());
assertEquals('★', translator.ebcdicToUnicode(0xC1));
assertEquals(0xC1, translator.unicodeToEbcdic('★'));
// Non-overridden characters should continue using default CP037
assertEquals('B', translator.ebcdicToUnicode(0xC2));
assertEquals(0xC2, translator.unicodeToEbcdic('B'));
// Safe conversion
assertEquals((byte) 0xC1, translator.unicodeToEbcdicSafe('★'));
// Remove override
translator.removeCustomOverride(0xC1);
assertFalse(translator.hasCustomOverrides());
assertEquals('A', translator.ebcdicToUnicode(0xC1));
}
@Test
public void testBulkOverrides() {
EbcdicTranslator translator = new EbcdicTranslator("037");
Map<Integer, Character> ebcToUni = new HashMap<>();
ebcToUni.put(0x81, 'α'); // replace 'a' with alpha
ebcToUni.put(0x82, 'β'); // replace 'b' with beta
translator.setCustomOverrides(ebcToUni, null);
assertTrue(translator.hasCustomOverrides());
assertEquals('α', translator.ebcdicToUnicode(0x81));
assertEquals('β', translator.ebcdicToUnicode(0x82));
assertEquals('c', translator.ebcdicToUnicode(0x83));
assertEquals(0x81, translator.unicodeToEbcdic('α'));
assertEquals(0x82, translator.unicodeToEbcdic('β'));
// Clear overrides
translator.clearCustomOverrides();
assertFalse(translator.hasCustomOverrides());
assertEquals('a', translator.ebcdicToUnicode(0x81));
assertEquals('b', translator.ebcdicToUnicode(0x82));
}
@Test
public void testStringTranslationWithOverrides() {
EbcdicTranslator translator = new EbcdicTranslator("037");
translator.setCustomOverride(0xBA, '❮'); // override '[' with '❮'
translator.setCustomOverride(0xBB, '❯'); // override ']' with '❯'
byte[] input = new byte[] { (byte) 0xBA, (byte) 0xC8, (byte) 0xC9, (byte) 0xBB }; // [HI] in CP037
String decoded = translator.ebcdicToString(input, 0, input.length);
assertEquals("❮HI❯", decoded);
byte[] reEncoded = translator.stringToEbcdic("❮HI❯");
assertArrayEquals(input, reEncoded);
}
}
@@ -0,0 +1,89 @@
package haus.nightmare.lib3270j.charset;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Phase 8 DBCS SO/SI escape sequence handling, shift preservation, and UTF-8 conversion.
*/
public class DBCSSOSIEscapingPhase8Test {
@Test
public void testSOSIPreserveMode() {
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) CodePageRegistry.getCodePage("930");
cp.registerDbcsPair(0x4341, '\u6771'); // '東'
cp.registerDbcsPair(0x4342, '\u4EAC'); // '京'
// EBCDIC: 'I'(0xC9) + SO(0x0E) + 東(0x4341) + 京(0x4342) + SI(0x0F) + '1'(0xF1)
byte[] ebcdic = new byte[] {
(byte) 0xC9,
0x0E, (byte) 0x43, (byte) 0x41, (byte) 0x43, (byte) 0x42, 0x0F,
(byte) 0xF1
};
// Standard mode strips SO/SI from Unicode string
String standard = cp.ebcdicToString(ebcdic, 0, ebcdic.length, false);
assertEquals("I東京1", standard);
// Preserved mode keeps \u000E and \u000F in Unicode string
String preserved = cp.ebcdicToString(ebcdic, 0, ebcdic.length, true);
assertEquals("I\u000E東京\u000F1", preserved);
// Utility helper
String utf8WithSosi = cp.ebcdicToUtf8WithSOSI(ebcdic);
assertEquals(preserved, utf8WithSosi);
}
@Test
public void testEmbeddedSOSIStringConversion() {
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) CodePageRegistry.getCodePage("930");
cp.registerDbcsPair(0x4341, '\u6771'); // '東'
cp.registerDbcsPair(0x4342, '\u4EAC'); // '京'
// Encode string that already contains embedded \u000E and \u000F
String inputWithSOSI = "I\u000E東京\u000F1";
byte[] encoded = cp.stringToEbcdic(inputWithSOSI, true);
byte[] expected = new byte[] {
(byte) 0xC9,
0x0E, (byte) 0x43, (byte) 0x41, (byte) 0x43, (byte) 0x42, 0x0F,
(byte) 0xF1
};
assertArrayEquals(expected, encoded);
// Utility helper
byte[] helperEncoded = cp.utf8WithSOSIToEbcdic(inputWithSOSI);
assertArrayEquals(expected, helperEncoded);
}
@Test
public void testAutoCloseUnclosedDBCSShift() {
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) CodePageRegistry.getCodePage("930");
cp.registerDbcsPair(0x4341, '\u6771'); // '東'
// Text ending with a DBCS character without explicit SI
String s = "A東京";
byte[] encoded = cp.stringToEbcdic(s, false);
// Must end with SI (0x0F)
assertTrue(encoded.length > 0);
assertEquals(0x0F, encoded[encoded.length - 1] & 0xFF);
}
@Test
public void testRedundantSOSISuppression() {
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) CodePageRegistry.getCodePage("930");
cp.registerDbcsPair(0x4341, '\u6771');
// Input containing redundant duplicate \u000E \u000E
String s = "\u000E\u000E東\u000F\u000F";
byte[] encoded = cp.stringToEbcdic(s, true);
// Should contain only one SO (0x0E) and one SI (0x0F)
assertEquals(4, encoded.length);
assertEquals(0x0E, encoded[0] & 0xFF);
assertEquals(0x43, encoded[1] & 0xFF);
assertEquals(0x41, encoded[2] & 0xFF);
assertEquals(0x0F, encoded[3] & 0xFF);
}
}
@@ -0,0 +1,194 @@
package haus.nightmare.lib3270j.charset;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Phase 8 Extended EBCDIC Codepages (Arabic, Hebrew, Thai, Cyrillic, Turkish, Extended DBCS).
*/
public class ExtendedCodePagesPhase8Test {
@Test
public void testArabicCp420() {
CodePage cp = CodePageRegistry.getCodePage("420");
assertNotNull(cp);
assertEquals("420", cp.getCodePageId());
assertEquals(420, cp.getCpgid());
assertFalse(cp.isDBCS());
// Test alias resolution
assertEquals("420", CodePageRegistry.getCodePage("ar").getCodePageId());
assertEquals("420", CodePageRegistry.getCodePage("arabic").getCodePageId());
assertEquals("420", CodePageRegistry.getCodePage("ebcdic-cp-ar").getCodePageId());
assertEquals("420", CodePageRegistry.getCodePage("IBM420").getCodePageId());
// Basic ASCII and Arabic character mapping
assertEquals('A', cp.ebcdicToUnicode(0xC1));
assertEquals(0xC1, cp.unicodeToEbcdic('A'));
assertEquals(' ', cp.ebcdicToUnicode(0x40));
assertEquals(0x40, cp.unicodeToEbcdic(' '));
// Arabic characters (Hamza, Alef, Yeh, etc.)
int ebcHamza = cp.unicodeToEbcdic('\u0621');
assertTrue(ebcHamza >= 0);
assertEquals('\u0621', cp.ebcdicToUnicode(ebcHamza));
}
@Test
public void testHebrewCp424AndCp803() {
CodePage cp424 = CodePageRegistry.getCodePage("424");
assertNotNull(cp424);
assertEquals("424", cp424.getCodePageId());
assertEquals(424, cp424.getCpgid());
assertEquals(941, cp424.getCgcsgid());
assertEquals("424", CodePageRegistry.getCodePage("he").getCodePageId());
assertEquals("424", CodePageRegistry.getCodePage("hebrew").getCodePageId());
// Hebrew Alef \u05D0 in Cp424
int ebcAlef424 = cp424.unicodeToEbcdic('\u05D0');
assertTrue(ebcAlef424 >= 0);
assertEquals('\u05D0', cp424.ebcdicToUnicode(ebcAlef424));
CodePage cp803 = CodePageRegistry.getCodePage("803");
assertNotNull(cp803);
assertEquals("803", cp803.getCodePageId());
assertEquals(803, cp803.getCpgid());
assertEquals(1147, cp803.getCgcsgid());
assertEquals("803", CodePageRegistry.getCodePage("hebrew-old").getCodePageId());
assertEquals("803", CodePageRegistry.getCodePage("israel").getCodePageId());
assertEquals("803", CodePageRegistry.getCodePage("iw").getCodePageId());
// Hebrew Alef in Cp803
int ebcAlef803 = cp803.unicodeToEbcdic('\u05D0');
assertTrue(ebcAlef803 >= 0);
assertEquals('\u05D0', cp803.ebcdicToUnicode(ebcAlef803));
}
@Test
public void testThaiCp838AndCp1160() {
CodePage cp838 = CodePageRegistry.getCodePage("838");
assertNotNull(cp838);
assertEquals("838", cp838.getCodePageId());
assertEquals(838, cp838.getCpgid());
assertEquals(1176, cp838.getCgcsgid());
assertEquals("838", CodePageRegistry.getCodePage("th").getCodePageId());
assertEquals("838", CodePageRegistry.getCodePage("thai").getCodePageId());
// Thai character Ko Kai \u0E01
int ebcKoKai = cp838.unicodeToEbcdic('\u0E01');
assertTrue(ebcKoKai >= 0);
assertEquals('\u0E01', cp838.ebcdicToUnicode(ebcKoKai));
CodePage cp1160 = CodePageRegistry.getCodePage("1160");
assertNotNull(cp1160);
assertEquals("1160", cp1160.getCodePageId());
assertEquals(1160, cp1160.getCpgid());
assertEquals('€', cp1160.ebcdicToUnicode(0x9F));
assertEquals(0x9F, cp1160.unicodeToEbcdic('€'));
assertEquals("1160", CodePageRegistry.getCodePage("thai-euro").getCodePageId());
}
@Test
public void testCyrillicCp1025Cp1123Cp1154Cp880() {
CodePage cp1025 = CodePageRegistry.getCodePage("1025");
assertNotNull(cp1025);
assertEquals(1025, cp1025.getCpgid());
assertEquals(1150, cp1025.getCgcsgid());
assertEquals("1025", CodePageRegistry.getCodePage("ru").getCodePageId());
assertEquals("1025", CodePageRegistry.getCodePage("cyrillic").getCodePageId());
assertEquals("1025", CodePageRegistry.getCodePage("russian").getCodePageId());
// Cyrillic Capital Letter A \u0410 and small a \u0430
int ebcA1025 = cp1025.unicodeToEbcdic('\u0410');
assertTrue(ebcA1025 >= 0);
assertEquals('\u0410', cp1025.ebcdicToUnicode(ebcA1025));
// Cp1123 Ukraine
CodePage cp1123 = CodePageRegistry.getCodePage("1123");
assertNotNull(cp1123);
assertEquals(1123, cp1123.getCpgid());
assertEquals(1399, cp1123.getCgcsgid());
assertEquals("1123", CodePageRegistry.getCodePage("ukraine").getCodePageId());
assertEquals("1123", CodePageRegistry.getCodePage("ukrainian").getCodePageId());
// Cp1154 Cyrillic with Euro
CodePage cp1154 = CodePageRegistry.getCodePage("1154");
assertNotNull(cp1154);
assertEquals(1154, cp1154.getCpgid());
assertEquals(1305, cp1154.getCgcsgid());
assertEquals('€', cp1154.ebcdicToUnicode(0x9F));
assertEquals("1154", CodePageRegistry.getCodePage("cyrillic-euro").getCodePageId());
// Cp880 Cyrillic Russian
CodePage cp880 = CodePageRegistry.getCodePage("880");
assertNotNull(cp880);
assertEquals(880, cp880.getCpgid());
assertEquals(960, cp880.getCgcsgid());
assertEquals("880", CodePageRegistry.getCodePage("cyrillic-russian").getCodePageId());
}
@Test
public void testTurkishCp1155AndCp905() {
CodePage cp1155 = CodePageRegistry.getCodePage("1155");
assertNotNull(cp1155);
assertEquals(1155, cp1155.getCpgid());
assertEquals(1306, cp1155.getCgcsgid());
assertEquals('€', cp1155.ebcdicToUnicode(0x9F));
assertEquals("1155", CodePageRegistry.getCodePage("turkish-euro").getCodePageId());
CodePage cp905 = CodePageRegistry.getCodePage("905");
assertNotNull(cp905);
assertEquals(905, cp905.getCpgid());
assertEquals(1151, cp905.getCgcsgid());
assertEquals("905", CodePageRegistry.getCodePage("turkish-latin3").getCodePageId());
}
@Test
public void testExtendedChineseDBCSCp1388AndCp1371() {
CodePage cp1388 = CodePageRegistry.getCodePage("1388");
assertNotNull(cp1388);
assertTrue(cp1388.isDBCS());
assertEquals(1388, cp1388.getCpgid());
assertEquals(1175, cp1388.getCgcsgid());
assertEquals("1388", CodePageRegistry.getCodePage("chinese-ext-simplified").getCodePageId());
assertEquals("1388", CodePageRegistry.getCodePage("zh-simplified-ext").getCodePageId());
CodePage cp1371 = CodePageRegistry.getCodePage("1371");
assertNotNull(cp1371);
assertTrue(cp1371.isDBCS());
assertEquals(1371, cp1371.getCpgid());
assertEquals(1174, cp1371.getCgcsgid());
assertEquals("1371", CodePageRegistry.getCodePage("chinese-ext-traditional").getCodePageId());
assertEquals("1371", CodePageRegistry.getCodePage("zh-traditional-ext").getCodePageId());
}
@Test
public void testHasCodePageQuery() {
assertTrue(CodePageRegistry.hasCodePage("037"));
assertTrue(CodePageRegistry.hasCodePage("1047"));
assertTrue(CodePageRegistry.hasCodePage("420"));
assertTrue(CodePageRegistry.hasCodePage("424"));
assertTrue(CodePageRegistry.hasCodePage("803"));
assertTrue(CodePageRegistry.hasCodePage("838"));
assertTrue(CodePageRegistry.hasCodePage("1160"));
assertTrue(CodePageRegistry.hasCodePage("1025"));
assertTrue(CodePageRegistry.hasCodePage("1123"));
assertTrue(CodePageRegistry.hasCodePage("1154"));
assertTrue(CodePageRegistry.hasCodePage("880"));
assertTrue(CodePageRegistry.hasCodePage("1155"));
assertTrue(CodePageRegistry.hasCodePage("905"));
assertTrue(CodePageRegistry.hasCodePage("1388"));
assertTrue(CodePageRegistry.hasCodePage("1371"));
assertTrue(CodePageRegistry.hasCodePage("ar"));
assertTrue(CodePageRegistry.hasCodePage("hebrew"));
assertTrue(CodePageRegistry.hasCodePage("thai"));
assertTrue(CodePageRegistry.hasCodePage("russian"));
assertTrue(CodePageRegistry.hasCodePage("ukraine"));
assertFalse(CodePageRegistry.hasCodePage("nonexistent-cp-9999"));
}
}
@@ -0,0 +1,453 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
/**
* Comprehensive Unit Tests for Phase 10: ECL API & Session Architecture Facade.
*/
public class ECLSessionPhase10Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLPS ps;
private ECLOIA oia;
private Telnet3270Client client;
private ECLSession session;
@BeforeEach
public void setUp() {
ConnectionConfig config = new ConnectionConfig("mainframe.example.com", 23, TerminalModel.IBM_3279_4);
config.setLuName("LU3270A");
config.setCodePage("037");
client = new Telnet3270Client(config);
session = new ECLSession(client);
ps = session.GetPS();
oia = session.GetOIA();
screen = client.getScreenBuffer();
translator = client.getTranslator();
input = client.getInputProcessor();
}
@Test
public void testECLSessionInstantiationWithConfigAndProperties() {
// Test default constructor
ECLSession defSession = new ECLSession();
assertNotNull(defSession.GetPS());
assertNotNull(defSession.GetOIA());
assertNotNull(defSession.GetConnection());
assertNotNull(defSession.GetXfer());
assertNotNull(defSession.GetFieldList());
assertNotNull(defSession.GetClient());
assertEquals(23, defSession.GetConnection().GetPort());
// Test explicit host/port/model constructor
ECLSession hpSession = new ECLSession("tso.test.org", 992, TerminalModel.IBM_3279_2, true);
assertEquals("tso.test.org", hpSession.GetConnection().GetHost());
assertEquals(992, hpSession.GetConnection().GetPort());
assertEquals(TerminalModel.IBM_3279_2, hpSession.GetConnection().GetModel());
assertTrue(hpSession.GetConnection().IsSSL());
// Test Properties constructor
Properties props = new Properties();
props.setProperty(ECLSession.SESSION_HOST, "mvs.corp.net");
props.setProperty(ECLSession.SESSION_PORT, "2023");
props.setProperty(ECLSession.SESSION_CODE_PAGE, "1047");
props.setProperty(ECLSession.SESSION_MODEL, "5"); // 27x132
props.setProperty(ECLSession.SESSION_SSL, "true");
props.setProperty(ECLSession.SESSION_LU_NAME, "TSOLU01");
props.setProperty(ECLSession.SESSION_TN3270E, "true");
ECLSession propSession = new ECLSession(props);
assertEquals("mvs.corp.net", propSession.GetConnection().GetHost());
assertEquals(2023, propSession.GetConnection().GetPort());
assertEquals("1047", propSession.GetConnection().GetCodePage());
assertEquals(TerminalModel.IBM_3279_5, propSession.GetConnection().GetModel());
assertTrue(propSession.GetConnection().IsSSL());
assertEquals("TSOLU01", propSession.GetConnection().GetLUName());
assertEquals("TN3270E", propSession.GetConnection().GetConnType());
// Verify Properties sync and retrieval
Properties retrieved = propSession.GetProperties();
assertEquals("mvs.corp.net", retrieved.getProperty(ECLSession.SESSION_HOST));
assertEquals("2023", retrieved.getProperty(ECLSession.SESSION_PORT));
}
@Test
public void testECLConnectionAttributesAndState() {
ECLConnection conn = session.GetConnection();
assertNotNull(conn);
assertEquals("mainframe.example.com", conn.GetHost());
assertEquals(23, conn.GetPort());
assertEquals("037", conn.GetCodePage());
assertEquals(TerminalModel.IBM_3279_4, conn.GetModel());
assertEquals("LU3270A", conn.GetLUName());
assertEquals("LU3270A", conn.GetDevName());
assertFalse(conn.IsSSL());
assertEquals(ConnectionState.NOT_CONNECTED, conn.GetState());
assertEquals(0, conn.GetStateCode());
assertFalse(conn.IsConnected());
assertFalse(conn.IsReady());
assertTrue(conn.IsDisconnecting());
// Verify state mappings
assertEquals(0, ConnectionState.NOT_CONNECTED.toHoDStateCode());
assertEquals(1, ConnectionState.TELNET_PENDING.toHoDStateCode());
assertEquals(2, ConnectionState.CONNECTED_NVT.toHoDStateCode());
assertEquals(2, ConnectionState.CONNECTED_UNBOUND.toHoDStateCode());
assertEquals(3, ConnectionState.CONNECTED_3270.toHoDStateCode());
assertEquals(3, ConnectionState.CONNECTED_TN3270E.toHoDStateCode());
}
@Test
public void testECLScreenDescMatching() {
// Setup screen buffer with sample formatted screen
screen.clear();
screen.setCursorAddress(0);
// Write: SF (prot), "TSO/E LOGON", SF (unprot), "USER01", SF (prot), "PASSWORD", SF (unprot), " "
screen.setCellFA(0, (byte) 0x60); // SF protected
String text1 = "TSO/E LOGON";
for (int i = 0; i < text1.length(); i++) {
screen.setChar(0, i + 1, text1.charAt(i));
}
int userFaPos = 20;
screen.setCellFA(userFaPos, (byte) 0x40); // SF unprotected
String userVal = "USER01";
for (int i = 0; i < userVal.length(); i++) {
screen.setChar(0, userFaPos + 1 + i, userVal.charAt(i));
}
int passFaPos = 40;
screen.setCellFA(passFaPos, (byte) 0x60); // SF protected
String passLabel = "PASSWORD";
for (int i = 0; i < passLabel.length(); i++) {
screen.setChar(0, passFaPos + 1 + i, passLabel.charAt(i));
}
int passInputPos = 60;
screen.setCellFA(passInputPos, (byte) 0x40); // SF unprot hidden
ps.setCursorPos(userFaPos + 1);
// 1. Test match string at 1-based row/col
ECLScreenDesc desc1 = new ECLScreenDesc();
desc1.AddString("TSO/E LOGON", 1, 2, true);
assertTrue(desc1.Matches(session));
assertTrue(desc1.Matches(ps, oia));
assertTrue(desc1.Matches(client));
// Test case sensitivity
ECLScreenDesc descCaseMismatch = new ECLScreenDesc();
descCaseMismatch.AddString("tso/e logon", 1, 2, true);
assertFalse(descCaseMismatch.Matches(session));
ECLScreenDesc descCaseInsensitive = new ECLScreenDesc();
descCaseInsensitive.AddString("tso/e logon", 1, 2, false);
assertTrue(descCaseInsensitive.Matches(session));
// 2. Test match string anywhere on screen
ECLScreenDesc descAnywhere = new ECLScreenDesc();
descAnywhere.AddString("PASSWORD");
assertTrue(descAnywhere.Matches(session));
ECLScreenDesc descAnywhereFail = new ECLScreenDesc();
descAnywhereFail.AddString("CICS TRANSACTION");
assertFalse(descAnywhereFail.Matches(session));
// 3. Test match string in rectangular region
ECLScreenDesc descRect = new ECLScreenDesc();
descRect.AddStringInRect("USER01", 1, 1, 2, 80, true);
assertTrue(descRect.Matches(session));
ECLScreenDesc descRectFail = new ECLScreenDesc();
descRectFail.AddStringInRect("USER01", 2, 1, 5, 80, true);
assertFalse(descRectFail.Matches(session));
// 4. Test match cursor position
ECLScreenDesc descCursor = new ECLScreenDesc();
descCursor.AddCursorPos(1, userFaPos + 2);
assertTrue(descCursor.Matches(session));
ECLScreenDesc descCursorFail = new ECLScreenDesc();
descCursorFail.AddCursorPos(10, 10);
assertFalse(descCursorFail.Matches(session));
// 5. Test field counts
ECLScreenDesc descFields = new ECLScreenDesc();
descFields.AddNumFields(4);
descFields.AddNumInputFields(2);
assertTrue(descFields.Matches(session));
ECLScreenDesc descFieldsFail = new ECLScreenDesc();
descFieldsFail.AddNumFields(10);
assertFalse(descFieldsFail.Matches(session));
// 6. Test OIA status
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
ECLScreenDesc descOia = new ECLScreenDesc();
descOia.AddOIAStatus(ECLConstants.INHIBIT_NOT_INHIBITED);
assertTrue(descOia.Matches(session));
oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK);
assertFalse(descOia.Matches(session));
ECLScreenDesc descOiaLocked = new ECLScreenDesc();
descOiaLocked.AddOIAStatus(ECLConstants.INHIBIT_SYSTEM_LOCK);
assertTrue(descOiaLocked.Matches(session));
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
// 7. Test Clear and composite descriptor
ECLScreenDesc composite = new ECLScreenDesc();
composite.AddString("TSO/E LOGON", 1, 2);
composite.AddString("PASSWORD");
composite.AddNumFields(4);
assertEquals(3, composite.getConditionCount());
assertTrue(composite.Matches(session));
composite.Clear();
assertEquals(0, composite.getConditionCount());
assertTrue(composite.Matches(session)); // Empty descriptor matches everything
}
@Test
public void testECLPSListenerAndEventDispatching() {
AtomicInteger updateCount = new AtomicInteger(0);
AtomicInteger cursorCount = new AtomicInteger(0);
AtomicInteger alarmCount = new AtomicInteger(0);
AtomicInteger resizeCount = new AtomicInteger(0);
AtomicReference<ECLPSEvent> lastEvent = new AtomicReference<>();
ECLPSListener listener = new ECLPSListener() {
@Override
public void psChanged(ECLPSEvent event) {
updateCount.incrementAndGet();
lastEvent.set(event);
}
@Override
public void psCursorMoved(ECLPSEvent event) {
cursorCount.incrementAndGet();
}
@Override
public void psAlarm(ECLPSEvent event) {
alarmCount.incrementAndGet();
}
@Override
public void psResized(ECLPSEvent event) {
resizeCount.incrementAndGet();
}
};
ps.RegisterPSEvent(listener);
// 1. Trigger PS Update
ps.notifyPSUpdate(2, 5, 2, 20, false);
assertEquals(1, updateCount.get());
assertNotNull(lastEvent.get());
assertEquals(ECLPSEvent.PS_UPDATE, lastEvent.get().getEventType());
assertEquals(2, lastEvent.get().getStartRow());
assertEquals(5, lastEvent.get().getStartCol());
assertEquals(2, lastEvent.get().getEndRow());
assertEquals(20, lastEvent.get().getEndCol());
assertFalse(lastEvent.get().isFullUpdate());
assertSame(ps, lastEvent.get().getPS());
// 2. Trigger Cursor Move
ps.notifyCursorMoved(0, 85);
assertEquals(2, updateCount.get());
assertEquals(1, cursorCount.get());
assertEquals(ECLPSEvent.PS_CURSOR, lastEvent.get().getEventType());
assertEquals(85, lastEvent.get().getNewCursorAddress());
// 3. Trigger Alarm
ps.notifyAlarm();
assertEquals(3, updateCount.get());
assertEquals(1, alarmCount.get());
assertEquals(ECLPSEvent.PS_ALARM, lastEvent.get().getEventType());
// 4. Trigger Screen Resize
ps.notifyScreenResized(32, 80);
assertEquals(4, updateCount.get());
assertEquals(1, resizeCount.get());
assertEquals(ECLPSEvent.PS_RESIZE, lastEvent.get().getEventType());
assertEquals(32, lastEvent.get().getRows());
assertEquals(80, lastEvent.get().getCols());
// 5. Unregister listener
ps.UnregisterPSEvent(listener);
ps.notifyPSUpdate(0, 0, 23, 79, true);
assertEquals(4, updateCount.get()); // Count should not change after unregister
}
@Test
public void testECLOIAListenerAndEventDispatching() {
AtomicInteger changeCount = new AtomicInteger(0);
AtomicInteger lockCount = new AtomicInteger(0);
AtomicReference<ECLOIAEvent> lastOiaEvent = new AtomicReference<>();
ECLOIAListener listener = new ECLOIAListener() {
@Override
public void oiaChanged(ECLOIAEvent event) {
changeCount.incrementAndGet();
lastOiaEvent.set(event);
}
@Override
public void oiaLockStateChanged(ECLOIAEvent event) {
lockCount.incrementAndGet();
}
};
oia.RegisterOIAEvent(listener);
// 1. Change inhibit state
oia.setInputInhibited(ECLConstants.INHIBIT_PROTECTED_FIELD);
assertEquals(1, changeCount.get());
assertNotNull(lastOiaEvent.get());
assertEquals(ECLConstants.INHIBIT_PROTECTED_FIELD, lastOiaEvent.get().getInputInhibited());
assertEquals("X-PROT", lastOiaEvent.get().getStatusString());
assertTrue(lastOiaEvent.get().isInputInhibited());
assertSame(oia, lastOiaEvent.get().getOIA());
// 2. Change inhibit back to normal
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
assertEquals(2, changeCount.get());
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, lastOiaEvent.get().getInputInhibited());
assertEquals("READY", lastOiaEvent.get().getStatusString());
assertFalse(lastOiaEvent.get().isInputInhibited());
// 3. Test uppercase wait methods
assertTrue(oia.WaitForInput(100));
assertTrue(oia.WaitForSystemAvailable(100));
assertTrue(oia.WaitForAppAvailable(100));
// 4. Unregister listener
oia.UnregisterOIAEvent(listener);
oia.setInputInhibited(ECLConstants.INHIBIT_NUMERIC_ONLY);
assertEquals(2, changeCount.get());
}
@Test
public void testECLCommListenerAndEventDispatching() {
ECLConnection conn = session.GetConnection();
AtomicInteger commCount = new AtomicInteger(0);
AtomicInteger errorCount = new AtomicInteger(0);
AtomicBoolean notifyConnected = new AtomicBoolean(false);
AtomicReference<ECLCommEvent> lastCommEvent = new AtomicReference<>();
ECLCommListener listener = new ECLCommListener() {
@Override
public void commEvent(ECLCommEvent event) {
commCount.incrementAndGet();
lastCommEvent.set(event);
}
@Override
public void commError(ECLCommEvent event) {
errorCount.incrementAndGet();
}
};
conn.RegisterCommEvent(listener);
conn.RegisterCommEvent(connected -> notifyConnected.set(connected), false);
// Simulate connection state change via listener
client.getTelnetFSM().onError("Connection reset by peer");
assertEquals(1, commCount.get());
assertEquals(1, errorCount.get());
assertNotNull(lastCommEvent.get());
assertEquals(ECLCommEvent.COMM_ERROR, lastCommEvent.get().getEventType());
assertEquals("Connection reset by peer", lastCommEvent.get().getErrorMessage());
// Unregister
conn.UnregisterCommEvent(listener);
}
@Test
public void testSendKeysWithCoordinates() {
screen.clear();
screen.setCursorAddress(0);
// Setup an unprotected field at row 5, col 10 (0-based: row 4, col 9)
int faPos = 4 * 80 + 8; // 328
screen.setCellFA(faPos, (byte) 0x40); // Unprotected
// Send keys with 1-based (row 5, col 10)
session.SendKeys("IBM3270", 5, 10);
assertEquals("IBM3270", ps.getString(faPos + 1, 7));
// Send bracketed mnemonics
ps.setCursorPos(1, 1);
ps.SendKeys("A[tab]B", 1, 1);
assertNotNull(ps.getString(0, 10));
}
@Test
public void testWaitForScreenWithDescriptor() {
screen.clear();
screen.setChar(0, 0, 'L');
screen.setChar(0, 1, 'O');
screen.setChar(0, 2, 'G');
screen.setChar(0, 3, 'O');
screen.setChar(0, 4, 'N');
ECLScreenDesc desc = new ECLScreenDesc();
desc.AddString("LOGON", 1, 1);
assertTrue(session.WaitForScreen(desc, 100));
assertTrue(ps.WaitForScreen(desc, 100));
ECLScreenDesc descFail = new ECLScreenDesc();
descFail.AddString("NONEXISTENT", 1, 1);
assertFalse(session.WaitForScreen(descFail, 50));
}
@Test
public void testIBMHoDPackageCompatibility() {
// Instantiate using haus.nightmare.lib3270j.eNetwork.ECL.* package hierarchy
haus.nightmare.lib3270j.eNetwork.ECL.ECLSession hodSession = new haus.nightmare.lib3270j.eNetwork.ECL.ECLSession();
assertNotNull(hodSession.GetPS());
assertNotNull(hodSession.GetOIA());
assertNotNull(hodSession.GetConnection());
assertNotNull(hodSession.GetXfer());
assertNotNull(hodSession.GetFieldList());
haus.nightmare.lib3270j.eNetwork.ECL.ECLScreenDesc hodDesc = new haus.nightmare.lib3270j.eNetwork.ECL.ECLScreenDesc();
hodDesc.AddString("TEST");
assertEquals(1, hodDesc.getConditionCount());
AtomicBoolean hodPsReceived = new AtomicBoolean(false);
hodSession.GetPS().RegisterPSEvent(new haus.nightmare.lib3270j.eNetwork.ECL.event.ECLPSListener() {
@Override
public void psChanged(haus.nightmare.lib3270j.ecl.ECLPSEvent event) {
hodPsReceived.set(true);
}
});
hodSession.GetPS().notifyPSUpdate(0, 0, 10, 10, true);
assertTrue(hodPsReceived.get());
// Test exception
haus.nightmare.lib3270j.eNetwork.ECL.ECLException ex = new haus.nightmare.lib3270j.eNetwork.ECL.ECLException(haus.nightmare.lib3270j.eNetwork.ECL.ECLErrors.ECL_ERR_COMM_TIMEOUT, "Timeout");
assertEquals(haus.nightmare.lib3270j.eNetwork.ECL.ECLErrors.ECL_ERR_COMM_TIMEOUT, ex.getErrorCode());
assertEquals("Timeout", ex.getMessage());
}
}
@@ -88,4 +88,51 @@ public class ECLXferTest {
assertTrue(loaded[0]);
assertEquals(1, fileList.size());
}
@Test
public void testGetFilesBatchDownloadCallback() {
boolean[] loaded = new boolean[1];
xfer.getFiles("PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01\n", "/tmp/nonexistent_test_dir", new haus.nightmare.lib3270j.ft.dir.FileTransferHostDirectoryInterface() {
@Override
public void onDirectoryLoaded(List<? extends HostDirectoryEntry> entries) {
loaded[0] = true;
}
@Override
public void onDirectoryError(String errorMessage) {}
});
assertTrue(loaded[0]);
}
@Test
public void testGetFilesVectorOverload() {
java.util.Vector<String> localFiles = new java.util.Vector<>();
java.util.Vector<String> hostFiles = new java.util.Vector<>();
boolean[] loaded = new boolean[1];
xfer.getFiles("PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01\n", 0, 0, localFiles, hostFiles, new haus.nightmare.lib3270j.ft.dir.FileTransferHostDirectoryInterface() {
@Override
public void onDirectoryLoaded(List<? extends HostDirectoryEntry> entries) {
loaded[0] = true;
}
@Override
public void onDirectoryError(String errorMessage) {}
});
assertTrue(loaded[0]);
assertEquals(1, hostFiles.size());
assertEquals("PROFILE EXEC A1", hostFiles.get(0));
}
@Test
public void testCMSPrintXferInstantiation() {
assertNotNull(xfer.getCMSPrintXfer());
}
@Test
public void testResendInboundDataBufferToHostDelegation() {
assertFalse(xfer.resendInboundDataBufferToHost());
}
}
@@ -0,0 +1,107 @@
package haus.nightmare.lib3270j.ft;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class CMSPrintXferTest {
@Test
public void testAsaCarriageControlTranslation() {
// ASA: ' ' = single space, '0' = double space, '-' = triple space, '1' = form feed, '+' = overstrike
String input = "1PAGE 1 HEADER\n" +
" --------------------\n" +
"0ITEM 1 DESCRIPTION\n" +
"-ITEM 2 AFTER 3 LINES\n" +
"+ITEM 2 UNDERLINED\n" +
"1PAGE 2 HEADER\n";
String expected = "\fPAGE 1 HEADER\n" +
"--------------------\n\n" +
"ITEM 1 DESCRIPTION\n\n\n" +
"ITEM 2 AFTER 3 LINES\r" +
"ITEM 2 UNDERLINED\n\f" +
"PAGE 2 HEADER";
String result = CMSPrintXfer.convertAsaCarriageControl(input);
assertEquals(expected, result);
}
@Test
public void testAsaCarriageControlBytesConversion() {
String input = " REPORT TITLE\n0SUBTITLE\n";
byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8);
byte[] converted = CMSPrintXfer.convertAsaCarriageControl(inputBytes, false);
String convertedStr = new String(converted, StandardCharsets.UTF_8);
assertEquals("REPORT TITLE\n\nSUBTITLE", convertedStr);
}
@Test
public void testMachineCarriageControlTranslation() {
// 0x09: Write and advance 1 line
// 0x11: Write and advance 2 lines
// 0x89: Write and skip to channel 1 (form feed)
byte[] input = new byte[] {
0x09, 'H', 'E', 'L', 'L', 'O', '\n',
0x11, 'W', 'O', 'R', 'L', 'D', '\n',
(byte) 0x89, 'N', 'E', 'X', 'T', ' ', 'P', 'A', 'G', 'E', '\n'
};
byte[] output = CMSPrintXfer.convertMachineCarriageControl(input);
String outputStr = new String(output, StandardCharsets.UTF_8);
assertTrue(outputStr.startsWith("HELLO\n"));
assertTrue(outputStr.contains("WORLD\n\n"));
assertTrue(outputStr.contains("NEXT PAGE\n\f"));
}
@Test
public void testParseQueryReaderOutput() {
String sampleRdrOutput =
"ORIGINID FILE CLASS RECORDS CPY HOLD DATE TIME NAME TYPE DIST\n" +
"MAINT 0042 A RDR 00000120 001 NONE 08/31 14:15:22 PROFILE EXEC SYSTEM\n" +
"OPERATOR 0099 B RDR 00000500 002 USER 08/31 14:20:00 PAYROLL DATA FINANCE\n";
List<CMSPrintXfer.SpoolFileEntry> entries = CMSPrintXfer.parseQueryReaderOutput(sampleRdrOutput);
assertEquals(2, entries.size());
CMSPrintXfer.SpoolFileEntry entry1 = entries.get(0);
assertEquals(42, entry1.getSpoolId());
assertEquals("MAINT", entry1.getOwner());
assertEquals("A", entry1.getSpoolClass());
assertEquals(120, entry1.getRecords());
assertEquals(1, entry1.getCopies());
assertEquals("NONE", entry1.getHoldStatus());
assertEquals("08/31", entry1.getDate());
assertEquals("14:15:22", entry1.getTime());
assertEquals("PROFILE", entry1.getFileName());
assertEquals("EXEC", entry1.getFileType());
assertEquals("RDR", entry1.getDeviceType());
CMSPrintXfer.SpoolFileEntry entry2 = entries.get(1);
assertEquals(99, entry2.getSpoolId());
assertEquals("OPERATOR", entry2.getOwner());
assertEquals("PAYROLL", entry2.getFileName());
assertEquals("DATA", entry2.getFileType());
}
@Test
public void testParseQueryPrinterOutput() {
String samplePrtOutput =
"ORIGINID FILE CLASS RECORDS CPY HOLD DATE TIME NAME TYPE DIST\n" +
"USER01 0105 A PRT 00000045 001 USER 08/31 15:00:00 REPORT LISTING OFFICE\n";
List<CMSPrintXfer.SpoolFileEntry> entries = CMSPrintXfer.parseQueryPrinterOutput(samplePrtOutput);
assertEquals(1, entries.size());
assertEquals(105, entries.get(0).getSpoolId());
assertEquals("USER01", entries.get(0).getOwner());
assertEquals("REPORT", entries.get(0).getFileName());
assertEquals("LISTING", entries.get(0).getFileType());
assertEquals("PRT", entries.get(0).getDeviceType());
}
}
@@ -80,4 +80,49 @@ public class FTConfigTest {
assertEquals("IND$FILE PUT TEST FILE A (ASCII CRLF RECFM V LRECL 132", config.buildCommand());
}
@Test
public void testParseOptionsComprehensive() {
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.SEND);
config.setHostFilename("MY.DATASET");
config.parseOptions("TSO ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120) SPACE(10,5) TRACKS REPLACE CODEPAGE(CP037) BUFFERSIZE(8192)");
assertEquals(FTConfig.HostType.TSO, config.getHostType());
assertEquals(FTConfig.TransferMode.ASCII, config.getTransferMode());
assertEquals(FTConfig.CrAction.REMOVE, config.getCrAction());
assertEquals(FTConfig.RecordFormat.FIXED, config.getRecfm());
assertEquals(80, config.getLrecl());
assertEquals(3120, config.getBlksize());
assertEquals(10, config.getPrimarySpace());
assertEquals(5, config.getSecondarySpace());
assertEquals(FTConfig.AllocationUnit.TRACKS, config.getUnits());
assertEquals(FTConfig.ExistAction.REPLACE, config.getExistAction());
assertEquals("CP037", config.getCodePage());
assertEquals(8192, config.getDftBufferSize());
String cmd = config.buildCommand();
assertTrue(cmd.contains("RECFM(F)"));
assertTrue(cmd.contains("LRECL(80)"));
assertTrue(cmd.contains("BLKSIZE(3120)"));
assertTrue(cmd.contains("SPACE(10,5)"));
assertTrue(cmd.contains("TRACKS"));
assertTrue(cmd.contains("REPLACE"));
}
@Test
public void testCicsCommandGeneration() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.CICS);
config.setDirection(FTConfig.Direction.SEND);
config.setHostFilename("FILE01");
config.parseOptions("CICS BINARY NOCRLF REPLACE");
assertEquals(FTConfig.HostType.CICS, config.getHostType());
assertEquals(FTConfig.TransferMode.BINARY, config.getTransferMode());
assertEquals(FTConfig.ExistAction.REPLACE, config.getExistAction());
String cmd = config.buildCommand();
assertEquals("IND$FILE PUT FILE01 (BINARY NOCRLF REPLACE", cmd);
}
}
@@ -500,4 +500,80 @@ public class FTDftTest {
assertArrayEquals(firstSent, retransmitted);
}
@Test
public void testSendDataPacketDirectWithMtuSplitting() {
ftDft.setMTUSize(512);
assertEquals(512, ftDft.getMTUSize());
// Send a 1000-byte payload, which should be split into 3 structured field chunks (each max 512 - 27 = 485 bytes)
byte[] testPayload = new byte[1000];
for (int i = 0; i < testPayload.length; i++) {
testPayload[i] = (byte) (i & 0xFF);
}
ftDft.sendDataPacket(testPayload);
assertEquals(3, inputProcessor.sentStructuredFields.size());
assertEquals(1000, ftDft.getBytesTransferred());
// Verify that each structured field is SF_TRANSFER_DATA (0xD0) and has TR_GET_REPLY (0x4605)
for (byte[] sf : inputProcessor.sentStructuredFields) {
assertEquals(AID_SF, sf[0] & 0xFF);
assertEquals(SF_TRANSFER_DATA, sf[3] & 0xFF);
assertEquals(0x46, sf[4] & 0xFF);
assertEquals(0x05, sf[5] & 0xFF);
}
}
@Test
public void testCicsHostMessageHandling() {
// DFH0500 File transfer completed successfully
String successMsg = "DFH0500 File transfer completed successfully";
byte[] msgBytes = successMsg.getBytes(StandardCharsets.ISO_8859_1);
int totalPayloadLen = msgBytes.length + 5;
ByteArrayOutputStream sfOut = new ByteArrayOutputStream();
sfOut.write(0); sfOut.write(0);
sfOut.write(0xD0);
sfOut.write((FTConstants.TR_DATA_INSERT >> 8) & 0xFF);
sfOut.write(FTConstants.TR_DATA_INSERT & 0xFF);
sfOut.write((FTConstants.TR_NOT_COMPRESSED >> 8) & 0xFF);
sfOut.write(FTConstants.TR_NOT_COMPRESSED & 0xFF);
sfOut.write(FTConstants.TR_BEGIN_DATA);
sfOut.write((totalPayloadLen >> 8) & 0xFF);
sfOut.write(totalPayloadLen & 0xFF);
sfOut.write(msgBytes, 0, msgBytes.length);
byte[] insertData = sfOut.toByteArray();
int sfLen = insertData.length;
insertData[0] = (byte) ((sfLen >> 8) & 0xFF);
insertData[1] = (byte) (sfLen & 0xFF);
// Open message stream first
byte[] openReq = new byte[] {
0x00, 0x23, (byte) 0xD0, 0x00, 0x12, 0x01, 0x06, 0x01, 0x01, 0x04,
0x03, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x11, 0x01, 0x01,
0x00, 0x50, 0x05, 0x52, 0x03, (byte) 0xF0, 0x03, 0x09,
0x46, 0x54, 0x3A, 0x4D, 0x53, 0x47, 0x20
};
ftDft.processStructuredField(openReq, 0, openReq.length);
// Deliver success message
ftDft.processStructuredField(insertData, 0, insertData.length);
assertTrue(listener.completeCalled);
assertFalse(listener.abortCalled);
}
@Test
public void testResendInboundDataBufferToHost() {
assertFalse(ftDft.resendInboundDataBufferToHost());
byte[] payload = "TEST BUFFER".getBytes(StandardCharsets.UTF_8);
ftDft.sendDataPacket(payload);
assertEquals(1, inputProcessor.sentStructuredFields.size());
assertTrue(ftDft.resendInboundDataBufferToHost());
assertEquals(2, inputProcessor.sentStructuredFields.size());
}
}
@@ -453,7 +453,7 @@ public class InputProcessorTest {
}
@Test
public void testSendAidAlwaysSendsStandard3270StreamEvenIfGraphicCursorActive() {
public void testSendAidWhenGraphicCursorActiveSendsGraphicInputStructuredField() {
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
screen.getCell(1).ec = (byte) 0xC1; // 'A'
@@ -462,6 +462,55 @@ public class InputProcessorTest {
haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane);
goca.setGraphicsCursorActive(true);
goca.setGraphicCursorPosition(100, -50);
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
protected void sendAidResponse(byte[] data) {
sent.set(data);
}
};
input.setGocaDecoder(goca);
input.sendAid(AID_ENTER);
byte[] result = sent.get();
assertNotNull(result);
// Format: AID_SF (1) + SF (56) + AID_ENTER (1) + Cursor Addr (2) + SBA (1) + Field Addr (2) + Data 'A' (1) = 64 bytes
assertEquals(64, result.length);
assertEquals((byte) AID_SF, result[0]);
assertEquals(0x00, result[1]);
assertEquals(0x34, result[2]); // SF len = 52
assertEquals(0x0F, result[3]);
assertEquals(0x0F, result[4]);
// Coordinates in SF at index 1 + 24 = 25
int gx = (result[25] << 8) | (result[26] & 0xFF);
int gy = (result[27] << 8) | (result[28] & 0xFF);
assertEquals(100, (short) gx);
assertEquals(-50, (short) gy);
// Keyboard AID trigger class constant (0x07) at index 1 + 31 = 32
assertEquals(0x07, result[32]);
assertEquals(0x07, result[34]);
assertEquals((byte) 0xFF, result[35]);
assertEquals((byte) AID_ENTER, result[36]);
// Trailing AID at index 57
assertEquals((byte) AID_ENTER, result[57]);
assertEquals((byte) ORDER_SBA, result[60]);
assertEquals((byte) 0xC1, result[63]);
}
@Test
public void testSendAidWhenGraphicCursorNotActiveSendsStandard3270Stream() {
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
screen.getCell(1).ec = (byte) 0xC1; // 'A'
screen.setCursorAddress(2);
haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane);
goca.setGraphicsCursorActive(false);
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) {
@@ -0,0 +1,384 @@
package haus.nightmare.lib3270j.nvt;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
/**
* Comprehensive Unit Tests for Phase 9: NVT Mode (VT100 / VT220 / VT320 / ANSI Emulation).
*/
public class NvtProcessorPhase9Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private NvtProcessor processor;
private ByteArrayOutputStream output;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator); // 24x80
processor = new NvtProcessor(screen, translator);
output = new ByteArrayOutputStream();
processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length));
}
private void feed(String s) {
byte[] b = s.getBytes(StandardCharsets.US_ASCII);
processor.processNVTData(b, 0, b.length);
}
private void feedUtf8(String s) {
byte[] b = s.getBytes(StandardCharsets.UTF_8);
processor.processNVTData(b, 0, b.length);
}
@Test
public void testDeviceStatusReportExtendedDSR() {
output.reset();
// DSR 5 (Status OK)
feed("\u001B[5n");
assertEquals("\u001B[0n", output.toString(StandardCharsets.US_ASCII));
output.reset();
// DSR 6 (Cursor Position Report) -> at row 10, col 20 (0-indexed: 9, 19)
feed("\u001B[10;20H");
feed("\u001B[6n");
assertEquals("\u001B[10;20R", output.toString(StandardCharsets.US_ASCII));
output.reset();
// DSR ?6 (DECXCPR Extended Cursor Position Report)
feed("\u001B[?6n");
assertEquals("\u001B[?10;20;1R", output.toString(StandardCharsets.US_ASCII));
output.reset();
// DSR ?15 (Printer status)
feed("\u001B[?15n");
assertEquals("\u001B[?13n", output.toString(StandardCharsets.US_ASCII));
output.reset();
// DSR ?26 (Keyboard dialect status)
feed("\u001B[?26n");
assertEquals("\u001B[?27;1n", output.toString(StandardCharsets.US_ASCII));
output.reset();
// DSR ?62 / ?63 (Macro/Memory Checksum)
feed("\u001B[?62n");
assertEquals("\u001B[?63;0n", output.toString(StandardCharsets.US_ASCII));
}
@Test
public void testDeviceAttributesPrimaryAndSecondaryDA() {
output.reset();
// Default Primary DA (VT100 with AVO): ESC [ c
feed("\u001B[c");
assertEquals("\u001B[?1;2c", output.toString(StandardCharsets.US_ASCII));
output.reset();
// Configure VT220 Primary DA
processor.setPrimaryDeviceAttributes("\u001B[?62;1;2;6;7;8;9c");
feed("\u001B[c");
assertEquals("\u001B[?62;1;2;6;7;8;9c", output.toString(StandardCharsets.US_ASCII));
output.reset();
feed("\u001B[0c");
assertEquals("\u001B[?62;1;2;6;7;8;9c", output.toString(StandardCharsets.US_ASCII));
output.reset();
// Secondary DA: ESC [ > c
feed("\u001B[>c");
assertEquals("\u001B[>1;10;0c", output.toString(StandardCharsets.US_ASCII));
output.reset();
// Secondary DA: ESC [ > 0 c
feed("\u001B[>0c");
assertEquals("\u001B[>1;10;0c", output.toString(StandardCharsets.US_ASCII));
output.reset();
// DECREQTPARM: ESC [ 1 x
feed("\u001B[1x");
assertEquals("\u001B[3;1;1;120;120;1;0x", output.toString(StandardCharsets.US_ASCII));
output.reset();
// DECREQTPARM: ESC [ 0 x
feed("\u001B[0x");
assertEquals("\u001B[2;1;1;120;120;1;0x", output.toString(StandardCharsets.US_ASCII));
}
@Test
public void testDecSoftTerminalResetDECSTR() {
// Set non-standard modes
processor.setCursorVisible(false);
processor.setApplicationKeypad(true);
processor.setApplicationCursorKeys(true);
processor.setOriginMode(true);
processor.setAutoWrap(false);
processor.setInsertMode(true);
// Set scroll region 3..10
feed("\u001B[3;10r");
// Soft Reset: ESC [ ! p
feed("\u001B[!p");
assertTrue(processor.isCursorVisible());
assertFalse(processor.isApplicationKeypad());
assertFalse(processor.isApplicationCursorKeys());
assertFalse(processor.isOriginMode());
assertTrue(processor.isAutoWrap());
assertFalse(processor.isInsertMode());
}
@Test
public void testOriginModeDECOM() {
// Set scroll region lines 5..15: ESC [ 5 ; 15 r
feed("\u001B[5;15r");
// Enable Origin Mode: ESC [ ? 6 h
feed("\u001B[?6h");
assertTrue(processor.isOriginMode());
// Move to line 1, col 1 in origin mode -> corresponds to row 5 (0-indexed: 4), col 0
feed("\u001B[1;1H");
assertEquals(4 * 80, screen.getCursorAddress());
// Move to line 3, col 10 in origin mode -> corresponds to row 7 (0-indexed: 6), col 9
feed("\u001B[3;10H");
assertEquals(6 * 80 + 9, screen.getCursorAddress());
// Check DSR extended CPR in origin mode: should report row 3, col 10 relative to top margin
output.reset();
feed("\u001B[?6n");
assertEquals("\u001B[?3;10;1R", output.toString(StandardCharsets.US_ASCII));
// Disable Origin Mode: ESC [ ? 6 l
feed("\u001B[?6l");
assertFalse(processor.isOriginMode());
// Move to line 1, col 1 in absolute mode -> row 0, col 0
feed("\u001B[1;1H");
assertEquals(0, screen.getCursorAddress());
}
@Test
public void testCharacterSetSCSandShifts() {
// Designate G0 as UK (ESC ( A), G1 as Line Drawing (ESC ) 0), G2 as German (ESC * K)
feed("\u001B(A\u001B)0\u001B*K");
assertEquals('A', processor.getG0Charset());
assertEquals('0', processor.getG1Charset());
assertEquals('K', processor.getG2Charset());
// In G0 (UK): '#' maps to '£'
feed("\u001B[1;1H#");
assertEquals('£', screen.getCell(0).ucs4);
// Shift-Out (0x0E -> G1 Line Drawing)
processor.processNVTData(new byte[] { 0x0E, (byte) 'q' }, 0, 2);
assertEquals('─', screen.getCell(1).ucs4);
// Shift-In (0x0F -> G0 UK)
processor.processNVTData(new byte[] { 0x0F, (byte) '#' }, 0, 2);
assertEquals('£', screen.getCell(2).ucs4);
// Single Shift SS2 (ESC N -> G2 German for next character only)
// In German: '@' -> '§', '{' -> 'ä'
feed("\u001BN@#");
assertEquals('§', screen.getCell(3).ucs4); // Single shift applied to '@'
assertEquals('£', screen.getCell(4).ucs4); // Returned to G0 UK for '#'
// Locking Shift LS2 (ESC n -> G2 German active)
feed("\u001Bn{|}~");
assertEquals('ä', screen.getCell(5).ucs4);
assertEquals('ö', screen.getCell(6).ucs4);
assertEquals('ü', screen.getCell(7).ucs4);
assertEquals('ß', screen.getCell(8).ucs4);
}
@Test
public void testOscWindowTitleAndIcon() {
AtomicReference<String> titleRef = new AtomicReference<>("");
processor.addTitleListener(titleRef::set);
// OSC 0 (BEL terminated): ESC ] 0 ; Main Terminal Window BEL
feedUtf8("\u001B]0;Main Terminal Window\u0007");
assertEquals("Main Terminal Window", processor.getTerminalTitle());
assertEquals("Main Terminal Window", titleRef.get());
// OSC 2 (ST terminated): ESC ] 2 ; IBM Host Session ESC \
feedUtf8("\u001B]2;IBM Host Session\u001B\\");
assertEquals("IBM Host Session", processor.getTerminalTitle());
assertEquals("IBM Host Session", titleRef.get());
}
@Test
public void testOsc52ClipboardReadAndWrite() {
AtomicReference<String> clipboardStorage = new AtomicReference<>("Initial Clipboard");
processor.setClipboardHandler(new NvtProcessor.ClipboardHandler() {
@Override
public String getClipboardText() {
return clipboardStorage.get();
}
@Override
public void setClipboardText(String text) {
clipboardStorage.set(text);
}
});
// OSC 52 Write: Base64 of "Hello from Host" is "SGVsbG8gZnJvbSBIb3N0"
feed("\u001B]52;c;SGVsbG8gZnJvbSBIb3N0\u0007");
assertEquals("Hello from Host", clipboardStorage.get());
// OSC 52 Read Query: ESC ] 52 ; c ; ? BEL
output.reset();
feed("\u001B]52;c;?\u0007");
String expectedB64 = java.util.Base64.getEncoder().encodeToString("Hello from Host".getBytes(StandardCharsets.UTF_8));
String response = output.toString(StandardCharsets.US_ASCII);
assertTrue(response.contains(expectedB64));
assertTrue(response.startsWith("\u001B]52;c;"));
}
@Test
public void testApplicationKeypadModeDECKPAM() {
assertFalse(processor.isApplicationKeypad());
// Enable Keypad Application Mode: ESC =
feed("\u001B=");
assertTrue(processor.isApplicationKeypad());
// Test keypad mapping
assertEquals("\u001BOp", processor.mapKeypadKey(0x60)); // NUMPAD0
assertEquals("\u001BOq", processor.mapKeypadKey(0x61)); // NUMPAD1
assertEquals("\u001BOr", processor.mapKeypadKey(0x62)); // NUMPAD2
assertEquals("\u001BOs", processor.mapKeypadKey(0x63)); // NUMPAD3
assertEquals("\u001BOt", processor.mapKeypadKey(0x64)); // NUMPAD4
assertEquals("\u001BOu", processor.mapKeypadKey(0x65)); // NUMPAD5
assertEquals("\u001BOv", processor.mapKeypadKey(0x66)); // NUMPAD6
assertEquals("\u001BOw", processor.mapKeypadKey(0x67)); // NUMPAD7
assertEquals("\u001BOx", processor.mapKeypadKey(0x68)); // NUMPAD8
assertEquals("\u001BOy", processor.mapKeypadKey(0x69)); // NUMPAD9
assertEquals("\u001BOm", processor.mapKeypadKey(0x6D)); // MINUS
assertEquals("\u001BOn", processor.mapKeypadKey(0x6E)); // DECIMAL
assertEquals("\u001BOM", processor.mapKeypadKey(0x0A)); // ENTER
byte[] bytes = processor.processApplicationKeypad(0x60);
assertNotNull(bytes);
assertEquals("\u001BOp", new String(bytes, StandardCharsets.US_ASCII));
// Enable Keypad Numeric Mode: ESC >
feed("\u001B>");
assertFalse(processor.isApplicationKeypad());
assertNull(processor.mapKeypadKey(0x60));
assertNull(processor.processApplicationKeypad(0x60));
}
@Test
public void testApplicationCursorKeysModeDECCKM() {
assertFalse(processor.isApplicationCursorKeys());
assertEquals("\u001B[A", processor.mapKey(0x26, '\0', false, false, false)); // VK_UP normal
assertEquals("\u001B[B", processor.mapKey(0x28, '\0', false, false, false)); // VK_DOWN normal
assertEquals("\u001B[C", processor.mapKey(0x27, '\0', false, false, false)); // VK_RIGHT normal
assertEquals("\u001B[D", processor.mapKey(0x25, '\0', false, false, false)); // VK_LEFT normal
// Enable Application Cursor Keys: ESC [ ? 1 h
feed("\u001B[?1h");
assertTrue(processor.isApplicationCursorKeys());
assertEquals("\u001BOA", processor.mapKey(0x26, '\0', false, false, false)); // VK_UP application
assertEquals("\u001BOB", processor.mapKey(0x28, '\0', false, false, false)); // VK_DOWN application
assertEquals("\u001BOC", processor.mapKey(0x27, '\0', false, false, false)); // VK_RIGHT application
assertEquals("\u001BOD", processor.mapKey(0x25, '\0', false, false, false)); // VK_LEFT application
// Disable Application Cursor Keys: ESC [ ? 1 l
feed("\u001B[?1l");
assertFalse(processor.isApplicationCursorKeys());
assertEquals("\u001B[A", processor.mapKey(0x26, '\0', false, false, false));
}
@Test
public void testFunctionAndEditingKeyMapping() {
// Function keys F1..F4
assertEquals("\u001BOP", processor.mapKey(0x70, '\0', false, false, false)); // F1
assertEquals("\u001BOQ", processor.mapKey(0x71, '\0', false, false, false)); // F2
assertEquals("\u001BOR", processor.mapKey(0x72, '\0', false, false, false)); // F3
assertEquals("\u001BOS", processor.mapKey(0x73, '\0', false, false, false)); // F4
// Function keys F5..F12
assertEquals("\u001B[15~", processor.mapKey(0x74, '\0', false, false, false)); // F5
assertEquals("\u001B[17~", processor.mapKey(0x75, '\0', false, false, false)); // F6
assertEquals("\u001B[24~", processor.mapKey(0x7B, '\0', false, false, false)); // F12
// Editing keys
assertEquals("\u001B[2~", processor.mapKey(0x9B, '\0', false, false, false)); // Insert
assertEquals("\u001B[3~", processor.mapKey(0x7F, '\0', false, false, false)); // Delete
assertEquals("\u001B[1~", processor.mapKey(0x24, '\0', false, false, false)); // Home
assertEquals("\u001B[4~", processor.mapKey(0x23, '\0', false, false, false)); // End
assertEquals("\u001B[5~", processor.mapKey(0x21, '\0', false, false, false)); // PageUp
assertEquals("\u001B[6~", processor.mapKey(0x22, '\0', false, false, false)); // PageDown
}
@Test
public void testInsertModeAndNewLineMode() throws IOException {
// Line Feed in standard mode: cursor moves down without returning to col 0
processor.setNewLineMode(false);
feed("\u001B[1;10H\n");
assertEquals(1 * 80 + 9, screen.getCursorAddress()); // Row 2, Col 10
// Enable New Line Mode: ESC [ 20 h
feed("\u001B[20h");
assertTrue(processor.isNewLineMode());
feed("\u001B[1;10H\n");
assertEquals(1 * 80, screen.getCursorAddress()); // Row 2, Col 0 (CR + LF)
// Insert Mode: ESC [ 4 h
feed("\u001B[1;1HAC");
feed("\u001B[1;2H"); // Move cursor between A and C
feed("\u001B[4h"); // Enable Insert Mode
assertTrue(processor.isInsertMode());
feed("B");
assertEquals('A', screen.getCell(0).ucs4);
assertEquals('B', screen.getCell(1).ucs4);
assertEquals('C', screen.getCell(2).ucs4);
// Reset Insert Mode: ESC [ 4 l
feed("\u001B[4l");
assertFalse(processor.isInsertMode());
}
@Test
public void testAutoWrapAndColumnMode() {
// Disable Auto Wrap: ESC [ ? 7 l
feed("\u001B[?7l");
assertFalse(processor.isAutoWrap());
// Move to col 79 (0-indexed: 78) on line 1, and write 5 characters
feed("\u001B[1;79H12345");
// Characters should truncate / overwrite at margin (col 79) and not wrap to next row
assertEquals('5', screen.getCell(79).ucs4);
assertEquals(79, screen.getCursorAddress());
// Enable Auto Wrap: ESC [ ? 7 h
feed("\u001B[?7h");
assertTrue(processor.isAutoWrap());
// 132-Column Mode switch: ESC [ ? 3 h (clears screen)
feed("\u001B[?3h");
assertEquals(0, screen.getCursorAddress());
assertEquals(0, screen.getCell(0).ucs4);
}
}
@@ -0,0 +1,136 @@
package haus.nightmare.lib3270j.printer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for Phase 7 Printer Definition Table (PDT) profiles, control sequence generation,
* character mappings, and GDDM host escapes in PD3270.
*/
public class PD3270PDTPhase7Test {
private PrinterConfig config;
private PD3270 pd;
@BeforeEach
public void setUp() {
config = new PrinterConfig("localhost", 23);
pd = new PD3270(config);
pd.openPrinter(null);
}
@Test
public void testBuiltinPDTPresets() {
// Plain Text
PrinterDefinitionTable textPdt = PrinterDefinitionTable.createPlainTextPDT();
assertEquals("PLAIN_TEXT", textPdt.getName());
assertTrue(textPdt.hasControlCode(PrinterDefinitionTable.CMD_PAGE_FEED));
// HP PCL 5
PrinterDefinitionTable pcl5 = PrinterDefinitionTable.createPcl5PDT();
assertEquals("PCL_5", pcl5.getName());
assertTrue(pcl5.hasControlCode(PrinterDefinitionTable.CMD_START_BOLD));
assertArrayEquals("\033(s3B".getBytes(), pcl5.getControlCode(PrinterDefinitionTable.CMD_START_BOLD));
// Epson ESC/P
PrinterDefinitionTable escp = PrinterDefinitionTable.createEpsonEscPPDT();
assertEquals("EPSON_ESC_P", escp.getName());
assertTrue(escp.hasControlCode(PrinterDefinitionTable.CMD_START_BOLD));
assertArrayEquals("\033E".getBytes(), escp.getControlCode(PrinterDefinitionTable.CMD_START_BOLD));
// PostScript
PrinterDefinitionTable ps = PrinterDefinitionTable.createPostScriptPDT();
assertEquals("POSTSCRIPT", ps.getName());
assertTrue(ps.hasControlCode(PrinterDefinitionTable.CMD_START_JOB));
}
@Test
public void testPcl5ControlCodeGeneration() {
pd.setPDT(PrinterDefinitionTable.createPcl5PDT());
pd.startJob();
pd.setBold(true);
pd.print("BOLD TEXT");
pd.setBold(false);
pd.setUnderline(true);
pd.print("UNDERLINED");
pd.setUnderline(false);
pd.setItalic(true);
pd.print("ITALIC");
pd.setItalic(false);
pd.setCPI(12);
pd.setLPI(8);
pd.formFeed();
pd.endJob();
String captured = pd.getCapturedText();
assertTrue(captured.contains("\033E")); // Reset / start job
assertTrue(captured.contains("\033(s3B")); // Bold on
assertTrue(captured.contains("\033(s0B")); // Bold off
assertTrue(captured.contains("\033&d0D")); // Underline on
assertTrue(captured.contains("\033(s1S")); // Italic on
assertTrue(captured.contains("\033&k2S")); // 12 CPI
assertTrue(captured.contains("\033&l8D")); // 8 LPI
assertTrue(captured.contains("\f")); // Page feed
}
@Test
public void testEpsonEscPControlCodeGeneration() {
pd.setPDT(PrinterDefinitionTable.createEpsonEscPPDT());
pd.startJob();
pd.setDoubleStrike(true);
pd.print("DOUBLE STRIKE");
pd.setDoubleStrike(false);
pd.setDoubleWidth(true);
pd.print("DOUBLE WIDTH");
pd.setDoubleWidth(false);
pd.setSuperscript(true);
pd.print("SUPER");
pd.setSuperscript(false);
pd.setSubscript(true);
pd.print("SUB");
pd.setSubscript(false);
pd.setCPI(10);
pd.setLPI(6);
pd.endJob();
String captured = pd.getCapturedText();
assertTrue(captured.contains("\033@")); // Init
assertTrue(captured.contains("\033G")); // Double strike on
assertTrue(captured.contains("\033H")); // Double strike off
assertTrue(captured.contains("\033W1")); // Double width on
assertTrue(captured.contains("\033W0")); // Double width off
assertTrue(captured.contains("\033S0")); // Superscript on
assertTrue(captured.contains("\033S1")); // Subscript on
assertTrue(captured.contains("\033P")); // 10 CPI
assertTrue(captured.contains("\0332")); // 6 LPI
}
@Test
public void testCustomPDTWithCharacterOverrides() {
PrinterDefinitionTable customPdt = new PrinterDefinitionTable("CUSTOM", "Custom Test Table");
customPdt.setControlCode("TEST_CMD", "[CMD]");
customPdt.setCharOverride('@', "[AT]".getBytes());
pd.setPDT(customPdt);
pd.writeControlCode("TEST_CMD");
pd.print('@');
pd.print("user@ibm.com");
String captured = pd.getCapturedText();
assertTrue(captured.contains("[CMD]"));
assertTrue(captured.contains("[AT]"));
}
@Test
public void testGddmHostPrintEscapePassthrough() {
byte[] gddmEscape = new byte[]{0x1B, '&', 'a', '1', '0', '0', 'V'};
pd.processGddmEscape(gddmEscape, 0, gddmEscape.length);
byte[] captured = pd.getCapturedBytes();
assertTrue(captured.length >= gddmEscape.length);
}
}
@@ -0,0 +1,100 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for Phase 7 PrintPS3270 EOJ completion, auto-flush timer, and 3270 printer data stream formatting.
*/
public class PrintPS3270Phase7Test {
private PrinterConfig config;
private PD3270 pd;
private EbcdicTranslator translator;
private PrintPS3270 printPs;
@BeforeEach
public void setUp() {
config = new PrinterConfig("localhost", 23);
config.setAutoFlushTimeoutMs(150); // Fast timeout for testing
pd = new PD3270(config);
translator = new EbcdicTranslator("037");
printPs = new PrintPS3270(config, pd, translator);
pd.openPrinter(null);
}
@Test
public void testProcessPrintCompleteEOJ() {
config.setFormFeedAtEoj(true);
config.setAutoFlushOnEoj(true);
byte[] ebcText = translator.stringToEbcdic("LU3 TEST PAGE");
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(DS3270Constants.CMD_WRITE);
stream.write(0x00); // WCC (Start Print = false)
stream.write(PrinterConstants.ORDER_SBA);
stream.write(0x40); // Row 0 Col 0
stream.write(0x40);
stream.write(ebcText, 0, ebcText.length);
printPs.process3270PrintDS(stream.toByteArray(), 0, stream.size());
// Process EOJ completion
printPs.processPrintComplete();
String captured = pd.getCapturedText();
assertTrue(captured.contains("LU3 TEST PAGE"));
assertEquals(1, pd.getPageCount());
}
@Test
public void testAutoFlushTimerTriggersOnIdle() throws InterruptedException {
byte[] ebcText = translator.stringToEbcdic("AUTO FLUSH DATA");
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(DS3270Constants.CMD_WRITE);
stream.write(0x00); // WCC with Start Print = 0
stream.write(PrinterConstants.ORDER_SBA);
stream.write(0x40);
stream.write(0x40);
stream.write(ebcText, 0, ebcText.length);
printPs.process3270PrintDS(stream.toByteArray(), 0, stream.size());
// Wait for auto-flush timer (150ms) to trigger
Thread.sleep(300);
String captured = pd.getCapturedText();
assertTrue(captured.contains("AUTO FLUSH DATA"));
}
@Test
public void testStartPrintBitImmediateFlush() {
byte[] ebcText = translator.stringToEbcdic("IMMEDIATE PRINT");
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(DS3270Constants.CMD_WRITE);
stream.write(PrinterConstants.WCC_START_PRINT_BIT); // WCC with Start Print = 1
stream.write(PrinterConstants.ORDER_SBA);
stream.write(0x40);
stream.write(0x40);
stream.write(ebcText, 0, ebcText.length);
printPs.process3270PrintDS(stream.toByteArray(), 0, stream.size());
String captured = pd.getCapturedText();
assertTrue(captured.contains("IMMEDIATE PRINT"));
}
@Test
public void testCancelAutoFlush() {
printPs.scheduleAutoFlush();
printPs.cancelAutoFlush();
// Cancel should succeed without exception
assertNotNull(printPs);
}
}
@@ -0,0 +1,238 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import static org.junit.jupiter.api.Assertions.*;
/**
* Comprehensive tests for Phase 7 SCS (SNA Character String) 28-command protocol engine,
* Enhanced Highlights, Vertical Channel Select (VCS), Graphic Error Action (GEA),
* Index Return (IRS), and Transparent data (TRN).
*/
public class PrintSCS3270Phase7Test {
private PrinterConfig config;
private PD3270 pd;
private EbcdicTranslator translator;
private PrintSCS3270 scs;
@BeforeEach
public void setUp() {
config = new PrinterConfig("localhost", 23);
pd = new PD3270(config);
translator = new EbcdicTranslator("037");
scs = new PrintSCS3270(config, pd, translator);
pd.openPrinter(null); // In-memory capture
}
@Test
public void testAll28SCSCommandsDispatch() {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// 1. NUL (0x00) & NOP (0x03)
stream.write(PrinterConstants.SCS_NUL);
stream.write(PrinterConstants.SCS_NOP);
// 2. Text "A" (0xC1 in Cp037)
stream.write(0xC1);
// 3. BS (0x16) & NBS (0x36)
stream.write(PrinterConstants.SCS_BS);
stream.write(PrinterConstants.SCS_NBS);
// 4. SP (0x40) & RSP (0x41)
stream.write(PrinterConstants.SCS_SP);
stream.write(PrinterConstants.SCS_RSP);
// 5. HT (0x05)
stream.write(PrinterConstants.SCS_HT);
// 6. Text "B" (0xC2)
stream.write(0xC2);
// 7. CR (0x0D) & RCR (0x07)
stream.write(PrinterConstants.SCS_CR);
stream.write(PrinterConstants.SCS_RCR);
// 8. LF (0x25) & NL (0x15) & RNL (0x06)
stream.write(PrinterConstants.SCS_LF);
stream.write(PrinterConstants.SCS_NL);
stream.write(PrinterConstants.SCS_RNLS);
// 9. VT (0x0B)
stream.write(PrinterConstants.SCS_VT);
// 10. SO (0x0E) & SI (0x0F)
stream.write(PrinterConstants.SCS_SO);
stream.write(PrinterConstants.SCS_SI);
// 11. INP (0x24) & ENP (0x14)
stream.write(PrinterConstants.SCS_INP);
stream.write(0xC3); // Should be ignored when presentation inhibited
stream.write(PrinterConstants.SCS_ENP);
stream.write(0xC3); // 'C'
// 12. BEL (0x2F) & POC (0x17)
stream.write(PrinterConstants.SCS_BEL);
stream.write(PrinterConstants.SCS_POC);
// 13. IRS (0x33)
stream.write(PrinterConstants.SCS_IRS);
// 14. SUB (0x3F)
stream.write(PrinterConstants.SCS_SUB);
// 15. FF (0x0C)
stream.write(PrinterConstants.SCS_FF);
byte[] payload = stream.toByteArray();
scs.processRecord(payload);
String captured = pd.getCapturedText();
assertNotNull(captured);
assertTrue(captured.contains("C"));
assertTrue(captured.contains("?"));
assertTrue(pd.getPageCount() >= 1);
}
@Test
public void testProcessTRNRawBytePassthrough() {
// TRN order: 0x35 <len> <rawBytes>
byte[] rawPrinterCodes = new byte[]{0x1B, 0x45, 'R', 'A', 'W', 0x1B, 0x46};
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(PrinterConstants.SCS_TRN);
stream.write(rawPrinterCodes.length);
stream.write(rawPrinterCodes, 0, rawPrinterCodes.length);
scs.processHostData(stream.toByteArray(), 0, stream.size());
byte[] captured = pd.getCapturedBytes();
assertTrue(captured.length >= rawPrinterCodes.length);
// Also test direct processTRN method
byte[] directBytes = new byte[]{0x1B, 0x2A, 0x62, 0x30, 0x57};
scs.processTRN(directBytes);
assertTrue(pd.getByteCount() >= rawPrinterCodes.length + directBytes.length);
}
@Test
public void testProcessVCSWithChannelTape() {
// Setup channel tape: Channel 2 = line 15, Channel 3 = line 30
scs.setChannelLine(2, 15);
scs.setChannelLine(3, 30);
assertEquals(15, scs.getChannelLine(2));
assertEquals(30, scs.getChannelLine(3));
// Start at line 1
assertEquals(1, scs.getCurrentRow());
// Process VCS for Channel 2
scs.processVCS(2);
assertEquals(15, scs.getCurrentRow());
// Process VCS for Channel 3
scs.processVCS(3);
assertEquals(30, scs.getCurrentRow());
// Process VCS for Channel 1 (line 1, which is < 30, so forms feed to next page)
int initialPages = pd.getPageCount();
scs.processVCS(1);
assertEquals(1, scs.getCurrentRow());
assertEquals(initialPages + 1, pd.getPageCount());
}
@Test
public void testProcessGEAGraphicErrorActions() {
// 1. GEA Default: replacement '?'
config.setGraphicErrorAction(PrinterConstants.GEA_NO_SUBSTITUTION);
scs.processSUB();
scs.flushLineBuffer();
assertTrue(pd.getCapturedText().contains("?"));
// 2. GEA Substitute specified character ('#')
pd.resetCapture();
scs.processGEA(PrinterConstants.GEA_SUBSTITUTE_SPECIFIED, '#');
scs.processSUB();
scs.flushLineBuffer();
assertTrue(pd.getCapturedText().contains("#"));
// 3. GEA 0x2B 0xC8 order
pd.resetCapture();
byte[] geaOrder = new byte[]{
(byte) PrinterConstants.SCS_PREFIX_2B,
(byte) PrinterConstants.SCS_GEA,
0x02,
PrinterConstants.GEA_SUBSTITUTE_SPECIFIED,
(byte) '*'
};
scs.processHostData(geaOrder, 0, geaOrder.length);
scs.processSUB();
scs.flushLineBuffer();
assertTrue(pd.getCapturedText().contains("*"));
}
@Test
public void testEnhancedHighlightAttributes() {
// Set PDT to PCL5 so we can verify escape sequences generated
PrinterDefinitionTable pcl5 = PrinterDefinitionTable.createPcl5PDT();
pd.setPDT(pcl5);
// Test Emphasized (Bold)
scs.setEnhancedHighlight(PrinterConstants.SEAC_EMPHASIZED);
assertEquals(PrinterConstants.SEAC_EMPHASIZED, scs.getActiveHighlight());
String text = pd.getCapturedText();
assertTrue(text.contains("\033(s3B")); // PCL Bold on
// Test Italic
pd.resetCapture();
scs.setEnhancedHighlight(PrinterConstants.SEAC_ITALIC);
assertEquals(PrinterConstants.SEAC_ITALIC, scs.getActiveHighlight());
text = pd.getCapturedText();
assertTrue(text.contains("\033(s1S")); // PCL Italic on
// Test Underline
pd.resetCapture();
scs.setEnhancedHighlight(PrinterConstants.SEAC_UNDERLINE);
assertEquals(PrinterConstants.SEAC_UNDERLINE, scs.getActiveHighlight());
text = pd.getCapturedText();
assertTrue(text.contains("\033&d0D")); // PCL Underline on
// Test Double-Width
pd.resetCapture();
scs.startDoubleWidthCharacters();
assertTrue(scs.isDoubleWidth());
text = pd.getCapturedText();
assertTrue(text.contains("\033&k1W"));
scs.endDoubleWidthCharacters();
assertFalse(scs.isDoubleWidth());
}
@Test
public void testExplicitSCSNamedMethods() {
scs.processNL();
scs.processCR();
scs.processLF();
scs.processFF();
scs.processBS();
scs.processNBS();
scs.processHT();
scs.processVT();
scs.processRNL();
scs.processRCR();
scs.processENP();
scs.processINP();
scs.processBEL();
scs.processIRS();
scs.processSO();
scs.processSI();
scs.processSCS(1);
scs.processPOC(new byte[]{0x01});
assertTrue(pd.getPageCount() >= 1);
}
}
@@ -0,0 +1,159 @@
package haus.nightmare.lib3270j.printer;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
/**
* End-to-end tests for Phase 7 Telnet3270EP printer protocol engine, LU1 and LU3 sessions,
* response headers, EOJ handling, listeners, and Telnet3270EPClient integration.
*/
public class Telnet3270EPPhase7FullTest {
private PrinterConfig config;
private PD3270 pd;
private EbcdicTranslator translator;
private Telnet3270EP ep;
private Telnet3270EPClient client;
@BeforeEach
public void setUp() {
config = new PrinterConfig("localhost", 23, "PRT01");
config.setAssociatedDisplayLuName("DSP01");
config.setFormFeedAtEoj(true);
config.setAutoFlushOnEoj(true);
pd = new PD3270(config);
translator = new EbcdicTranslator("037");
ep = new Telnet3270EP(config, pd, translator);
client = new Telnet3270EPClient(config);
pd.openPrinter(null);
}
@Test
public void testLU1SCSPrintSessionFlow() {
// 1. Process BIND for LU Type 1 (SCS)
ep.process_bind(PrinterConstants.LU_TYPE_1_SCS);
assertEquals(PrinterConstants.LU_TYPE_1_SCS, ep.getActiveLuType());
assertEquals(PrinterConstants.STATUS_PRINTER_READY, ep.getStatusCode());
// 2. Feed SCS print records
byte[] hello = translator.stringToEbcdic("REPORT TITLE");
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(hello, 0, hello.length);
stream.write(PrinterConstants.SCS_NL);
byte[] data = translator.stringToEbcdic("LINE ITEM 1");
stream.write(data, 0, data.length);
stream.write(PrinterConstants.SCS_NL);
byte[] payload = stream.toByteArray();
ep.getSCS().processHostData(payload, 0, payload.length);
// 3. EOJ
ep.sendEOJ(true);
String text = pd.getCapturedText();
assertTrue(text.contains("REPORT TITLE"));
assertTrue(text.contains("LINE ITEM 1"));
assertEquals(1, pd.getPageCount());
assertEquals(PrinterConstants.STATUS_JOB_COMPLETE, ep.getStatusCode());
}
@Test
public void testLU33270PrintSessionFlow() {
// 1. Process BIND for LU Type 3 (3270 Data Stream)
ep.process_bind(PrinterConstants.LU_TYPE_3_DS);
assertEquals(PrinterConstants.LU_TYPE_3_DS, ep.getActiveLuType());
// 2. Feed 3270 print DS
byte[] hello = translator.stringToEbcdic("LU3 REPORT CONTENT");
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(0xF5); // ERASE WRITE
stream.write(0x00); // WCC
stream.write(PrinterConstants.ORDER_SBA);
stream.write(0x40);
stream.write(0x40);
stream.write(hello, 0, hello.length);
byte[] payload = stream.toByteArray();
ep.getPrintPS().process3270PrintDS(payload, 0, payload.length);
// 3. EOJ
ep.sendEOJ(true);
String text = pd.getCapturedText();
assertTrue(text.contains("LU3 REPORT CONTENT"));
assertEquals(1, pd.getPageCount());
}
@Test
public void testClientFacadePDTAndChannelConfiguration() {
PrinterDefinitionTable pcl5 = PrinterDefinitionTable.createPcl5PDT();
client.setPDT(pcl5);
assertEquals("PCL_5", client.getPDT().getName());
client.setChannelLine(4, 25);
assertEquals(25, client.getChannelLine(4));
byte[] text = translator.stringToEbcdic("CLIENT CONVENIENCE TEST");
client.printDirectBytes(text, 0, text.length);
client.flush();
String captured = client.getPD().getCapturedText();
assertTrue(captured.contains("CLIENT CONVENIENCE TEST"));
}
@Test
public void testPrintSessionEventListeners() {
AtomicBoolean jobStarted = new AtomicBoolean(false);
AtomicBoolean jobComplete = new AtomicBoolean(false);
AtomicInteger pagesReceived = new AtomicInteger(0);
PrintSessionListener listener = new PrintSessionListener() {
@Override
public void onPrintJobStarted(PrintSessionEvent event) {
jobStarted.set(true);
}
@Override
public void onPrintJobData(PrintSessionEvent event) {
}
@Override
public void onPrintJobPageComplete(PrintSessionEvent event) {
pagesReceived.incrementAndGet();
}
@Override
public void onPrintJobComplete(PrintSessionEvent event) {
jobComplete.set(true);
}
@Override
public void onPrinterStatusChanged(PrintSessionEvent event) {
}
@Override
public void onPrinterError(PrintSessionEvent event) {
}
};
ep.addPrintListener(listener);
ep.firePrintJobStarted("JOB01");
ep.firePrintJobPageComplete(1);
ep.sendEOJ(true);
assertTrue(jobStarted.get());
assertTrue(jobComplete.get());
assertEquals(1, pagesReceived.get());
ep.removePrintListener(listener);
}
}