Add bugfix for ATTN
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 35s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m13s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m36s

This commit is contained in:
2026-09-18 17:33:58 +00:00
parent 5049d8df74
commit 8a30efe9c0
35 changed files with 4201 additions and 6 deletions
@@ -0,0 +1,359 @@
package haus.nightmare.lib3270j.macro;
import static org.junit.jupiter.api.Assertions.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLOIA;
import haus.nightmare.lib3270j.ecl.ECLPS;
import haus.nightmare.lib3270j.ecl.ECLScreenDesc;
import haus.nightmare.lib3270j.ecl.ECLSession;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.macro.action.*;
import haus.nightmare.lib3270j.macro.variable.MacroExpressionParser;
import haus.nightmare.lib3270j.macro.variable.MacroVariables;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
/**
* Test suite for Phase 24: IBM HoD XML Macro Runtime (<HASCRIPT>) & Expression Evaluator.
*/
public class Phase24MacroRuntimeTest {
private ScreenBuffer screen;
private InputProcessor inputProcessor;
private EbcdicTranslator translator;
private ECLPS ps;
private ECLOIA oia;
private ECLSession session;
@BeforeEach
public void setUp() {
session = new ECLSession();
ps = session.GetPS();
oia = session.GetOIA();
screen = ps.getScreenBuffer();
translator = ps.getTranslator();
inputProcessor = ps.getInputProcessor();
}
@Test
@DisplayName("24.2: MacroVariables variable creation, types, and canonical resolution")
public void testMacroVariables() {
MacroVariables vars = new MacroVariables();
vars.createVariable("$username", "string", "IBMUSER");
vars.createVariable("$counter$", "integer", "10");
vars.createVariable("$rate", "double", "3.14159");
vars.createVariable("$flag", "boolean", "true");
vars.createVariable("$pos", "field", "5,10");
assertTrue(vars.isUseVars());
assertTrue(vars.containsKey("$username"));
assertTrue(vars.containsKey("$username$"));
assertTrue(vars.containsKey("username"));
assertEquals("IBMUSER", vars.getString("$username"));
assertEquals(10, vars.getInt("$counter"));
assertEquals(3.14159, vars.getDouble("$rate"), 0.0001);
assertTrue(vars.getBoolean("$flag"));
assertEquals("5,10", vars.getString("$pos"));
// Update variable
vars.setVariable("$counter", 25);
assertEquals(25, vars.getInt("$counter"));
// Reset restores initial values
vars.reset();
assertEquals(10, vars.getInt("$counter"));
// Validation
assertTrue(MacroVariables.isValidName("$myVar"));
assertTrue(MacroVariables.isValidName("$my_var$"));
assertFalse(MacroVariables.isValidName("invalidVar"));
assertTrue(MacroVariables.isValidType("string"));
assertTrue(MacroVariables.isValidType("integer"));
assertFalse(MacroVariables.isValidType("unknown"));
}
@Test
@DisplayName("24.2: MacroExpressionParser arithmetic, comparisons, logic, and concatenation")
public void testMacroExpressionParser() {
MacroVariables vars = new MacroVariables();
vars.createVariable("$a", "integer", "15");
vars.createVariable("$b", "integer", "5");
vars.createVariable("$name", "string", "World");
// Arithmetic
assertEquals(20, MacroExpressionParser.evaluateInt(vars, "$a + $b"));
assertEquals(10, MacroExpressionParser.evaluateInt(vars, "$a - $b"));
assertEquals(75, MacroExpressionParser.evaluateInt(vars, "$a * $b"));
assertEquals(3, MacroExpressionParser.evaluateInt(vars, "$a / $b"));
assertEquals(1, MacroExpressionParser.evaluateInt(vars, "$a % 2"));
assertEquals(35, MacroExpressionParser.evaluateInt(vars, "$a + $b * 4")); // Precedence 15 + (5*4) = 35
assertEquals(80, MacroExpressionParser.evaluateInt(vars, "($a + $b) * 4")); // Parentheses (15+5)*4 = 80
// String Concatenation
assertEquals("Hello World", MacroExpressionParser.evaluateString(vars, "'Hello ' + $name"));
assertEquals("Count: 15", MacroExpressionParser.evaluateString(vars, "'Count: ' + $a"));
// Comparisons
assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a > $b"));
assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a >= 15"));
assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$b < 10"));
assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$b <= 5"));
assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a == 15"));
assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$name == 'world'")); // Case-insensitive per HoD
assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a != 99"));
// Logical Operators
assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a > 10 && $b < 10"));
assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a > 20 || $b == 5"));
assertTrue(MacroExpressionParser.evaluateBoolean(vars, "!($a < 10)"));
assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a == 15 AND $b == 5"));
assertTrue(MacroExpressionParser.evaluateBoolean(vars, "$a == 99 OR $b == 5"));
// resolveText
assertEquals("Logon for World with ID 15", MacroExpressionParser.resolveText(vars, "Logon for $name with ID $a"));
}
@Test
@DisplayName("24.1: MacroXmlParser parsing <HASCRIPT> AST and roundtrip XML formatting")
public void testMacroXmlParser() {
String xml = "<HASCRIPT name=\"TestMacro\" description=\"A unit test macro\" timeout=\"8000\" pausetime=\"200\" usevars=\"true\">\n"
+ " <vars>\n"
+ " <create name=\"$acct\" type=\"string\" value=\"12345\" />\n"
+ " <create name=\"$retry\" type=\"integer\" value=\"0\" />\n"
+ " </vars>\n"
+ " <screen name=\"Screen1\" entryscreen=\"true\" pausetime=\"150\">\n"
+ " <description>\n"
+ " <string value=\"TSO/E LOGON\" row=\"1\" col=\"35\" />\n"
+ " <cursor row=\"5\" col=\"10\" />\n"
+ " </description>\n"
+ " <actions>\n"
+ " <input value=\"$acct + '[enter]'\" row=\"5\" col=\"10\" movecursor=\"true\" xlatehostkeys=\"true\" />\n"
+ " <varupdate name=\"$retry\" value=\"$retry + 1\" />\n"
+ " </actions>\n"
+ " <nextscreens timeout=\"5000\">\n"
+ " <nextscreen name=\"Screen2\" />\n"
+ " </nextscreens>\n"
+ " <recolimit value=\"3\" goto=\"ScreenError\" />\n"
+ " </screen>\n"
+ " <screen name=\"Screen2\" exitscreen=\"true\">\n"
+ " <description>\n"
+ " <oia status=\"NOTINHIBITED\" />\n"
+ " </description>\n"
+ " <actions>\n"
+ " <message title=\"Done\" value=\"Success!\" />\n"
+ " </actions>\n"
+ " </screen>\n"
+ "</HASCRIPT>";
MacroScreens screens = MacroXmlParser.parse(xml);
assertNotNull(screens);
assertEquals("TestMacro", screens.getName());
assertEquals("A unit test macro", screens.getDescription());
assertEquals(8000, screens.getTimeout());
assertEquals(200, screens.getPauseTime());
assertTrue(screens.isUseVars());
// Check variables
MacroVariables vars = screens.getVariables();
assertNotNull(vars);
assertEquals("12345", vars.getString("$acct"));
assertEquals(0, vars.getInt("$retry"));
// Check screens
assertEquals(2, screens.size());
MacroScreen s1 = screens.getByName("Screen1");
assertNotNull(s1);
assertTrue(s1.isEntryScreen());
assertEquals(150, s1.getPauseTime());
assertEquals(3, s1.getRecoLimit());
assertEquals("ScreenError", s1.getRecoGoto());
// Descriptors
assertEquals(2, s1.getDescription().size());
// Actions
assertEquals(2, s1.getActions().size());
assertTrue(s1.getActions().get(0) instanceof MacroActionInput);
MacroActionInput inputAct = (MacroActionInput) s1.getActions().get(0);
assertEquals("$acct + '[enter]'", inputAct.getValue());
assertEquals(5, inputAct.getRow());
assertEquals(10, inputAct.getCol());
assertTrue(s1.getActions().get(1) instanceof MacroActionVarUpdate);
MacroActionVarUpdate vuAct = (MacroActionVarUpdate) s1.getActions().get(1);
assertEquals("$retry", vuAct.getName());
assertEquals("$retry + 1", vuAct.getValue());
// Next screens
assertEquals(1, s1.getNextScreens().size());
assertTrue(s1.getNextScreens().containsName("Screen2"));
assertEquals(5000, s1.getNextScreens().getTimeout());
// Screen 2
MacroScreen s2 = screens.getByName("Screen2");
assertNotNull(s2);
assertTrue(s2.isExitScreen());
assertEquals(1, s2.getActions().size());
assertTrue(s2.getActions().get(0) instanceof MacroActionMessage);
// Roundtrip serialization
String formatted = screens.format();
assertTrue(formatted.contains("<HASCRIPT"));
assertTrue(formatted.contains("name=\"TestMacro\""));
assertTrue(formatted.contains("<screen name=\"Screen1\""));
assertTrue(formatted.contains("<actions>"));
MacroScreens reparsed = MacroXmlParser.parse(formatted);
assertEquals("TestMacro", reparsed.getName());
assertEquals(2, reparsed.size());
}
@Test
@DisplayName("24.3: PlayThread finite state machine loop execution")
public void testPlayThreadStateMachineExecution() throws Exception {
// Setup initial screen in PS
ps.SetText("TSO/E LOGON", 1, 35);
ps.SetCursorPos(5, 10); // row 5, col 10 (1-based)
String xml = "<HASCRIPT name=\"FlowTest\" timeout=\"5000\" pausetime=\"50\" usevars=\"true\">\n"
+ " <vars>\n"
+ " <create name=\"$step\" type=\"integer\" value=\"0\" />\n"
+ " <create name=\"$extracted\" type=\"string\" value=\"\" />\n"
+ " </vars>\n"
+ " <screen name=\"Logon\" entryscreen=\"true\">\n"
+ " <description>\n"
+ " <string value=\"TSO/E LOGON\" row=\"1\" col=\"35\" />\n"
+ " </description>\n"
+ " <actions>\n"
+ " <varupdate name=\"$step\" value=\"1\" />\n"
+ " <input value=\"USER01[enter]\" />\n"
+ " </actions>\n"
+ " <nextscreens timeout=\"3000\">\n"
+ " <nextscreen name=\"Welcome\" />\n"
+ " </nextscreens>\n"
+ " </screen>\n"
+ " <screen name=\"Welcome\" exitscreen=\"true\">\n"
+ " <description>\n"
+ " <string value=\"READY TO WORK\" row=\"2\" col=\"10\" />\n"
+ " </description>\n"
+ " <actions>\n"
+ " <extract row=\"2\" col=\"10\" erow=\"2\" ecol=\"22\" variable=\"$extracted\" />\n"
+ " <varupdate name=\"$step\" value=\"2\" />\n"
+ " </actions>\n"
+ " </screen>\n"
+ "</HASCRIPT>";
Macro macro = new Macro(session);
macro.load(xml);
AtomicReference<String> matched = new AtomicReference<>("");
macro.addMacroListener(new Macro.MacroListener() {
@Override public void macroStateChanged(Macro m, int oldState, int newState) {}
@Override public void macroScreenMatching(MacroScreen screen) { matched.set(screen.getName()); }
@Override public void macroActionExecuting(MacroAction action) {}
@Override public void macroMessage(String title, String message) {}
@Override public void macroError(String error) {}
});
// Launch macro in background thread
macro.play();
// Wait for Screen1 to match and execute actions
Thread.sleep(200);
assertEquals(1, macro.getVariables().getInt("$step"));
assertEquals("Logon", matched.get());
// Now simulate host transition to second screen
ps.SetText("READY TO WORK", 2, 10);
// Wait for Welcome screen to match and complete
long start = System.currentTimeMillis();
while (macro.getState() == Macro.STATE_PLAYING && System.currentTimeMillis() - start < 2000) {
Thread.sleep(50);
}
assertEquals(Macro.STATE_STOPPED, macro.getState());
assertEquals("Welcome", matched.get());
assertEquals(2, macro.getVariables().getInt("$step"));
assertEquals("READY TO WORK", macro.getVariables().getString("$extracted").trim());
}
@Test
@DisplayName("24.3: MacroActionIf conditional branching during macro execution")
public void testMacroActionIfBranching() {
Macro macro = new Macro(session);
MacroVariables vars = new MacroVariables();
vars.createVariable("$flag", "boolean", "true");
vars.createVariable("$result", "string", "");
macro.getScreens().setVariables(vars);
macro.getScreens().setUseVars(true);
MacroActionIf ifAct = new MacroActionIf("$flag == true");
ifAct.addIfAction(new MacroActionVarUpdate("$result", "'THEN_BRANCH'"));
ifAct.addElseAction(new MacroActionVarUpdate("$result", "'ELSE_BRANCH'"));
ifAct.execute(macro, session);
assertEquals("THEN_BRANCH", vars.getString("$result"));
// Switch flag and re-execute
vars.setVariable("$flag", false);
ifAct.execute(macro, session);
assertEquals("ELSE_BRANCH", vars.getString("$result"));
}
@Test
@DisplayName("24.3: MacroActionPrompt modal input provider")
public void testMacroActionPrompt() {
Macro macro = new Macro(session);
MacroVariables vars = new MacroVariables();
vars.createVariable("$secret", "string", "");
macro.getScreens().setVariables(vars);
macro.getScreens().setUseVars(true);
// Set mock prompt handler
macro.setPromptHandler(prompt -> {
assertEquals("Password Prompt", prompt.getTitle());
return "Pass123";
});
MacroActionPrompt promptAct = new MacroActionPrompt();
promptAct.setTitle("Password Prompt");
promptAct.setDescription("Enter Password:");
promptAct.setEncrypted(true);
promptAct.setVariable("$secret");
promptAct.setDontSend(true); // Don't write to PS, only store variable
promptAct.execute(macro, session);
assertEquals("Pass123", vars.getString("$secret"));
}
@Test
@DisplayName("24.1 & 24.2: com.ibm.eNetwork HOD facades compatibility")
public void testHodFacades() {
com.ibm.eNetwork.beans.HOD.Macro hodMacro = new com.ibm.eNetwork.beans.HOD.Macro(session);
com.ibm.eNetwork.beans.HOD.MacroScreens hodScreens = new com.ibm.eNetwork.beans.HOD.MacroScreens();
com.ibm.eNetwork.beans.HOD.MacroScreen hodScreen = new com.ibm.eNetwork.beans.HOD.MacroScreen("TestScreen");
hodScreens.add(hodScreen);
hodMacro.setScreens(hodScreens);
assertEquals(1, hodMacro.getScreens().size());
assertEquals("TestScreen", hodMacro.getScreens().get(0).getName());
com.ibm.eNetwork.ECL.macrovariable.MacroVariables hodVars = new com.ibm.eNetwork.ECL.macrovariable.MacroVariables();
hodVars.createVariable("$test", "integer", "42");
assertEquals(42, hodVars.getInt("$test"));
int res = com.ibm.eNetwork.ECL.macrovariable.MacroExpressionParser.evaluateInt(hodVars, "$test * 2");
assertEquals(84, res);
}
}
@@ -0,0 +1,160 @@
package haus.nightmare.lib3270j.telnet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import haus.nightmare.lib3270j.protocol.TelnetConstants;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
/**
* Tests verifying proper transmission of the ATTN key in accordance with
* RFC 2355, RFC 1576, and IBM Host On-Demand parity.
*/
public class AttnTransmissionTest {
private ConnectionConfig config;
private ScreenBuffer screenBuffer;
private DataStreamProcessor dsProcessor;
private InputProcessor inputProcessor;
private TelnetFSM fsm;
private MockConnection connection;
private EbcdicTranslator translator;
private static class MockConnection extends TelnetConnection {
final List<byte[]> sentData = new ArrayList<>();
boolean connected = true;
MockConnection(ConnectionConfig config, TelnetFSM fsm) {
super(config, fsm);
}
@Override
public boolean isConnected() {
return connected;
}
@Override
public synchronized void sendRaw(byte[] data) {
sentData.add(data.clone());
}
@Override
public synchronized void sendRaw(byte[] data, int offset, int length) {
byte[] b = new byte[length];
System.arraycopy(data, offset, b, 0, length);
sentData.add(b);
}
}
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
dsProcessor = new DataStreamProcessor(screenBuffer, translator);
fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
dsProcessor.setInputProcessor(inputProcessor);
connection = new MockConnection(config, fsm);
fsm.setConnection(connection);
}
@Test
public void testAttnInTn3270eBoundModeSendsRawIacIp() {
fsm.setConnectionState(ConnectionState.CONNECTED_TN3270E);
fsm.setTn3270eNegotiated(true);
connection.sentData.clear();
inputProcessor.processAttn();
// Must send exactly 1 packet with 2 bytes: IAC IP (0xFF, 0xF4)
// Must NOT escape IAC into IAC IAC (0xFF 0xFF) or add IAC EOR (0xFF 0xEF)
assertEquals(1, connection.sentData.size(), "Should have transmitted exactly one packet");
byte[] packet = connection.sentData.get(0);
assertEquals(2, packet.length, "ATTN packet in TN3270E bound mode must be exactly 2 bytes (IAC IP)");
assertArrayEquals(new byte[] { (byte) IAC, (byte) IP }, packet);
}
@Test
public void testAttnInStandardTn3270ModeSendsPa1AndBreak() {
fsm.setConnectionState(ConnectionState.CONNECTED_3270);
fsm.setTn3270eNegotiated(false);
connection.sentData.clear();
inputProcessor.processAttn();
// HoD parity: In standard TN3270 mode, sends PA1 record + IAC BREAK:
// { 108, -1, -17, -1, -13 } -> { 0x6C, IAC, EOR, IAC, BREAK }
assertEquals(1, connection.sentData.size(), "Should have transmitted exactly one packet");
byte[] packet = connection.sentData.get(0);
assertEquals(5, packet.length, "ATTN packet in TN3270 mode must be 5 bytes (PA1 + IAC EOR + IAC BREAK)");
assertArrayEquals(new byte[] {
(byte) DS3270Constants.AID_PA1,
(byte) IAC, (byte) EOR,
(byte) IAC, (byte) BREAK
}, packet);
}
@Test
public void testAttnInSscpModeDoesNotTransmitAndResetsState() {
fsm.setConnectionState(ConnectionState.CONNECTED_SSCP);
fsm.setTn3270eNegotiated(true);
inputProcessor.setKeyboardLocked(true);
connection.sentData.clear();
inputProcessor.processAttn();
// In SSCP-LU mode, host does not receive ATTN; client resets and unlocks
assertTrue(connection.sentData.isEmpty(), "ATTN in SSCP-LU mode must not transmit to host");
assertFalse(inputProcessor.isKeyboardLocked(), "Keyboard should be unlocked in SSCP-LU mode on ATTN");
}
@Test
public void testAttnInNvtModeSendsRawIacIp() {
fsm.setConnectionState(ConnectionState.CONNECTED_NVT);
connection.sentData.clear();
inputProcessor.processAttn();
assertEquals(1, connection.sentData.size(), "Should transmit one packet in NVT mode");
byte[] packet = connection.sentData.get(0);
assertArrayEquals(new byte[] { (byte) IAC, (byte) IP }, packet);
}
@Test
public void testAttnResetsInsertModeIfInsertOffOnAid() {
fsm.setConnectionState(ConnectionState.CONNECTED_TN3270E);
fsm.setTn3270eNegotiated(true);
inputProcessor.setInsertOffOnAid(true);
inputProcessor.setInsertMode(true);
assertTrue(inputProcessor.isInsertMode());
inputProcessor.processAttn();
assertFalse(inputProcessor.isInsertMode(), "ATTN should reset insert mode when insertOffOnAid is enabled");
}
@Test
public void testDirectFsmHandleAttn() {
fsm.setConnectionState(ConnectionState.CONNECTED_TN3270E);
fsm.setTn3270eNegotiated(true);
connection.sentData.clear();
fsm.processAttn();
assertEquals(1, connection.sentData.size());
assertArrayEquals(new byte[] { (byte) IAC, (byte) IP }, connection.sentData.get(0));
}
}