Clear cruft
This commit is contained in:
+371
@@ -0,0 +1,371 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.ecl.ECLConnection;
|
||||
import haus.nightmare.lib3270j.ecl.ECLConstants;
|
||||
import haus.nightmare.lib3270j.ecl.ECLOIA;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPSEvent;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPSListener;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.protocol.DS3270Constants;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
import haus.nightmare.lib3270j.tn3270.DS3270;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Phase 10 Test Suite: DataStream Protocol Hardening & AUTO_SYS_UNLOCK Parity.
|
||||
*
|
||||
* Verifies:
|
||||
* 10.1 WCC Keyboard Restore Logic (intermediate writes without restore bit keep keyboard locked).
|
||||
* 10.2 AUTO_SYS_UNLOCK parity across ConnectionConfig, ECLSession, ECLConnection, DS3270, and TelnetFSM.
|
||||
* 10.3 Contention Resolution negotiation, SDI/KRI tracking, and ECLPSEvent.EVENT_KEY_UNLOCKED dispatch.
|
||||
*/
|
||||
public class Phase10ProtocolHardeningTest {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor inputProcessor;
|
||||
private DataStreamProcessor processor;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
inputProcessor = new InputProcessor(screen, translator, null);
|
||||
processor = new DataStreamProcessor(screen, translator);
|
||||
processor.setInputProcessor(inputProcessor);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 10.1 WCC Keyboard Restore Logic
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("10.1: Write without WCC restore bit keeps keyboard locked")
|
||||
public void testWriteWithoutWccRestoreKeepsKeyboardLocked() {
|
||||
// Initially lock the keyboard
|
||||
inputProcessor.setKeyboardLocked(true);
|
||||
assertTrue(inputProcessor.isKeyboardLocked());
|
||||
|
||||
// Construct 3270 Write Command (0x01) with WCC = 0xC0 (Reset MDT only, no keyboard restore 0x02)
|
||||
byte[] record = new byte[] {
|
||||
(byte) DS3270Constants.CMD_WRITE,
|
||||
(byte) 0xC0, // WCC: bit 6 set (reset MDT), bit 1 (restore) NOT set
|
||||
(byte) 0x11, 0x40, 0x40, // SBA to 0
|
||||
(byte) 0xC1, (byte) 0xC2 // 'A', 'B'
|
||||
};
|
||||
|
||||
processor.processRecord(record, 0, record.length, false);
|
||||
|
||||
// Keyboard MUST remain locked because WCC restore bit 0x02 was not set
|
||||
assertTrue(inputProcessor.isKeyboardLocked(),
|
||||
"Keyboard must remain locked when WCC does not have restore bit (0x02) set");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.1: Write with WCC restore bit unlocks keyboard")
|
||||
public void testWriteWithWccRestoreUnlocksKeyboard() {
|
||||
// Initially lock the keyboard
|
||||
inputProcessor.setKeyboardLocked(true);
|
||||
assertTrue(inputProcessor.isKeyboardLocked());
|
||||
|
||||
// Construct 3270 Write Command (0x01) with WCC = 0xC2 (Reset MDT + Keyboard Restore bit 0x02)
|
||||
byte[] record = new byte[] {
|
||||
(byte) DS3270Constants.CMD_WRITE,
|
||||
(byte) 0xC2, // WCC: restore bit (0x02) set
|
||||
(byte) 0x11, 0x40, 0x40, // SBA to 0
|
||||
(byte) 0xC1, (byte) 0xC2 // 'A', 'B'
|
||||
};
|
||||
|
||||
processor.processRecord(record, 0, record.length, false);
|
||||
|
||||
// Keyboard MUST now be unlocked
|
||||
assertFalse(inputProcessor.isKeyboardLocked(),
|
||||
"Keyboard must be unlocked when WCC has restore bit (0x02) set");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.1: Intermediate writes without restore bit maintain keyboard lock across sequence")
|
||||
public void testIntermediateWritesKeepKeyboardLockedUntilFinalRestore() {
|
||||
inputProcessor.setKeyboardLocked(true);
|
||||
assertTrue(inputProcessor.isKeyboardLocked());
|
||||
|
||||
// First intermediate write: WCC = 0x00 (no restore)
|
||||
byte[] write1 = new byte[] {
|
||||
(byte) DS3270Constants.CMD_WRITE,
|
||||
(byte) 0x00,
|
||||
(byte) 0x11, 0x40, 0x40,
|
||||
(byte) 0xC1
|
||||
};
|
||||
processor.processRecord(write1, 0, write1.length, false);
|
||||
assertTrue(inputProcessor.isKeyboardLocked(), "Write 1 without restore bit must leave keyboard locked");
|
||||
|
||||
// Second intermediate write: Erase / Write with WCC = 0x40 (Reset, no restore)
|
||||
byte[] write2 = new byte[] {
|
||||
(byte) DS3270Constants.CMD_ERASE_WRITE,
|
||||
(byte) 0x40,
|
||||
(byte) 0x11, 0x40, 0x50,
|
||||
(byte) 0xC2
|
||||
};
|
||||
processor.processRecord(write2, 0, write2.length, false);
|
||||
assertTrue(inputProcessor.isKeyboardLocked(), "Write 2 without restore bit must leave keyboard locked");
|
||||
|
||||
// Final write: WCC = 0x42 (Reset + Restore)
|
||||
byte[] write3 = new byte[] {
|
||||
(byte) DS3270Constants.CMD_WRITE,
|
||||
(byte) 0x42,
|
||||
(byte) 0x11, 0x40, 0x60,
|
||||
(byte) 0xC3
|
||||
};
|
||||
processor.processRecord(write3, 0, write3.length, false);
|
||||
assertFalse(inputProcessor.isKeyboardLocked(), "Final write with restore bit must unlock keyboard");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 10.2 AUTO_SYS_UNLOCK Parity
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("10.2: autoSysUnlock configuration property propagation across ECL components")
|
||||
public void testAutoSysUnlockConfigurationPropagation() {
|
||||
// Default ConnectionConfig should have autoSysUnlock == true
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
assertTrue(config.isAutoSysUnlock(), "Default autoSysUnlock in ConnectionConfig must be true");
|
||||
|
||||
config.setAutoSysUnlock(false);
|
||||
assertFalse(config.isAutoSysUnlock());
|
||||
|
||||
// Telnet3270Client propagation
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
assertFalse(client.isAutoSysUnlock());
|
||||
|
||||
// ECLSession propagation
|
||||
ECLSession session = new ECLSession(client);
|
||||
assertFalse(session.isAutoSysUnlock());
|
||||
assertFalse(session.getConnection().isAutoSysUnlock());
|
||||
|
||||
// Modify via ECLSession
|
||||
session.setAutoSysUnlock(true);
|
||||
assertTrue(session.isAutoSysUnlock());
|
||||
assertTrue(client.isAutoSysUnlock());
|
||||
assertTrue(config.isAutoSysUnlock());
|
||||
|
||||
// Test ECLSession Properties parsing
|
||||
Properties props = new Properties();
|
||||
props.setProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK, "false");
|
||||
props.setProperty(ECLSession.SESSION_HOST, "mainframe.org");
|
||||
ECLSession propsSession = new ECLSession(props);
|
||||
assertFalse(propsSession.isAutoSysUnlock());
|
||||
assertFalse(propsSession.getConnection().isAutoSysUnlock());
|
||||
|
||||
// ECLConnection standalone properties
|
||||
ECLConnection standaloneConn = new ECLConnection(props);
|
||||
assertFalse(standaloneConn.isAutoSysUnlock());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.2: DS3270 endOfRecord unlocks keyboard and clears OIA when autoSysUnlock is true and in X SYSTEM")
|
||||
public void testDs3270AutoSysUnlockOnEor() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
config.setAutoSysUnlock(true);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLPS ps = session.GetPS();
|
||||
ECLOIA oia = session.GetOIA();
|
||||
|
||||
DS3270 ds = new DS3270(session, ps);
|
||||
assertTrue(ds.isAutoSysUnlock());
|
||||
|
||||
// Set connected state so OIA does not treat it as communication check
|
||||
client.getTelnetFSM().setConnectionState(haus.nightmare.lib3270j.ConnectionState.CONNECTED_3270);
|
||||
|
||||
// Put session in X SYSTEM lock
|
||||
ps.lockKeyboard(8); // reason 8: SYSLOCK
|
||||
assertTrue(ps.islocked_SYSLOCK());
|
||||
assertTrue(client.getInputProcessor().isKeyboardLocked());
|
||||
assertTrue(oia.isXSystem());
|
||||
|
||||
// Trigger endOfRecord() with autoSysUnlock=true
|
||||
ds.endOfRecord();
|
||||
|
||||
// Keyboard must be auto-unlocked and OIA cleared
|
||||
assertFalse(client.getInputProcessor().isKeyboardLocked(),
|
||||
"Keyboard should be unlocked on EOR when autoSysUnlock is true");
|
||||
assertFalse(oia.isXSystem(),
|
||||
"OIA X SYSTEM lock should be cleared on EOR when autoSysUnlock is true");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.2: DS3270 endOfRecord leaves keyboard locked when autoSysUnlock is false and in X SYSTEM")
|
||||
public void testDs3270AutoSysUnlockDisabledKeepsSystemLock() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
config.setAutoSysUnlock(false);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLPS ps = session.GetPS();
|
||||
|
||||
DS3270 ds = new DS3270(session, ps);
|
||||
assertFalse(ds.isAutoSysUnlock());
|
||||
|
||||
// Put session in X SYSTEM lock
|
||||
ps.lockKeyboard(8);
|
||||
assertTrue(ps.islocked_SYSLOCK());
|
||||
assertTrue(client.getInputProcessor().isKeyboardLocked());
|
||||
|
||||
// Trigger endOfRecord() with autoSysUnlock=false
|
||||
ds.endOfRecord();
|
||||
|
||||
// Keyboard MUST remain locked
|
||||
assertTrue(client.getInputProcessor().isKeyboardLocked(),
|
||||
"Keyboard must remain locked on EOR when autoSysUnlock is false");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 10.3 Contention Resolution & Pre-Data State Transitions
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("10.3: Contention Resolution negotiation flag synchronization with ECLConnection")
|
||||
public void testContentionResolutionNegotiationSync() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLConnection conn = session.getConnection();
|
||||
TelnetFSM fsm = client.getTelnetFSM();
|
||||
|
||||
assertNotNull(fsm);
|
||||
assertTrue(fsm.isNegotiateContentionResolution(),
|
||||
"Default negotiateContentionResolution should be true");
|
||||
|
||||
// Test setter on FSM and reflection in ECLConnection
|
||||
fsm.setContentionResolutionNegotiated(true);
|
||||
|
||||
assertTrue(fsm.isContentionResolutionNegotiated(),
|
||||
"Contention resolution should be negotiated in FSM");
|
||||
assertTrue(conn.getContentionResolution(),
|
||||
"ECLConnection should reflect negotiated contention resolution");
|
||||
assertTrue(session.getContentionResolution(),
|
||||
"ECLSession should reflect negotiated contention resolution");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.3: Contention Resolution SDI flag restores keyboard and fires EVENT_KEY_UNLOCKED on EOR")
|
||||
public void testContentionResolutionWithSdiRestoresKeyboardAndFiresEvent() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLPS ps = session.GetPS();
|
||||
DS3270 ds = new DS3270(session, ps);
|
||||
|
||||
// Enable Contention Resolution on session
|
||||
session.setContentionResolution(true);
|
||||
assertTrue(session.getContentionResolution());
|
||||
|
||||
// Lock keyboard
|
||||
client.getInputProcessor().setKeyboardLocked(true);
|
||||
assertTrue(client.getInputProcessor().isKeyboardLocked());
|
||||
|
||||
// Register listener for EVENT_KEY_UNLOCKED
|
||||
AtomicInteger eventTypeReceived = new AtomicInteger(-1);
|
||||
AtomicBoolean unlockedEventFired = new AtomicBoolean(false);
|
||||
ps.RegisterPSEvent(new ECLPSListener() {
|
||||
@Override
|
||||
public void PSNotifyEvent(ECLPSEvent event) {
|
||||
eventTypeReceived.set(event.getEventType());
|
||||
unlockedEventFired.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
// Inbound TN3270E header with SDI bit set:
|
||||
// s2 request flag has bit 0 (0x01) set -> Send Data Indicator (SDI)
|
||||
short s1 = 0;
|
||||
short s2 = DS3270.request_bit_SDI; // 1
|
||||
short s3 = 0;
|
||||
ds.receiveHeaderData(s1, s2, s3, 0);
|
||||
|
||||
assertTrue(ds.isSdi_flag(), "SDI flag should be tracked from TN3270E header");
|
||||
|
||||
// Trigger endOfRecord()
|
||||
ds.endOfRecord();
|
||||
|
||||
// Keyboard should be unlocked because SDI was received
|
||||
assertFalse(client.getInputProcessor().isKeyboardLocked(),
|
||||
"Keyboard should be restored on EOR when CR is active and SDI is set");
|
||||
assertFalse(ds.isSdi_flag(), "SDI flag should be reset after EOR");
|
||||
assertTrue(unlockedEventFired.get(), "ECLPSEvent must be dispatched on keyboard unlock");
|
||||
assertEquals(ECLPSEvent.EVENT_KEY_UNLOCKED, eventTypeReceived.get(),
|
||||
"Dispatched event must match EVENT_KEY_UNLOCKED");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.3: Contention Resolution WITHOUT SDI flag keeps keyboard locked on EOR")
|
||||
public void testContentionResolutionWithoutSdiKeepsKeyboardLocked() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLPS ps = session.GetPS();
|
||||
DS3270 ds = new DS3270(session, ps);
|
||||
|
||||
session.setContentionResolution(true);
|
||||
assertTrue(session.getContentionResolution());
|
||||
|
||||
// Initially lock keyboard
|
||||
client.getInputProcessor().setKeyboardLocked(true);
|
||||
|
||||
// Inbound TN3270E header WITHOUT SDI flag (s2 = 0)
|
||||
ds.receiveHeaderData((short) 0, (short) 0, (short) 0, 0);
|
||||
assertFalse(ds.isSdi_flag());
|
||||
|
||||
// Trigger EOR
|
||||
ds.endOfRecord();
|
||||
|
||||
// Keyboard MUST stay locked because host did not grant Send Data Indicator (turn)
|
||||
assertTrue(client.getInputProcessor().isKeyboardLocked(),
|
||||
"Keyboard must remain locked on EOR when Contention Resolution is active and SDI is not set");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.3: Contention Resolution KRI flag resets system lock pending on EOR")
|
||||
public void testContentionResolutionKriResetsSystemLock() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLPS ps = session.GetPS();
|
||||
DS3270 ds = new DS3270(session, ps);
|
||||
|
||||
session.setContentionResolution(true);
|
||||
|
||||
// Put in system lock
|
||||
ps.lockKeyboard(8);
|
||||
assertTrue(ps.islocked_SYSLOCK());
|
||||
|
||||
// Receive header with KRI flag (bit 1 = 0x02) and SDI flag (bit 0 = 0x01)
|
||||
short s2 = (short) (DS3270.request_bit_SDI | DS3270.request_bit_KRI);
|
||||
ds.receiveHeaderData((short) 0, s2, (short) 0, 0);
|
||||
|
||||
assertTrue(ds.isSdi_flag());
|
||||
assertTrue(ds.isKri_flag());
|
||||
|
||||
ds.endOfRecord();
|
||||
|
||||
// Keyboard restored and system lock reset
|
||||
assertFalse(client.getInputProcessor().isKeyboardLocked());
|
||||
assertFalse(ps.islocked_SYSLOCK());
|
||||
assertFalse(ds.isKri_flag());
|
||||
}
|
||||
}
|
||||
+397
@@ -0,0 +1,397 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.ecl.ECLConnection;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.listener.ConnectionListener;
|
||||
import haus.nightmare.lib3270j.protocol.TelnetConstants;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetConnection;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Phase 11 Test Suite: Enterprise Connection Resilience & "Dirty" Connection Handling.
|
||||
*
|
||||
* Verifies:
|
||||
* 11.1 Application-Layer Telnet Keep-Alive / Heartbeat Engine (NOP and TIMING-MARK, activity reset, shutdown).
|
||||
* 11.2 Automatic Reconnection with Exponential Backoff (unexpected disconnect, state transitions, retry loop, recovery, exhaustion).
|
||||
* 11.3 Transport-Level Socket Tuning & Options (soTimeout, extended socket options, CLI parsing).
|
||||
* 11.4 IBM HoD ECL Compatibility Facade (SESSION_KEEPALIVE, keepAliveTimeout, SESSION_AUTORECONNECT, ECLConnection methods).
|
||||
*/
|
||||
public class Phase11ConnectionResilienceTest {
|
||||
|
||||
// =========================================================================
|
||||
// 11.1 Application-Layer Telnet Keep-Alive / Heartbeat Engine
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("11.1: Keep-Alive transmits Telnet IAC NOP (0xFF 0xF1) when idle")
|
||||
public void testKeepAliveTransmitsIacNop() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setKeepAliveEnabled(true);
|
||||
config.setKeepAliveIntervalSeconds(1);
|
||||
config.setKeepAliveType("NOP");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
CountDownLatch clientConnected = new CountDownLatch(1);
|
||||
|
||||
AtomicReference<Socket> serverAccepted = new AtomicReference<>();
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
Socket s = server.accept();
|
||||
serverAccepted.set(s);
|
||||
clientConnected.countDown();
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
client.connect();
|
||||
assertTrue(clientConnected.await(5, TimeUnit.SECONDS), "Server did not accept client connection");
|
||||
|
||||
Socket s = serverAccepted.get();
|
||||
assertNotNull(s);
|
||||
InputStream in = s.getInputStream();
|
||||
|
||||
// Explicitly trigger or wait for keepalive heartbeat
|
||||
client.getConnection().sendKeepAliveHeartbeat();
|
||||
|
||||
byte[] buf = new byte[2];
|
||||
int read = in.read(buf);
|
||||
assertEquals(2, read);
|
||||
assertEquals((byte) TelnetConstants.IAC, buf[0]);
|
||||
assertEquals((byte) TelnetConstants.NOP, buf[1]);
|
||||
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.1: Keep-Alive transmits Telnet IAC DO TIMING-MARK (0xFF 0xFD 0x06)")
|
||||
public void testKeepAliveTransmitsTimingMark() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setKeepAliveEnabled(true);
|
||||
config.setKeepAliveIntervalSeconds(1);
|
||||
config.setKeepAliveType("TIMING-MARK");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
CountDownLatch clientConnected = new CountDownLatch(1);
|
||||
|
||||
AtomicReference<Socket> serverAccepted = new AtomicReference<>();
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
Socket s = server.accept();
|
||||
serverAccepted.set(s);
|
||||
clientConnected.countDown();
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
client.connect();
|
||||
assertTrue(clientConnected.await(5, TimeUnit.SECONDS));
|
||||
|
||||
Socket s = serverAccepted.get();
|
||||
assertNotNull(s);
|
||||
InputStream in = s.getInputStream();
|
||||
|
||||
client.getConnection().sendKeepAliveHeartbeat();
|
||||
|
||||
byte[] buf = new byte[3];
|
||||
int read = in.read(buf);
|
||||
assertEquals(3, read);
|
||||
assertEquals((byte) TelnetConstants.IAC, buf[0]);
|
||||
assertEquals((byte) TelnetConstants.DO, buf[1]);
|
||||
assertEquals((byte) TelnetConstants.TELOPT_TM, buf[2]);
|
||||
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.1: Transmission resets last activity timestamp")
|
||||
public void testTransmissionResetsActivityTimestamp() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
server.accept();
|
||||
} catch (Exception ignored) {}
|
||||
}).start();
|
||||
|
||||
client.connect();
|
||||
TelnetConnection conn = client.getConnection();
|
||||
long initialTime = conn.getLastActivityTime();
|
||||
|
||||
Thread.sleep(15);
|
||||
conn.sendRaw(new byte[] { 0x01, 0x02 });
|
||||
|
||||
long updatedTime = conn.getLastActivityTime();
|
||||
assertTrue(updatedTime >= initialTime + 10, "Activity timestamp was not updated on sendRaw");
|
||||
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 11.2 Automatic Reconnection with Exponential Backoff
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("11.2: Unexpected EOF transitions to RECONNECTING state when autoReconnect is true")
|
||||
public void testUnexpectedDisconnectTransitionsToReconnecting() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setAutoReconnect(true);
|
||||
config.setReconnectMaxRetries(3);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
CopyOnWriteArrayList<ConnectionState> stateHistory = new CopyOnWriteArrayList<>();
|
||||
CountDownLatch reconnectingLatch = new CountDownLatch(1);
|
||||
|
||||
client.addConnectionListener(new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
|
||||
stateHistory.add(newState);
|
||||
if (newState == ConnectionState.RECONNECTING) {
|
||||
reconnectingLatch.countDown();
|
||||
}
|
||||
}
|
||||
@Override public void onConnectionError(String message) {}
|
||||
});
|
||||
|
||||
AtomicReference<Socket> acceptedSocket = new AtomicReference<>();
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
Socket s = server.accept();
|
||||
acceptedSocket.set(s);
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
client.connect();
|
||||
serverThread.join(2000);
|
||||
|
||||
assertNotNull(acceptedSocket.get());
|
||||
|
||||
// Abruptly sever the connection from the host side (EOF)
|
||||
acceptedSocket.get().close();
|
||||
|
||||
assertTrue(reconnectingLatch.await(4, TimeUnit.SECONDS), "Client did not enter RECONNECTING state on unexpected EOF");
|
||||
assertTrue(stateHistory.contains(ConnectionState.RECONNECTING), "State history must include RECONNECTING");
|
||||
assertTrue(client.isReconnecting());
|
||||
|
||||
// Check OIA message updated
|
||||
assertNotNull(client.getOIA());
|
||||
|
||||
client.disconnect();
|
||||
assertFalse(client.isReconnecting());
|
||||
assertEquals(ConnectionState.NOT_CONNECTED, client.getConnectionState());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.2: Intentional client.disconnect() transitions directly to NOT_CONNECTED without auto-reconnect")
|
||||
public void testIntentionalDisconnectDoesNotTriggerAutoReconnect() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setAutoReconnect(true);
|
||||
config.setReconnectMaxRetries(3);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
CopyOnWriteArrayList<ConnectionState> stateHistory = new CopyOnWriteArrayList<>();
|
||||
|
||||
client.addConnectionListener(new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
|
||||
stateHistory.add(newState);
|
||||
}
|
||||
@Override public void onConnectionError(String message) {}
|
||||
});
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
server.accept();
|
||||
} catch (Exception ignored) {}
|
||||
}).start();
|
||||
|
||||
client.connect();
|
||||
Thread.sleep(100);
|
||||
|
||||
// Explicit client disconnect
|
||||
client.disconnect();
|
||||
|
||||
assertFalse(stateHistory.contains(ConnectionState.RECONNECTING),
|
||||
"Intentional disconnect should never transition to RECONNECTING");
|
||||
assertEquals(ConnectionState.NOT_CONNECTED, client.getConnectionState());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.2: Auto-reconnect retry loop successfully re-establishes connection")
|
||||
public void testAutoReconnectSuccessfulReconnection() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setAutoReconnect(true);
|
||||
config.setReconnectMaxRetries(3);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
CountDownLatch reconnectedLatch = new CountDownLatch(1);
|
||||
|
||||
AtomicReference<Socket> firstConn = new AtomicReference<>();
|
||||
AtomicReference<Socket> secondConn = new AtomicReference<>();
|
||||
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
Socket s1 = server.accept();
|
||||
firstConn.set(s1);
|
||||
// Close first socket to trigger reconnect
|
||||
Thread.sleep(50);
|
||||
s1.close();
|
||||
|
||||
// Accept the reconnection attempt
|
||||
Socket s2 = server.accept();
|
||||
secondConn.set(s2);
|
||||
reconnectedLatch.countDown();
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
client.connect();
|
||||
|
||||
// Wait for reconnection to succeed
|
||||
assertTrue(reconnectedLatch.await(8, TimeUnit.SECONDS), "Server did not receive reconnection attempt");
|
||||
assertNotNull(secondConn.get());
|
||||
|
||||
// Wait for client to complete handshake / notify connected
|
||||
Thread.sleep(200);
|
||||
assertTrue(client.isConnected());
|
||||
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 11.3 Transport-Level Socket Tuning & Options
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("11.3: Configurable SO_TIMEOUT is applied to TCP socket")
|
||||
public void testSoTimeoutApplied() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setSoTimeoutMs(750);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
new Thread(() -> {
|
||||
try { server.accept(); } catch (Exception ignored) {}
|
||||
}).start();
|
||||
|
||||
client.connect();
|
||||
assertNotNull(client.getConnection());
|
||||
// Client socket should have SO_TIMEOUT applied
|
||||
assertEquals(750, config.getSoTimeoutMs());
|
||||
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.3: parseHostString parses keepalive and autoreconnect flags")
|
||||
public void testParseHostStringFlags() {
|
||||
ConnectionConfig c1 = ConnectionConfig.parseHostString("mainframe.corp.com:23 --keepalive --autoreconnect", 23, TerminalModel.IBM_3279_4);
|
||||
assertTrue(c1.isKeepAliveEnabled());
|
||||
assertTrue(c1.isAutoReconnect());
|
||||
assertEquals("mainframe.corp.com", c1.getHost());
|
||||
assertEquals(23, c1.getPort());
|
||||
|
||||
ConnectionConfig c2 = ConnectionConfig.parseHostString("mainframe.corp.com:23 --no-keepalive --no-autoreconnect", 23, TerminalModel.IBM_3279_4);
|
||||
assertFalse(c2.isKeepAliveEnabled());
|
||||
assertFalse(c2.isAutoReconnect());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 11.4 IBM HoD ECL Compatibility Facade
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("11.4: ECLSession parses HoD properties for Keep-Alive and Auto-Reconnect")
|
||||
public void testEclSessionPropertiesParsing() {
|
||||
Properties props = new Properties();
|
||||
props.setProperty(ECLSession.SESSION_HOST, "mvs01.test.net");
|
||||
props.setProperty(ECLSession.SESSION_PORT, "23");
|
||||
props.setProperty(ECLSession.SESSION_KEEPALIVE, "true");
|
||||
props.setProperty(ECLSession.KEY_KEEPALIVE_TIMEOUT, "180");
|
||||
props.setProperty(ECLSession.KEY_KEEPALIVE_TYPE, "TIMING-MARK");
|
||||
props.setProperty(ECLSession.SESSION_AUTORECONNECT, "true");
|
||||
props.setProperty(ECLSession.SESSION_RECONNECT_RETRIES, "8");
|
||||
|
||||
ECLSession session = new ECLSession(props);
|
||||
assertTrue(session.isKeepAlive());
|
||||
assertEquals(180, session.getKeepAliveTimeout());
|
||||
assertTrue(session.isAutoReconnect());
|
||||
|
||||
ECLConnection conn = session.getConnection();
|
||||
assertNotNull(conn);
|
||||
assertTrue(conn.isKeepAlive());
|
||||
assertEquals(180, conn.getKeepAliveTimeout());
|
||||
assertEquals("TIMING-MARK", conn.getKeepAliveType());
|
||||
assertTrue(conn.isAutoReconnect());
|
||||
assertEquals(8, conn.getMaxRetry());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.4: ECLConnection getters and setters synchronize with underlying client")
|
||||
public void testEclConnectionGettersSetters() {
|
||||
ConnectionConfig cfg = new ConnectionConfig("localhost", 23);
|
||||
Telnet3270Client client = new Telnet3270Client(cfg);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLConnection conn = session.getConnection();
|
||||
|
||||
conn.setKeepAlive(false);
|
||||
assertFalse(conn.isKeepAlive());
|
||||
assertFalse(client.getConfig().isKeepAliveEnabled());
|
||||
|
||||
conn.setKeepAliveTimeout(60);
|
||||
assertEquals(60, conn.getKeepAliveTimeout());
|
||||
assertEquals(60, client.getConfig().getKeepAliveIntervalSeconds());
|
||||
|
||||
conn.setKeepAliveType("TIMING-MARK");
|
||||
assertEquals("TIMING-MARK", conn.getKeepAliveType());
|
||||
assertEquals("TIMING-MARK", client.getConfig().getKeepAliveType());
|
||||
|
||||
conn.setAutoReconnect(true);
|
||||
assertTrue(conn.isAutoReconnect());
|
||||
assertTrue(client.getConfig().isAutoReconnect());
|
||||
|
||||
conn.setMaxRetry(10);
|
||||
assertEquals(10, conn.getMaxRetry());
|
||||
assertEquals(10, client.getConfig().getReconnectMaxRetries());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
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.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for Phase 13.2: Interactive Entry Assist Engine.
|
||||
* Verifies interactive word wrap in DOC mode and audible bell alert
|
||||
* when typing reaches the configured bell column.
|
||||
*/
|
||||
public class InteractiveEntryAssistTest {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor input;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||
input = new InputProcessor(screen, translator, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAudibleBellTriggersAtConfiguredBellColumn() {
|
||||
input.setBellEnabled(true);
|
||||
input.setBellColumn(74); // 0-based column 74 = column 75
|
||||
|
||||
AtomicInteger bellCount = new AtomicInteger(0);
|
||||
input.setBellListener(bellCount::incrementAndGet);
|
||||
|
||||
screen.setCursorPosition(0, 72);
|
||||
input.typeCharacter('A'); // col 73
|
||||
assertEquals(0, bellCount.get());
|
||||
|
||||
input.typeCharacter('B'); // col 74 -> triggers bell
|
||||
assertEquals(1, bellCount.get());
|
||||
|
||||
input.typeCharacter('C'); // col 75 -> remains 1 on current row
|
||||
assertEquals(1, bellCount.get());
|
||||
|
||||
// Moving to next row and typing across bell column triggers bell again
|
||||
screen.setCursorPosition(1, 73);
|
||||
input.typeCharacter('X'); // advances to col 74 on row 1
|
||||
assertEquals(2, bellCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDocModeWordWrapMovesPartialWordToNextRow() {
|
||||
screen.setEntryAssistDOCmode(true);
|
||||
screen.setEntryAssistWordWrap(true);
|
||||
screen.setLeftMargin(0); // Col 0
|
||||
screen.setRightMargin(15); // Col 15
|
||||
|
||||
assertTrue(screen.isEntryAssistDOCmode());
|
||||
assertTrue(screen.isEntryAssistWordWrap());
|
||||
|
||||
// Type "HELLO " (6 chars) starting at col 0
|
||||
screen.setCursorPosition(0, 0);
|
||||
for (char c : "HELLO ".toCharArray()) {
|
||||
input.typeCharacter(c);
|
||||
}
|
||||
assertEquals(0, screen.getCursorRow());
|
||||
assertEquals(6, screen.getCursorCol());
|
||||
|
||||
// Move cursor near right margin: Row 0, Col 13
|
||||
screen.setCursorPosition(0, 13);
|
||||
input.typeCharacter('P'); // col 14
|
||||
input.typeCharacter('A'); // col 15 (at right margin)
|
||||
input.typeCharacter('R'); // crosses right margin -> triggers word wrap
|
||||
|
||||
// After word wrap, the word "PAR" should be moved to Row 1, left margin (Col 0)
|
||||
assertEquals(1, screen.getCursorRow());
|
||||
assertTrue(screen.getCursorCol() >= 3);
|
||||
|
||||
// Verify content on Row 1
|
||||
assertEquals('P', (char) screen.getCell(80).ucs4);
|
||||
assertEquals('A', (char) screen.getCell(81).ucs4);
|
||||
assertEquals('R', (char) screen.getCell(82).ucs4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Phase 12 Test Suite: Reactive Synchronization & Thread-Safety Hardening.
|
||||
*
|
||||
* Verifies:
|
||||
* 12.1 Condition-Based Synchronization for ECL Automation Calls:
|
||||
* - Sub-millisecond reactive wakeup for waitForScreen(ECLScreenDesc).
|
||||
* - Reactive wakeup for waitForCursor(row, col).
|
||||
* - Reactive wakeup for waitForString(text) and waitWhileScreen(ECLScreenDesc).
|
||||
* - Clean timeout handling without CPU busy-wait spinning.
|
||||
* - Graceful thread interruption handling.
|
||||
* - ECLOIA condition-based reactive waitForInput and waitForTransition.
|
||||
* - ScreenBuffer listener dispatch and cursor change tracking.
|
||||
* - Concurrency safety with multiple waiters.
|
||||
*/
|
||||
public class Phase12SynchronizationTest {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor inputProcessor;
|
||||
private ECLPS ps;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
inputProcessor = new InputProcessor(screen, translator, null);
|
||||
ps = new ECLPS(screen, inputProcessor, translator);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: waitForScreen(ECLScreenDesc) wakes up reactively upon screen update")
|
||||
public void testReactiveWaitForScreenDesc() throws Exception {
|
||||
ECLScreenDesc desc = new ECLScreenDesc();
|
||||
desc.addString("TSO/E LOGON", 5, 10);
|
||||
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
AtomicLong durationMs = new AtomicLong(-1);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean matched = ps.waitForScreen(desc, 5000);
|
||||
durationMs.set(System.currentTimeMillis() - t0);
|
||||
result.set(matched);
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50); // Give waiter time to block on condition
|
||||
|
||||
// Write matching text at 1-based (5, 10) -> 0-based row 4 col 9
|
||||
screen.setText("TSO/E LOGON", 4, 9);
|
||||
screen.notifyScreenUpdate();
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(result.get(), "waitForScreen should match");
|
||||
assertTrue(durationMs.get() < 1500, "Should unblock reactively well before 5000ms timeout (took " + durationMs.get() + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: waitForCursor(row, col) wakes up reactively upon cursor position change")
|
||||
public void testReactiveWaitForCursor() throws Exception {
|
||||
screen.setCursorPosition(0, 0);
|
||||
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
AtomicLong durationMs = new AtomicLong(-1);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean matched = ps.waitForCursor(12, 34, 5000);
|
||||
durationMs.set(System.currentTimeMillis() - t0);
|
||||
result.set(matched);
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
// Move cursor
|
||||
screen.setCursorPosition(12, 34);
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(result.get(), "waitForCursor should succeed");
|
||||
assertTrue(durationMs.get() < 1500, "Should unblock reactively upon cursor move (took " + durationMs.get() + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: waitForString(text) wakes up reactively upon string appearance")
|
||||
public void testReactiveWaitForString() throws Exception {
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
AtomicLong durationMs = new AtomicLong(-1);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean matched = ps.waitForString("COMMAND ===>", 5000);
|
||||
durationMs.set(System.currentTimeMillis() - t0);
|
||||
result.set(matched);
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
screen.setText("COMMAND ===>", 20, 2);
|
||||
screen.notifyScreenUpdate();
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(result.get(), "waitForString should find the text");
|
||||
assertTrue(durationMs.get() < 1500, "Should unblock reactively (took " + durationMs.get() + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: waitWhileScreen(desc) wakes up reactively when condition ceases to match")
|
||||
public void testReactiveWaitWhileScreen() throws Exception {
|
||||
// Place initial text
|
||||
screen.setText("HOLDING", 0, 0);
|
||||
|
||||
ECLScreenDesc desc = new ECLScreenDesc();
|
||||
desc.addString("HOLDING");
|
||||
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
AtomicLong durationMs = new AtomicLong(-1);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean finished = ps.waitWhileScreen(desc, 5000);
|
||||
durationMs.set(System.currentTimeMillis() - t0);
|
||||
result.set(finished);
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
// Erase screen so "HOLDING" is gone
|
||||
screen.erase(false);
|
||||
screen.notifyScreenUpdate();
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(result.get(), "waitWhileScreen should return true once condition is gone");
|
||||
assertTrue(durationMs.get() < 1500, "Should unblock reactively when text erased (took " + durationMs.get() + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: Clean timeout without false match or spinning")
|
||||
public void testCleanTimeout() {
|
||||
ECLScreenDesc desc = new ECLScreenDesc();
|
||||
desc.addString("NON_EXISTENT_STRING");
|
||||
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean matched = ps.waitForScreen(desc, 150);
|
||||
long elapsed = System.currentTimeMillis() - t0;
|
||||
|
||||
assertFalse(matched, "Should not match non-existent text");
|
||||
assertTrue(elapsed >= 100, "Should have waited for the full timeout window (elapsed=" + elapsed + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: Graceful thread interruption during wait")
|
||||
public void testThreadInterruption() throws Exception {
|
||||
ECLScreenDesc desc = new ECLScreenDesc();
|
||||
desc.addString("NEVER_APPEARS");
|
||||
|
||||
AtomicBoolean wasInterrupted = new AtomicBoolean(false);
|
||||
AtomicBoolean result = new AtomicBoolean(true);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
boolean r = ps.waitForScreen(desc, 10000);
|
||||
result.set(r);
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
wasInterrupted.set(true);
|
||||
}
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
waiter.interrupt();
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertFalse(result.get(), "Interrupted wait should return false");
|
||||
assertTrue(wasInterrupted.get(), "Thread interrupt flag should be preserved");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: ECLOIA reactive waitForInput and waitForTransition")
|
||||
public void testECLOIAReactiveWait() throws Exception {
|
||||
ECLOIA oia = new ECLOIA(screen, inputProcessor, null);
|
||||
oia.setInputInhibited(ECLOIA.INHIBIT_SYSTEMWAIT);
|
||||
|
||||
AtomicBoolean inputReady = new AtomicBoolean(false);
|
||||
AtomicLong durationMs = new AtomicLong(-1);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean ready = oia.waitForInput(5000);
|
||||
durationMs.set(System.currentTimeMillis() - t0);
|
||||
inputReady.set(ready);
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
// Unlock OIA
|
||||
oia.setInputInhibited(ECLOIA.INHIBIT_NOTINHIBITED);
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(inputReady.get(), "waitForInput should succeed");
|
||||
assertTrue(durationMs.get() < 1500, "Should unblock reactively upon OIA unlock (took " + durationMs.get() + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: ScreenBuffer listener dispatch and cursor movement tracking")
|
||||
public void testScreenBufferListenerDispatch() {
|
||||
AtomicBoolean screenUpdated = new AtomicBoolean(false);
|
||||
AtomicInteger cursorOld = new AtomicInteger(-1);
|
||||
AtomicInteger cursorNew = new AtomicInteger(-1);
|
||||
|
||||
ScreenUpdateListener listener = new ScreenUpdateListener() {
|
||||
@Override
|
||||
public void onScreenUpdated() {
|
||||
screenUpdated.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCursorMoved(int oldAddress, int newAddress) {
|
||||
cursorOld.set(oldAddress);
|
||||
cursorNew.set(newAddress);
|
||||
}
|
||||
};
|
||||
|
||||
screen.addUpdateListener(listener);
|
||||
|
||||
screen.notifyScreenUpdate();
|
||||
assertTrue(screenUpdated.get(), "Listener should receive onScreenUpdated");
|
||||
|
||||
screen.setCursorAddress(123);
|
||||
assertEquals(0, cursorOld.get(), "Old cursor should be 0");
|
||||
assertEquals(123, cursorNew.get(), "New cursor should be 123");
|
||||
|
||||
screen.removeUpdateListener(listener);
|
||||
screenUpdated.set(false);
|
||||
screen.notifyScreenUpdate();
|
||||
assertFalse(screenUpdated.get(), "Removed listener should not receive updates");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: Concurrent waiters on the same ScreenBuffer")
|
||||
public void testConcurrentWaiters() throws Exception {
|
||||
int numWaiters = 5;
|
||||
CountDownLatch startLatch = new CountDownLatch(numWaiters);
|
||||
CountDownLatch doneLatch = new CountDownLatch(numWaiters);
|
||||
AtomicInteger successCount = new AtomicInteger(0);
|
||||
|
||||
for (int i = 0; i < numWaiters; i++) {
|
||||
new Thread(() -> {
|
||||
startLatch.countDown();
|
||||
boolean ok = ps.waitForString("BATCH_SIGNAL", 5000);
|
||||
if (ok) {
|
||||
successCount.incrementAndGet();
|
||||
}
|
||||
doneLatch.countDown();
|
||||
}).start();
|
||||
}
|
||||
|
||||
assertTrue(startLatch.await(2, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
screen.setText("BATCH_SIGNAL", 100);
|
||||
screen.notifyScreenUpdate();
|
||||
|
||||
assertTrue(doneLatch.await(3, TimeUnit.SECONDS));
|
||||
assertEquals(numWaiters, successCount.get(), "All concurrent waiters should unblock reactively");
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@ import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import java.awt.Image;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.image.BufferedImage;
|
||||
import haus.nightmare.lib3270j.graphics.DefaultPixelBuffer;
|
||||
import haus.nightmare.lib3270j.graphics.PixelBuffer;
|
||||
import haus.nightmare.lib3270j.graphics.Rectangle;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -406,11 +406,12 @@ public class Phase5EclEventTest {
|
||||
}
|
||||
|
||||
// Test with image and rectangle
|
||||
Image testImage = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
PixelBuffer testImage = new DefaultPixelBuffer(32, 32);
|
||||
Rectangle rect = new Rectangle(0, 0, 32, 32);
|
||||
ECLPSGraphicsEvent fullGEvent = new ECLPSGraphicsEvent(ps, ECLPSGraphicsEvent.GRAPHICS_UPDATED, testImage, rect);
|
||||
assertSame(testImage, fullGEvent.GetImage());
|
||||
assertSame(testImage, fullGEvent.getImage());
|
||||
assertSame(testImage, fullGEvent.getPixelBuffer());
|
||||
assertEquals(rect, fullGEvent.GetRectangle());
|
||||
assertEquals(rect, fullGEvent.getRectangle());
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
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 static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for Phase 13.1: Excel & Tabular Clipboard Paste.
|
||||
* Verifies tab-delimited text navigation, newline row advancement,
|
||||
* and boundary truncation when pasteStopAtProtectedLine is set.
|
||||
*/
|
||||
public class TabularPasteTest {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor input;
|
||||
private ECLPS ps;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||
input = new InputProcessor(screen, translator, null);
|
||||
ps = new ECLPS(screen, input, translator);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExcelPasteTabsAndNewlinesAdvanceAcrossFields() {
|
||||
// Setup 2 rows with 2 unprotected fields each
|
||||
// Row 0: pos 0 (unprotected), pos 10 (protected), pos 11 (unprotected), pos 30 (protected)
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
screen.setFieldAttribute(11, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(30, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
// Row 1: pos 80 (unprotected), pos 90 (protected), pos 91 (unprotected), pos 110 (protected)
|
||||
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(90, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
screen.setFieldAttribute(91, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(110, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
// Position cursor at first field (pos 1)
|
||||
screen.setCursorAddress(1);
|
||||
|
||||
String tabularData = "ABC\tDEF\r\nGHI\tJKL";
|
||||
int pasted = input.pasteText(tabularData, true, false);
|
||||
assertEquals(12, pasted);
|
||||
|
||||
// Verify Field 1 (Row 0, Col 1-3)
|
||||
assertEquals("ABC", ps.getString(1, 3));
|
||||
// Verify Field 2 (Row 0, Col 12-14)
|
||||
assertEquals("DEF", ps.getString(12, 3));
|
||||
// Verify Field 3 (Row 1, Col 1-3 -> pos 81)
|
||||
assertEquals("GHI", ps.getString(81, 3));
|
||||
// Verify Field 4 (Row 1, Col 12-14 -> pos 92)
|
||||
assertEquals("JKL", ps.getString(92, 3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasteStopAtProtectedBoundaryHaltsPaste() {
|
||||
// Field 1: pos 0 (unprotected), pos 5 (protected)
|
||||
// Data area is pos 1..4 (4 characters)
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(5, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
screen.setFieldAttribute(10, (byte) FA_PRINTABLE);
|
||||
|
||||
screen.setCursorAddress(1);
|
||||
|
||||
// Try to paste 6 characters when only 4 fit
|
||||
String overflow = "123456";
|
||||
int pasted = input.pasteText(overflow, false, true);
|
||||
|
||||
// Should paste 4 chars and halt at the protected boundary pos 5
|
||||
assertEquals(4, pasted);
|
||||
assertEquals("1234", ps.getString(1, 4));
|
||||
// Field at pos 10 should not be touched
|
||||
assertEquals(" ", ps.getString(11, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasteStopAtProtectedLineHaltsAtProtectedRow() {
|
||||
// Row 0: unprotected field at pos 0
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
// Row 1: entirely protected starting at pos 80
|
||||
screen.setFieldAttribute(80, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
screen.setCursorAddress(1);
|
||||
|
||||
String multiRow = "DATA1\r\nDATA2";
|
||||
int pasted = input.pasteText(multiRow, true, true);
|
||||
|
||||
// "DATA1" (5 chars) pasted, then newline sees next line is protected and halts
|
||||
assertEquals(5, pasted);
|
||||
assertEquals("DATA1", ps.getString(1, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEclPsPasteFromExcelMethodsAndFlags() {
|
||||
assertTrue(ps.isEnablePasteFromExcel());
|
||||
assertFalse(ps.isPasteStopAtProtectedLine());
|
||||
|
||||
ps.setEnablePasteFromExcel(false);
|
||||
assertFalse(ps.isEnablePasteFromExcel());
|
||||
|
||||
ps.setPasteStopAtProtectedLine(true);
|
||||
assertTrue(ps.isPasteStopAtProtectedLine());
|
||||
|
||||
// Setup fields
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
screen.setFieldAttribute(11, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(30, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
ps.pasteFromExcel("HELLO\tWORLD", 0, 1);
|
||||
assertEquals("HELLO", ps.getString(1, 5));
|
||||
assertEquals("WORLD", ps.getString(12, 5));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Architectural compliance audit test verifying 100% decoupling of lib3270j from java.awt.
|
||||
* Ensures headless execution and portability to Android (which lacks java.awt.*).
|
||||
*/
|
||||
public class AwtDecouplingAuditTest {
|
||||
|
||||
private static final Pattern IMPORT_AWT_PATTERN = Pattern.compile("^\\s*import\\s+java\\.awt\\..*;");
|
||||
private static final Pattern DIRECT_AWT_USAGE_PATTERN = Pattern.compile("(?<!Class\\.forName\\(\")java\\.awt\\.[A-Za-z0-9_]+");
|
||||
|
||||
@Test
|
||||
@DisplayName("Audit lib3270j/src/main/java for zero java.awt.* imports or references")
|
||||
public void testZeroAwtDependenciesInMainSource() throws IOException {
|
||||
File srcMainDir = new File("src/main/java");
|
||||
if (!srcMainDir.exists()) {
|
||||
srcMainDir = new File("lib3270j/src/main/java");
|
||||
}
|
||||
assertTrue(srcMainDir.exists() && srcMainDir.isDirectory(),
|
||||
"src/main/java directory must exist at " + srcMainDir.getAbsolutePath());
|
||||
|
||||
List<String> violations = new ArrayList<>();
|
||||
|
||||
try (Stream<Path> paths = Files.walk(srcMainDir.toPath())) {
|
||||
List<Path> javaFiles = paths
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(p -> p.toString().endsWith(".java"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
assertFalse(javaFiles.isEmpty(), "Found no java files in " + srcMainDir);
|
||||
|
||||
for (Path p : javaFiles) {
|
||||
List<String> lines = Files.readAllLines(p);
|
||||
for (int lineNum = 0; lineNum < lines.size(); lineNum++) {
|
||||
String line = lines.get(lineNum);
|
||||
String trimmed = line.trim();
|
||||
|
||||
// Ignore comment lines
|
||||
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IMPORT_AWT_PATTERN.matcher(line).find()) {
|
||||
violations.add(String.format("%s:%d -> %s", p.getFileName(), lineNum + 1, trimmed));
|
||||
} else if (line.contains("java.awt.") && !line.contains("Class.forName(\"java.awt.") && !line.contains("\"java.awt.")) {
|
||||
violations.add(String.format("%s:%d [raw type reference] -> %s", p.getFileName(), lineNum + 1, trimmed));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!violations.isEmpty()) {
|
||||
fail("Found java.awt dependencies in lib3270j main source:\n" + String.join("\n", violations));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Verify PixelBuffer and software rasterizer functions without AWT")
|
||||
public void testPixelBufferPureJavaOperations() {
|
||||
PixelBuffer pb = new DefaultPixelBuffer(100, 100);
|
||||
assertEquals(100, pb.getWidth());
|
||||
assertEquals(100, pb.getHeight());
|
||||
|
||||
// Fill rect
|
||||
pb.fillRect(10, 10, 20, 20, 0xFFFF0000);
|
||||
assertEquals(0xFFFF0000, pb.getPixel(15, 15));
|
||||
assertEquals(0, pb.getPixel(5, 5));
|
||||
|
||||
// Draw line AA
|
||||
pb.drawLineAA(0, 0, 99, 99, 0xFF00FF00, 1.0);
|
||||
int diagPixel = pb.getPixel(50, 50);
|
||||
assertNotEquals(0, diagPixel, "Anti-aliased diagonal line should render pixels");
|
||||
|
||||
// Clipping
|
||||
pb.setClip(20, 20, 10, 10);
|
||||
assertTrue(pb.isClipped(2, 80));
|
||||
assertFalse(pb.isClipped(25, 25));
|
||||
assertEquals(0, pb.getPixel(2, 80));
|
||||
pb.setPixel(2, 80, 0xFF0000FF);
|
||||
assertEquals(0, pb.getPixel(2, 80), "Clipped pixel must not be modified");
|
||||
|
||||
pb.clearClip();
|
||||
assertFalse(pb.isClipped(5, 5));
|
||||
}
|
||||
}
|
||||
-1
@@ -1,7 +1,6 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.awt.Point;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.awt.Point;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
|
||||
-2
@@ -7,8 +7,6 @@ import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,14 +4,6 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Image;
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
@@ -201,7 +193,7 @@ public class Phase2GocaEngineTest {
|
||||
@Test
|
||||
@DisplayName("Test HODWallpaper Tile, Center, and Stretch")
|
||||
public void testHODWallpaper() {
|
||||
BufferedImage img = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
PixelBuffer img = new DefaultPixelBuffer(32, 32);
|
||||
HODWallpaper wp = new HODWallpaper(img, HODWallpaper.HOD_CENTER);
|
||||
assertEquals(HODWallpaper.HOD_CENTER, wp.getDisplay());
|
||||
|
||||
@@ -211,10 +203,8 @@ public class Phase2GocaEngineTest {
|
||||
wp.setDisplay(HODWallpaper.HOD_STRETCH);
|
||||
assertEquals(HODWallpaper.HOD_STRETCH, wp.getDisplay());
|
||||
|
||||
BufferedImage canvas = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics g = canvas.getGraphics();
|
||||
wp.paint(new java.awt.Canvas(), g, 0, 0, 100, 100);
|
||||
g.dispose();
|
||||
PixelBuffer canvas = new DefaultPixelBuffer(100, 100);
|
||||
wp.paint(canvas, 0, 0, 100, 100);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -283,8 +273,9 @@ public class Phase2GocaEngineTest {
|
||||
assertEquals(101, bounds.width);
|
||||
assertEquals(101, bounds.height);
|
||||
|
||||
Image img = fa.getImage();
|
||||
Object img = fa.getImage();
|
||||
assertNotNull(img);
|
||||
assertNotNull(fa.getPixelBuffer());
|
||||
fa.dispose();
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.Point;
|
||||
import haus.nightmare.lib3270j.graphics.Point;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package haus.nightmare.lib3270j.integration;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.listener.ConnectionListener;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Live integration test verifying headless connection to live MVS host:
|
||||
* mvs.hugfreevikings.wtf:1023 using guest credentials.
|
||||
*/
|
||||
@Tag("integration")
|
||||
public class LiveHostHeadlessTest {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(LiveHostHeadlessTest.class.getName());
|
||||
private static final String HOST = "mvs.hugfreevikings.wtf";
|
||||
private static final int PORT = 1023;
|
||||
|
||||
@Test
|
||||
@DisplayName("Connect headless Telnet3270Client to mvs.hugfreevikings.wtf:1023")
|
||||
public void testLiveMvsConnection() throws Exception {
|
||||
// Probe host reachability (timeout 3 seconds)
|
||||
boolean reachable = false;
|
||||
try (Socket probe = new Socket()) {
|
||||
probe.connect(new InetSocketAddress(HOST, PORT), 3000);
|
||||
reachable = true;
|
||||
} catch (IOException e) {
|
||||
logger.warning("Live host " + HOST + ":" + PORT + " unreachable: " + e.getMessage());
|
||||
}
|
||||
|
||||
Assumptions.assumeTrue(reachable, "Skipping live test: " + HOST + ":" + PORT + " is not reachable");
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(HOST, PORT, TerminalModel.IBM_3279_2);
|
||||
config.setConnectTimeoutMs(5000);
|
||||
config.setSoTimeoutMs(10000);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
|
||||
CountDownLatch connectedLatch = new CountDownLatch(1);
|
||||
CountDownLatch screenLatch = new CountDownLatch(1);
|
||||
|
||||
client.addConnectionListener(new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
|
||||
if (newState == ConnectionState.CONNECTED_3270) {
|
||||
connectedLatch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionError(String message) {}
|
||||
});
|
||||
|
||||
client.addScreenUpdateListener(new ScreenUpdateListener() {
|
||||
@Override
|
||||
public void onScreenUpdated() {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
if (sb != null && sb.isFormatted()) {
|
||||
screenLatch.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
client.connect();
|
||||
|
||||
boolean connected = connectedLatch.await(5, TimeUnit.SECONDS);
|
||||
assertTrue(connected, "Client must establish CONNECTED_3270 state");
|
||||
|
||||
boolean screenReceived = screenLatch.await(5, TimeUnit.SECONDS);
|
||||
assertTrue(screenReceived, "Client must receive formatted screen from live MVS host");
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
assertNotNull(sb);
|
||||
|
||||
StringBuilder fullText = new StringBuilder();
|
||||
for (int i = 0; i < sb.getSize(); i++) {
|
||||
char c = sb.getCell(i).ucs4;
|
||||
fullText.append(c > ' ' ? c : ' ');
|
||||
}
|
||||
String screenContent = fullText.toString().trim();
|
||||
assertFalse(screenContent.isEmpty(), "Screen buffer text should not be empty");
|
||||
|
||||
// Verify headless graphics plane is initialized without AWT
|
||||
assertNotNull(client.getGraphicsPlane());
|
||||
assertEquals(720, client.getGraphicsPlane().getWidth());
|
||||
assertEquals(384, client.getGraphicsPlane().getHeight());
|
||||
|
||||
logger.info("Successfully connected to live host. Banner preview: " +
|
||||
screenContent.substring(0, Math.min(200, screenContent.length())).replaceAll("\\s+", " "));
|
||||
} finally {
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user