Bugfixes from zOS testing
This commit is contained in:
+476
@@ -0,0 +1,476 @@
|
||||
package haus.nightmare.lib3270j.ft;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.CodePageRegistry;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import static haus.nightmare.lib3270j.ft.FTConstants.*;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Phase 16 Test Suite: IND$FILE Host File Transfer Protocol Hardening.
|
||||
* Validates 1:1 packet, timing, and record-layout compatibility with IBM Host On-Demand v14.
|
||||
*/
|
||||
public class FTProtocolHardeningPhase16Test {
|
||||
|
||||
private EbcdicTranslator translator;
|
||||
private ScreenBuffer screen;
|
||||
private MockInputProcessor input;
|
||||
private MockDftListener listener;
|
||||
private FTDft ftDft;
|
||||
|
||||
static class MockInputProcessor extends InputProcessor {
|
||||
final List<byte[]> sentStructuredFields = new ArrayList<>();
|
||||
|
||||
public MockInputProcessor(ScreenBuffer screen, EbcdicTranslator translator) {
|
||||
super(screen, translator, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendStructuredFieldData(byte[] data) {
|
||||
sentStructuredFields.add(data.clone());
|
||||
}
|
||||
}
|
||||
|
||||
static class MockDftListener implements FTDft.FTDftListener {
|
||||
FTConfig config = new FTConfig();
|
||||
FTState state = FTState.RUNNING;
|
||||
long bytesTransferred = 0;
|
||||
boolean completeCalled = false;
|
||||
boolean abortCalled = false;
|
||||
String abortMessage = null;
|
||||
|
||||
@Override
|
||||
public void onDftRunning() {
|
||||
state = FTState.RUNNING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransferComplete(String errorMessage) {
|
||||
state = FTState.NONE;
|
||||
completeCalled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransferAborted(String errorMessage) {
|
||||
state = FTState.NONE;
|
||||
abortCalled = true;
|
||||
abortMessage = errorMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBytesTransferred(long bytes) {
|
||||
this.bytesTransferred = bytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FTState getCurrentState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setState(FTState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FTConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getLocalFile() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||
input = new MockInputProcessor(screen, translator);
|
||||
listener = new MockDftListener();
|
||||
ftDft = new FTDft(input, translator, listener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHoDBinaryPacketTemplates() {
|
||||
// 1. RB_OPEN_ACK: 8 bytes matching HoD Xfer3270 lines 553-561
|
||||
byte[] expectedOpenAck = {
|
||||
(byte) 0x88, 0x00, 0x05, (byte) 0xD0, 0x00, 0x09, (byte) 0xFF, (byte) 0xEF
|
||||
};
|
||||
assertArrayEquals(expectedOpenAck, RB_OPEN_ACK);
|
||||
|
||||
// 2. RB_CLOSE_ACK: 8 bytes matching HoD Xfer3270 lines 578-586
|
||||
byte[] expectedCloseAck = {
|
||||
(byte) 0x88, 0x00, 0x05, (byte) 0xD0, 0x41, 0x09, (byte) 0xFF, (byte) 0xEF
|
||||
};
|
||||
assertArrayEquals(expectedCloseAck, RB_CLOSE_ACK);
|
||||
|
||||
// 3. RB_GET_PAST_EOF: 12 bytes matching HoD Xfer3270 lines 795-806
|
||||
byte[] expectedEof = {
|
||||
(byte) 0x88, 0x00, 0x09, (byte) 0xD0, 0x46, 0x08, 0x69, 0x04, 0x22, 0x00, (byte) 0xFF, (byte) 0xEF
|
||||
};
|
||||
assertArrayEquals(expectedEof, RB_GET_PAST_EOF);
|
||||
|
||||
// 4. RB_OPEN_FAILED: 11 bytes matching HoD Xfer3270 lines 588-599
|
||||
byte[] expectedOpenFailed = {
|
||||
0x00, 0x09, (byte) 0xD0, 0x00, 0x08, 0x69, 0x04, 0x01, 0x00, (byte) 0xFF, (byte) 0xEF
|
||||
};
|
||||
assertArrayEquals(expectedOpenFailed, RB_OPEN_FAILED);
|
||||
|
||||
// 5. createDataAck(recnum): 14 bytes matching HoD Xfer3270 lines 563-576 + doubleFoxFox
|
||||
byte[] ack1 = createDataAck(1);
|
||||
assertEquals(14, ack1.length);
|
||||
assertEquals((byte) 0x88, ack1[0]);
|
||||
assertEquals(0x00, ack1[1]);
|
||||
assertEquals(0x0B, ack1[2]);
|
||||
assertEquals((byte) 0xD0, ack1[3]);
|
||||
assertEquals(0x47, ack1[4]);
|
||||
assertEquals(0x05, ack1[5]);
|
||||
assertEquals(0x63, ack1[6]);
|
||||
assertEquals(0x06, ack1[7]);
|
||||
assertEquals(0x00, ack1[8]);
|
||||
assertEquals(0x00, ack1[9]);
|
||||
assertEquals(0x00, ack1[10]);
|
||||
assertEquals(0x01, ack1[11]);
|
||||
assertEquals((byte) 0xFF, ack1[12]);
|
||||
assertEquals((byte) 0xEF, ack1[13]);
|
||||
|
||||
byte[] ackLarge = createDataAck(0x12345678L);
|
||||
assertEquals(0x12, ackLarge[8] & 0xFF);
|
||||
assertEquals(0x34, ackLarge[9] & 0xFF);
|
||||
assertEquals(0x56, ackLarge[10] & 0xFF);
|
||||
assertEquals(0x78, ackLarge[11] & 0xFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOpenRequestSendsExactHoDOpenAck() {
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ftDft.initTransfer(in, out);
|
||||
|
||||
// Build Open Request SF
|
||||
byte[] openSf = {
|
||||
0x00, 0x05, (byte) 0xD0, 0x00, 0x12
|
||||
};
|
||||
|
||||
ftDft.processStructuredField(openSf, 0, openSf.length);
|
||||
|
||||
assertEquals(1, input.sentStructuredFields.size());
|
||||
assertArrayEquals(RB_OPEN_ACK, input.sentStructuredFields.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCloseRequestFlushesAndSendsExactHoDCloseAck() {
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ftDft.initTransfer(in, out);
|
||||
|
||||
byte[] closeSf = {
|
||||
0x00, 0x05, (byte) 0xD0, 0x41, 0x12
|
||||
};
|
||||
|
||||
ftDft.processStructuredField(closeSf, 0, closeSf.length);
|
||||
|
||||
assertEquals(1, input.sentStructuredFields.size());
|
||||
assertArrayEquals(RB_CLOSE_ACK, input.sentStructuredFields.get(0));
|
||||
assertTrue(listener.completeCalled);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUploadEofSendsExactHoDGetPastEof() {
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ftDft.initTransfer(in, out);
|
||||
|
||||
// Host sends TR_GET_REQ (0x4611)
|
||||
byte[] getSf = {
|
||||
0x00, 0x05, (byte) 0xD0, 0x46, 0x11
|
||||
};
|
||||
|
||||
ftDft.processStructuredField(getSf, 0, getSf.length);
|
||||
|
||||
assertEquals(1, input.sentStructuredFields.size());
|
||||
assertArrayEquals(RB_GET_PAST_EOF, input.sentStructuredFields.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDataAckLayoutOnDownload() {
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ftDft.initTransfer(in, out);
|
||||
|
||||
// Host sends Data Insert SF with 4 data bytes
|
||||
byte[] dataInsert = {
|
||||
0x00, 0x0C, (byte) 0xD0, 0x47, 0x04,
|
||||
0x61, 0x00, 0x09, // TR_BEGIN_DATA + len (9 = 4 data + 5)
|
||||
'T', 'E', 'S', 'T'
|
||||
};
|
||||
|
||||
ftDft.processStructuredField(dataInsert, 0, dataInsert.length);
|
||||
|
||||
assertEquals(1, input.sentStructuredFields.size());
|
||||
byte[] expectedAck = createDataAck(1);
|
||||
assertArrayEquals(expectedAck, input.sentStructuredFields.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHostErrorRepliesMappedToDescriptions() {
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ftDft.initTransfer(in, out);
|
||||
|
||||
// 1. TR_ERR_OPEN_FAILED (0x0100)
|
||||
byte[] errOpenFailed = {
|
||||
0x00, 0x09, (byte) 0xD0, 0x00, 0x08, 0x69, 0x04, 0x01, 0x00
|
||||
};
|
||||
ftDft.processStructuredField(errOpenFailed, 0, errOpenFailed.length);
|
||||
assertTrue(listener.abortCalled);
|
||||
assertTrue(listener.abortMessage.toLowerCase().contains("file not found") ||
|
||||
listener.abortMessage.toLowerCase().contains("open failed"));
|
||||
|
||||
// Reset listener
|
||||
listener.abortCalled = false;
|
||||
listener.abortMessage = null;
|
||||
|
||||
// 2. TR_ERR_NOT_AUTHORIZED (0x3E00)
|
||||
byte[] errNotAuth = {
|
||||
0x00, 0x09, (byte) 0xD0, 0x00, 0x08, 0x69, 0x04, 0x3E, 0x00
|
||||
};
|
||||
ftDft.processStructuredField(errNotAuth, 0, errNotAuth.length);
|
||||
assertTrue(listener.abortCalled);
|
||||
assertTrue(listener.abortMessage.toLowerCase().contains("not authorized") ||
|
||||
listener.abortMessage.toLowerCase().contains("security"));
|
||||
|
||||
// 3. TR_ERR_SYNTAX_ERROR (0x6000)
|
||||
listener.abortCalled = false;
|
||||
byte[] errSyntax = {
|
||||
0x00, 0x09, (byte) 0xD0, 0x00, 0x08, 0x69, 0x04, 0x60, 0x00
|
||||
};
|
||||
ftDft.processStructuredField(errSyntax, 0, errSyntax.length);
|
||||
assertTrue(listener.abortCalled);
|
||||
assertTrue(listener.abortMessage.toLowerCase().contains("syntax error") ||
|
||||
listener.abortMessage.toLowerCase().contains("options"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStreamNameDetectionAsciiAndEbcdic() {
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ftDft.initTransfer(in, out);
|
||||
|
||||
// Open request containing EBCDIC FT:MSG
|
||||
ByteArrayOutputStream sfMsg = new ByteArrayOutputStream();
|
||||
sfMsg.write(0x00); sfMsg.write(0x10);
|
||||
sfMsg.write(SF_TRANSFER_DATA);
|
||||
sfMsg.write(0x00); sfMsg.write(0x12);
|
||||
sfMsg.write(STREAM_NAME_FT_MSG_BYTES_EBCDIC, 0, STREAM_NAME_FT_MSG_BYTES_EBCDIC.length);
|
||||
|
||||
byte[] msgPacket = sfMsg.toByteArray();
|
||||
msgPacket[0] = (byte) ((msgPacket.length >> 8) & 0xFF);
|
||||
msgPacket[1] = (byte) (msgPacket.length & 0xFF);
|
||||
|
||||
ftDft.processStructuredField(msgPacket, 0, msgPacket.length);
|
||||
assertEquals(FTState.RUNNING, listener.state);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecfmFPaddingAndSplittingUpload() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setDirection(FTConfig.Direction.SEND);
|
||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
config.setRecfm(FTConfig.RecordFormat.FIXED);
|
||||
config.setLrecl("10");
|
||||
listener.config = config;
|
||||
|
||||
// Local content has two lines: "SHORT" (5 chars) and "LONGER LINE THAN TEN" (20 chars)
|
||||
String localContent = "SHORT\nLONGER LINE THAN TEN\n";
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(localContent.getBytes(StandardCharsets.UTF_8));
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ftDft.initTransfer(in, out);
|
||||
|
||||
// Host sends TR_GET_REQ
|
||||
byte[] getSf = { 0x00, 0x05, (byte) 0xD0, 0x46, 0x11 };
|
||||
ftDft.processStructuredField(getSf, 0, getSf.length);
|
||||
|
||||
assertEquals(1, input.sentStructuredFields.size());
|
||||
byte[] reply = input.sentStructuredFields.get(0);
|
||||
int headerLen = 17;
|
||||
int dataLen = reply.length - headerLen;
|
||||
assertTrue(dataLen >= 30, "Should have packed at least 3 fixed records of 10 bytes each");
|
||||
|
||||
String rec1 = new String(reply, headerLen, 10, StandardCharsets.UTF_8);
|
||||
assertEquals("SHORT ", rec1);
|
||||
|
||||
String rec2 = new String(reply, headerLen + 10, 10, StandardCharsets.UTF_8);
|
||||
assertEquals("LONGER LIN", rec2);
|
||||
|
||||
String rec3 = new String(reply, headerLen + 20, 10, StandardCharsets.UTF_8);
|
||||
assertEquals("E THAN TEN", rec3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecfmFDownloadTrimmingAndNewline() throws Exception {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setDirection(FTConfig.Direction.RECEIVE);
|
||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
config.setRecfm(FTConfig.RecordFormat.FIXED);
|
||||
config.setLrecl("10");
|
||||
listener.config = config;
|
||||
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ftDft.initTransfer(in, out);
|
||||
|
||||
// Host sends 10-byte fixed record "HELLO "
|
||||
byte[] hostRecord = "HELLO ".getBytes(StandardCharsets.UTF_8);
|
||||
ByteArrayOutputStream sf = new ByteArrayOutputStream();
|
||||
sf.write(0x00); sf.write(hostRecord.length + 8);
|
||||
sf.write(SF_TRANSFER_DATA);
|
||||
sf.write(0x47); sf.write(0x04);
|
||||
sf.write(TR_BEGIN_DATA);
|
||||
int fieldLen = hostRecord.length + 5;
|
||||
sf.write((fieldLen >> 8) & 0xFF); sf.write(fieldLen & 0xFF);
|
||||
sf.write(hostRecord);
|
||||
|
||||
byte[] pkt = sf.toByteArray();
|
||||
pkt[0] = (byte) ((pkt.length >> 8) & 0xFF);
|
||||
pkt[1] = (byte) (pkt.length & 0xFF);
|
||||
|
||||
ftDft.processStructuredField(pkt, 0, pkt.length);
|
||||
|
||||
// Verify output has "HELLO" stripped of trailing spaces plus local line separator
|
||||
String result = new String(out.toByteArray(), StandardCharsets.UTF_8);
|
||||
assertEquals("HELLO" + System.lineSeparator(), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecfmVRdwUploadAndDownload() throws Exception {
|
||||
// 1. Upload RECFM V
|
||||
FTConfig sendConfig = new FTConfig();
|
||||
sendConfig.setDirection(FTConfig.Direction.SEND);
|
||||
sendConfig.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
sendConfig.setRecfm(FTConfig.RecordFormat.VARIABLE);
|
||||
listener.config = sendConfig;
|
||||
|
||||
String testLine = "VARIABLE RECORD TEST";
|
||||
ByteArrayInputStream in = new ByteArrayInputStream((testLine + "\n").getBytes(StandardCharsets.UTF_8));
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ftDft.initTransfer(in, out);
|
||||
|
||||
byte[] getSf = { 0x00, 0x05, (byte) 0xD0, 0x46, 0x11 };
|
||||
ftDft.processStructuredField(getSf, 0, getSf.length);
|
||||
|
||||
assertEquals(1, input.sentStructuredFields.size());
|
||||
byte[] reply = input.sentStructuredFields.get(0);
|
||||
int headerLen = 17;
|
||||
|
||||
// Verify 4-byte RDW header: length should be testLine.length() + 4
|
||||
int rdwLen = ((reply[headerLen] & 0xFF) << 8) | (reply[headerLen + 1] & 0xFF);
|
||||
assertEquals(testLine.length() + 4, rdwLen);
|
||||
assertEquals(0, reply[headerLen + 2]);
|
||||
assertEquals(0, reply[headerLen + 3]);
|
||||
String payload = new String(reply, headerLen + 4, testLine.length(), StandardCharsets.UTF_8);
|
||||
assertEquals(testLine, payload);
|
||||
|
||||
// 2. Download RECFM V
|
||||
FTConfig recvConfig = new FTConfig();
|
||||
recvConfig.setDirection(FTConfig.Direction.RECEIVE);
|
||||
recvConfig.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
recvConfig.setRecfm(FTConfig.RecordFormat.VARIABLE);
|
||||
listener.config = recvConfig;
|
||||
|
||||
ByteArrayOutputStream downOut = new ByteArrayOutputStream();
|
||||
ftDft.initTransfer(new ByteArrayInputStream(new byte[0]), downOut);
|
||||
|
||||
// Host sends RDW header + "TEST V DATA"
|
||||
byte[] vData = "TEST V DATA".getBytes(StandardCharsets.UTF_8);
|
||||
int vRdwLen = vData.length + 4;
|
||||
ByteArrayOutputStream sfDown = new ByteArrayOutputStream();
|
||||
sfDown.write(0x00); sfDown.write(0x00);
|
||||
sfDown.write(SF_TRANSFER_DATA);
|
||||
sfDown.write(0x47); sfDown.write(0x04);
|
||||
sfDown.write(TR_BEGIN_DATA);
|
||||
int fLen = vRdwLen + 5;
|
||||
sfDown.write((fLen >> 8) & 0xFF); sfDown.write(fLen & 0xFF);
|
||||
sfDown.write((vRdwLen >> 8) & 0xFF); sfDown.write(vRdwLen & 0xFF);
|
||||
sfDown.write(0x00); sfDown.write(0x00);
|
||||
sfDown.write(vData);
|
||||
|
||||
byte[] downPkt = sfDown.toByteArray();
|
||||
downPkt[0] = (byte) ((downPkt.length >> 8) & 0xFF);
|
||||
downPkt[1] = (byte) (downPkt.length & 0xFF);
|
||||
|
||||
ftDft.processStructuredField(downPkt, 0, downPkt.length);
|
||||
|
||||
String downResult = new String(downOut.toByteArray(), StandardCharsets.UTF_8);
|
||||
assertEquals("TEST V DATA" + System.lineSeparator(), downResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDbcsProgramSelectionAndOptions() {
|
||||
FTConfig cfgDbcs = new FTConfig();
|
||||
cfgDbcs.setCodePage("930"); // Japanese DBCS
|
||||
assertEquals("APVUFILE", cfgDbcs.getProgramName());
|
||||
|
||||
FTConfig cfgSbcs = new FTConfig();
|
||||
cfgSbcs.setCodePage("037"); // US English SBCS
|
||||
assertEquals("IND$FILE", cfgSbcs.getProgramName());
|
||||
|
||||
// Option NEW and NOSO parsing
|
||||
FTConfig optionsCfg = new FTConfig();
|
||||
optionsCfg.setHostType(FTConfig.HostType.TSO);
|
||||
optionsCfg.setDirection(FTConfig.Direction.SEND);
|
||||
optionsCfg.setHostFilename("'TEST.FILE'");
|
||||
optionsCfg.parseOptions("NEW NOSO APVUFILE");
|
||||
|
||||
assertTrue(optionsCfg.isOptionNew());
|
||||
assertTrue(optionsCfg.isOptionNoso());
|
||||
assertEquals("APVUFILE", optionsCfg.getProgramName());
|
||||
|
||||
String cmd = optionsCfg.buildCommand();
|
||||
assertTrue(cmd.startsWith("APVUFILE PUT 'TEST.FILE'"));
|
||||
assertTrue(cmd.contains("NEW"));
|
||||
assertTrue(cmd.contains("NOSO"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCutErrorScreenScanning() {
|
||||
FTCut cut = new FTCut(screen, input, translator, new FTCut.FTCutListener() {
|
||||
@Override public void onCutRunning() {}
|
||||
@Override public void onTransferComplete(String err) {}
|
||||
@Override public void onTransferAborted(String err) {}
|
||||
@Override public void onBytesTransferred(long b) {}
|
||||
@Override public FTConstants.FTState getCurrentState() { return FTConstants.FTState.AWAIT_ACK; }
|
||||
@Override public void setState(FTConstants.FTState s) {}
|
||||
@Override public FTConfig getConfig() { return new FTConfig(); }
|
||||
@Override public File getLocalFile() { return null; }
|
||||
});
|
||||
|
||||
// Write an error line on row 2 of screen
|
||||
String errLine = "IKJ56700I DATASET 'NONEXIST.FILE' NOT FOUND";
|
||||
int startPos = 80; // row 2, col 1
|
||||
for (int i = 0; i < errLine.length(); i++) {
|
||||
screen.setCell(startPos + i, translator.unicodeToEbcdic(errLine.charAt(i)));
|
||||
}
|
||||
|
||||
String detected = cut.scanScreenForErrors();
|
||||
assertNotNull(detected);
|
||||
assertTrue(detected.contains("IKJ56700I"));
|
||||
assertTrue(detected.contains("NOT FOUND"));
|
||||
}
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.protocol.DS3270Constants;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Targeted verification tests for Phase 17:
|
||||
* - Multi-polygon area fills with boundary enforcement & winding/even-odd rules
|
||||
* - GFARC/GCFARC full arc & ellipse generation with HODEllipse Bresenham tables
|
||||
* - GARC/GCARC 3-point circular arc interpolation
|
||||
* - FilletPts polynomial interpolation (HoD getFilletPoints) and area accumulation
|
||||
* - GRLINE/GCRLIN relative lines with signed 8-bit deltas
|
||||
* - GSAP (P/Q/R/S) aspect ratio and coordinate transformations
|
||||
* - GBIMG image multiplier (imult) support
|
||||
* - 56-byte Graphic Input structured field layout and sendGraphicMouseAid dispatch
|
||||
*/
|
||||
public class Phase17GocaGraphicsParityTest {
|
||||
|
||||
@Test
|
||||
public void testMultiPolygonAreaFillWithBoundaryAndFillRules() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// 1. Begin Area with boundary flag set (0x40) and Winding rule (0x20)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GSCOL); out.write(0x02); // Red
|
||||
out.write(GocaConstants.G_GSLW); out.write(0x02); // Line width 2
|
||||
out.write(GocaConstants.G_GBAR); out.write(0x40 | 0x20); // flags: drawBoundary=true, winding=true
|
||||
|
||||
// First polygon: Triangle
|
||||
out.write(GocaConstants.G_GLINE); out.write(12);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(50); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(30); out.write(0x00); out.write(50);
|
||||
|
||||
// End Area
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeGoca(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent(), "GraphicsPlane must have content after area fill");
|
||||
assertTrue(decoder.hasActiveGraphics(), "Decoder must report active graphics when plane has content");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHODEllipseAndFullArcGeneration() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// Set GSAP aspect ratio / semi-axes: P=20, Q=1, R=10, S=1
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GSAP); out.write(8);
|
||||
out.write(0x00); out.write(20); // P
|
||||
out.write(0x00); out.write(1); // Q
|
||||
out.write(0x00); out.write(10); // R
|
||||
out.write(0x00); out.write(1); // S
|
||||
|
||||
// Full Arc Absolute (0xC7): Center=(100, 100), Multiplier=1.0 (1, 0)
|
||||
out.write(GocaConstants.G_GFARC); out.write(6);
|
||||
out.write(0x00); out.write(100); // centerX
|
||||
out.write(0x00); out.write(100); // centerY
|
||||
out.write(0x01); out.write(0x00); // Multiplier 1.00
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeGoca(stream, 0, stream.length);
|
||||
|
||||
assertEquals(20, decoder.getArcParamP());
|
||||
assertEquals(1, decoder.getArcParamQ());
|
||||
assertEquals(10, decoder.getArcParamR());
|
||||
assertEquals(1, decoder.getArcParamS());
|
||||
assertTrue(plane.hasContent(), "Full arc must render on graphics plane");
|
||||
|
||||
// Verify direct HODEllipse Bresenham generation
|
||||
int[] ptsX = new int[256];
|
||||
int[] ptsY = new int[256];
|
||||
int nPts = HODEllipse.getHODEllipse(20.0, 1.0, 10.0, 1.0, ptsX, ptsY, 50, 50);
|
||||
assertTrue(nPts > 8, "HODEllipse must generate valid polygon vertices");
|
||||
assertEquals(ptsX[0], ptsX[nPts - 1], "HODEllipse polygon must close to initial vertex");
|
||||
assertEquals(ptsY[0], ptsY[nPts - 1], "HODEllipse polygon must close to initial vertex");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThreePointCircularArcInterpolation() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// Current Position Arc (GCARC 0x86):
|
||||
// Start at (0, 0), intermediate at (50, 50), end at (100, 0) -> upper semicircle
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GSCP); out.write(4);
|
||||
out.write(0x00); out.write(0);
|
||||
out.write(0x00); out.write(0);
|
||||
|
||||
out.write(GocaConstants.G_GCARC); out.write(8);
|
||||
out.write(0x00); out.write(50); // intermediate X
|
||||
out.write(0x00); out.write(50); // intermediate Y
|
||||
out.write(0x00); out.write(100); // end X
|
||||
out.write(0x00); out.write(0); // end Y
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeGoca(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent(), "3-point circular arc must render on graphics plane");
|
||||
assertEquals(100, decoder.getCurX(), "Terminal X must update to arc end point");
|
||||
assertEquals(0, decoder.getCurY(), "Terminal Y must update to arc end point");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilletPolynomialInterpolationAndAreaAccumulation() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// Direct test of FilletPts polynomial interpolation
|
||||
FilletPts fillet = new FilletPts();
|
||||
int[] ctrlX = new int[] { 10, 50, 90 };
|
||||
int[] ctrlY = new int[] { 10, 80, 10 };
|
||||
int req = fillet.getPointsRequired(3);
|
||||
int[] outX = new int[req];
|
||||
int[] outY = new int[req];
|
||||
int nPts = fillet.getFilletPoints(ctrlX, ctrlY, 3, outX, outY);
|
||||
assertTrue(nPts >= 3, "FilletPts must interpolate curve points across control vertices");
|
||||
|
||||
// GFLT (0xC5 Fillet Absolute) inside an Area
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GBAR); out.write(0x40); // drawBoundary=true
|
||||
out.write(GocaConstants.G_GFLT); out.write(12);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(50); out.write(0x00); out.write(80);
|
||||
out.write(0x00); out.write(90); out.write(0x00); out.write(10);
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeGoca(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent(), "Area with fillet must render on graphics plane");
|
||||
assertEquals(90, decoder.getCurX(), "Current X must update to last fillet vertex");
|
||||
assertEquals(10, decoder.getCurY(), "Current Y must update to last fillet vertex");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRelativeLinesWithSignedDeltas() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// GRLINE (0xE1): Start at (100, 100), dx=-20, dy=+30, dx=+40, dy=-15
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GRLINE); out.write(8);
|
||||
out.write(0x00); out.write(100); // startX
|
||||
out.write(0x00); out.write(100); // startY
|
||||
out.write((byte) -20); // dx1 = -20
|
||||
out.write((byte) 30); // dy1 = +30
|
||||
out.write((byte) 40); // dx2 = +40
|
||||
out.write((byte) -15); // dy2 = -15
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeGoca(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
// 100 - 20 + 40 = 120
|
||||
assertEquals(120, decoder.getCurX(), "Current X should be 120");
|
||||
// 100 + 30 - 15 = 115
|
||||
assertEquals(115, decoder.getCurY(), "Current Y should be 115");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGsapAspectRatioAndCoordinateTransform() {
|
||||
GddmCoordinateTransform transform = new GddmCoordinateTransform(400, 300);
|
||||
transform.setAspectRatio(2, 1, 3, 1);
|
||||
|
||||
assertEquals(2.0, transform.getScaleX(), 0.001);
|
||||
assertEquals(3.0, transform.getScaleY(), 0.001);
|
||||
|
||||
// Coordinate inversion tests
|
||||
int gocaY = 50;
|
||||
int screenY = transform.mapGocaYToScreen(gocaY);
|
||||
int invertedGocaY = transform.mapScreenYToGoca(screenY);
|
||||
assertEquals(gocaY, invertedGocaY, "Inversion round-trip must preserve GOCA Y");
|
||||
|
||||
transform.resetAspectRatio();
|
||||
assertEquals(1.0, transform.getScaleX(), 0.001);
|
||||
assertEquals(1.0, transform.getScaleY(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImageMultiplierSupport() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// GBIMG (0xD1): origin=(10, 10), imult=2, w=8, h=8, 1 bpp uncompressed
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GBIMG); out.write(10);
|
||||
out.write(0x00); out.write(10); // X
|
||||
out.write(0x00); out.write(10); // Y
|
||||
out.write(0x00); out.write(2); // imult = 2
|
||||
out.write(0x00); out.write(8); // w = 8
|
||||
out.write(0x00); out.write(8); // h = 8
|
||||
|
||||
// GIMD (0x92): 8 bytes (8 rows of 8 bits each)
|
||||
out.write(GocaConstants.G_GIMD); out.write(8);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
out.write((byte) 0xFF);
|
||||
}
|
||||
|
||||
// GEIMG (0x93)
|
||||
out.write(GocaConstants.G_GEIMG);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeGoca(stream, 0, stream.length);
|
||||
|
||||
assertEquals(2, decoder.getImageMultiplier(), "Image multiplier must be parsed as 2");
|
||||
assertTrue(plane.hasContent(), "Image must render on graphics plane");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGraphicInputStructuredField56BytesLayout() {
|
||||
int gocaX = 1234;
|
||||
int gocaY = -5678;
|
||||
int row = 10;
|
||||
int col = 20;
|
||||
|
||||
// Button 1 (Pick), Shift=true, Ctrl=false
|
||||
byte[] sf = GraphicInputBuilder.buildGraphicInput(gocaX, gocaY, row, col, 1, true, true, false);
|
||||
|
||||
assertEquals(56, sf.length, "Structured field length must be 56 bytes");
|
||||
// Bytes 0-1: 0x0034 (52 decimal)
|
||||
assertEquals(0x00, sf[0]);
|
||||
assertEquals(0x34, sf[1]);
|
||||
// Bytes 16-19: Class descriptor (0x23, 0x00, 0x23, 0x00)
|
||||
assertEquals(0x23, sf[16]);
|
||||
assertEquals(0x00, sf[17]);
|
||||
assertEquals(0x23, sf[18]);
|
||||
assertEquals(0x00, sf[19]);
|
||||
// Bytes 24-25: 16-bit signed GOCA X
|
||||
short x = (short) (((sf[24] & 0xFF) << 8) | (sf[25] & 0xFF));
|
||||
assertEquals(gocaX, x);
|
||||
// Bytes 26-27: 16-bit signed GOCA Y
|
||||
short y = (short) (((sf[26] & 0xFF) << 8) | (sf[27] & 0xFF));
|
||||
assertEquals(gocaY, y);
|
||||
// Bytes 28-31: Fixed mouse trigger class (0x00000004)
|
||||
assertEquals(0x00, sf[28]);
|
||||
assertEquals(0x00, sf[29]);
|
||||
assertEquals(0x00, sf[30]);
|
||||
assertEquals(0x04, sf[31]);
|
||||
// Bytes 32-33: Fixed correlation class (0x0004)
|
||||
assertEquals(0x00, sf[32]);
|
||||
assertEquals(0x04, sf[33]);
|
||||
// Byte 34: Shift flag 0x80
|
||||
assertEquals((byte) 0x80, sf[34]);
|
||||
// Byte 35: Button 1 (0x01)
|
||||
assertEquals(0x01, sf[35]);
|
||||
|
||||
// Button 2 (Action), Shift=false, Ctrl=true
|
||||
byte[] sf2 = GraphicInputBuilder.buildGraphicInput(gocaX, gocaY, row, col, 2, true, false, true);
|
||||
assertEquals((byte) 0x40, sf2[34]);
|
||||
assertEquals(0x02, sf2[35]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendGraphicMouseAidInteractiveDispatch() {
|
||||
EbcdicTranslator translator = new EbcdicTranslator();
|
||||
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
AtomicReference<byte[]> sentData = new AtomicReference<>();
|
||||
InputProcessor inputProcessor = new InputProcessor(screen, translator, null) {
|
||||
@Override
|
||||
protected void sendAidResponse(byte[] data) {
|
||||
sentData.set(data);
|
||||
}
|
||||
};
|
||||
inputProcessor.setGocaDecoder(decoder);
|
||||
|
||||
// When no graphics are active, regular RMA is sent
|
||||
inputProcessor.sendGraphicMouseAid(DS3270Constants.AID_ENTER, 1, false, false);
|
||||
assertNotNull(sentData.get());
|
||||
assertNotEquals(DS3270Constants.AID_SF, sentData.get()[0], "Without active graphics, AID_SF must not be sent");
|
||||
|
||||
// Activate graphics cursor
|
||||
sentData.set(null);
|
||||
inputProcessor.setKeyboardLocked(false);
|
||||
decoder.attachGraphicCursor(100, 200);
|
||||
assertTrue(decoder.hasActiveGraphics());
|
||||
|
||||
inputProcessor.sendGraphicMouseAid(100, 200, DS3270Constants.AID_ENTER, 1, false, false);
|
||||
byte[] out = sentData.get();
|
||||
assertNotNull(out);
|
||||
assertEquals(DS3270Constants.AID_SF, out[0] & 0xFF, "With active graphics, Structured Field AID (0x88) must be sent");
|
||||
assertTrue(out.length >= 57, "Output must contain AID byte + 56-byte Graphic Input SF");
|
||||
assertEquals(0x00, out[1]);
|
||||
assertEquals(0x34, out[2]);
|
||||
}
|
||||
}
|
||||
+126
-2
@@ -51,7 +51,7 @@ public class ProgramSymbolManagerTest {
|
||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
|
||||
assertNotNull(slot);
|
||||
assertEquals(9, slot.getWidth());
|
||||
assertEquals(12, slot.getHeight());
|
||||
assertEquals(16, slot.getHeight());
|
||||
assertFalse(slot.isTriplePlane());
|
||||
|
||||
// Verify pixel data (Row 0 Col 0 is 1)
|
||||
@@ -64,7 +64,7 @@ public class ProgramSymbolManagerTest {
|
||||
int bg = 0xFF000000; // Black
|
||||
int[] rgb = slot.getRgbPixels(fg, bg);
|
||||
assertNotNull(rgb);
|
||||
assertEquals(9 * 12, rgb.length);
|
||||
assertEquals(9 * 16, rgb.length);
|
||||
assertEquals(fg, rgb[0]); // Row 0 Col 0 pixel should be foreground Green
|
||||
}
|
||||
|
||||
@@ -164,4 +164,128 @@ public class ProgramSymbolManagerTest {
|
||||
assertNotNull(slot);
|
||||
assertEquals(1, slot.getPixelData()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiSymbolLoadPsTriplePlane190Symbols() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
|
||||
// Simulate bits image loader connect payload: 190 glyphs * 18 bytes = 3420 bytes
|
||||
// Sent to a triple plane set (RWS 4, LCID 0x42) with colorPlane = 0 (no ext header)
|
||||
byte[] payload = new byte[4 + 190 * 18];
|
||||
payload[0] = 0x41; // Format 1, clearAll = true
|
||||
payload[1] = 0x42; // LCID 0x42
|
||||
payload[2] = 0x41; // Start codepoint 0x41
|
||||
payload[3] = 0x04; // RWS 4 (Triple Plane)
|
||||
|
||||
for (int i = 0; i < 190; i++) {
|
||||
// Set Row 0 Col 0 for each symbol
|
||||
payload[4 + i * 18] = (byte) 0x80;
|
||||
}
|
||||
|
||||
manager.loadps(payload);
|
||||
|
||||
ProgramSymbolSet set = manager.getSymbolSet(0x42);
|
||||
assertNotNull(set);
|
||||
|
||||
// Verify ALL 190 glyphs (from 0x41 to 0xFE) were loaded, NOT just 63!
|
||||
for (int cp = 0x41; cp <= 0xFE; cp++) {
|
||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x42, cp);
|
||||
assertNotNull(slot, "Glyph for codepoint 0x" + Integer.toHexString(cp) + " must not be null!");
|
||||
assertEquals(9, slot.getWidth());
|
||||
assertEquals(16, slot.getHeight());
|
||||
assertTrue(slot.isTriplePlane());
|
||||
assertEquals(7, slot.getPixelData()[0] & 0xFF, "Pixel (0,0) must have all 3 planes set (7)");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testColorPlaneCompositeMergingAndPalette() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
|
||||
// 1. Send Plane 1 (Red) for codepoint 0x41
|
||||
byte[] sf1 = new byte[4 + 6 + 18];
|
||||
sf1[0] = (byte) 0x81; // Format 1, extHeader=true
|
||||
sf1[1] = 0x42; // LCID 0x42
|
||||
sf1[2] = 0x41; // Codepoint 0x41
|
||||
sf1[3] = 0x04; // RWS 4
|
||||
sf1[4] = 0x06; // Ext header len = 6
|
||||
sf1[9] = 0x01; // Plane 1 = Red
|
||||
sf1[10] = (byte) 0x80;// Row 0 Col 0 = bit 1
|
||||
manager.loadps(sf1);
|
||||
|
||||
ProgramSymbolSet.SymbolSlot slot1 = manager.getSymbol(0x42, 0x41);
|
||||
assertNotNull(slot1);
|
||||
assertEquals(1, slot1.getPixelData()[0] & 0xFF);
|
||||
// Plane 1 alone must be Red (GOCA_COLORS[2] = 0xFFF01818)
|
||||
assertEquals(GocaConstants.GOCA_COLORS[2], slot1.getRgbPixels(0, 0)[0]);
|
||||
|
||||
// 2. Send Plane 2 (Green) for codepoint 0x41
|
||||
byte[] sf2 = new byte[4 + 6 + 18];
|
||||
sf2[0] = (byte) 0x81;
|
||||
sf2[1] = 0x42;
|
||||
sf2[2] = 0x41;
|
||||
sf2[3] = 0x04;
|
||||
sf2[4] = 0x06;
|
||||
sf2[9] = 0x02; // Plane 2 = Green
|
||||
sf2[10] = (byte) 0x80;// Row 0 Col 0 = bit 1
|
||||
manager.loadps(sf2);
|
||||
|
||||
ProgramSymbolSet.SymbolSlot slot2 = manager.getSymbol(0x42, 0x41);
|
||||
assertNotNull(slot2);
|
||||
// Red (1) | Green (2) = 3 (Yellow)
|
||||
assertEquals(3, slot2.getPixelData()[0] & 0xFF);
|
||||
assertEquals(GocaConstants.GOCA_COLORS[6], slot2.getRgbPixels(0, 0)[0]);
|
||||
|
||||
// 3. Send Plane 4 (Blue) for codepoint 0x41
|
||||
byte[] sf3 = new byte[4 + 6 + 18];
|
||||
sf3[0] = (byte) 0x81;
|
||||
sf3[1] = 0x42;
|
||||
sf3[2] = 0x41;
|
||||
sf3[3] = 0x04;
|
||||
sf3[4] = 0x06;
|
||||
sf3[9] = 0x04; // Plane 4 = Blue
|
||||
sf3[10] = (byte) 0x80;// Row 0 Col 0 = bit 1
|
||||
manager.loadps(sf3);
|
||||
|
||||
ProgramSymbolSet.SymbolSlot slot3 = manager.getSymbol(0x42, 0x41);
|
||||
assertNotNull(slot3);
|
||||
// Red (1) | Green (2) | Blue (4) = 7 (White)
|
||||
assertEquals(7, slot3.getPixelData()[0] & 0xFF);
|
||||
assertEquals(GocaConstants.GOCA_COLORS[7], slot3.getRgbPixels(0, 0)[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransitionMonochromeToColor() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
|
||||
// 1. Initial monochrome load (colorPlane = 0)
|
||||
byte[] sfMono = new byte[4 + 18];
|
||||
sfMono[0] = 0x01; // Format 1
|
||||
sfMono[1] = 0x42;
|
||||
sfMono[2] = 0x41;
|
||||
sfMono[3] = 0x04; // RWS 4
|
||||
sfMono[4] = (byte) 0x80; // Row 0 Col 0 set
|
||||
manager.loadps(sfMono);
|
||||
|
||||
ProgramSymbolSet.SymbolSlot monoSlot = manager.getSymbol(0x42, 0x41);
|
||||
assertEquals(7, monoSlot.getPixelData()[0] & 0xFF);
|
||||
assertTrue(monoSlot.isAllPlanesLoaded());
|
||||
|
||||
// 2. Host now sends color plane 1 (Red only)
|
||||
byte[] sfRed = new byte[4 + 6 + 18];
|
||||
sfRed[0] = (byte) 0x81;
|
||||
sfRed[1] = 0x42;
|
||||
sfRed[2] = 0x41;
|
||||
sfRed[3] = 0x04;
|
||||
sfRed[4] = 0x06;
|
||||
sfRed[9] = 0x01; // Plane 1 (Red)
|
||||
sfRed[10] = (byte) 0x80;
|
||||
manager.loadps(sfRed);
|
||||
|
||||
ProgramSymbolSet.SymbolSlot colorSlot = manager.getSymbol(0x42, 0x41);
|
||||
// Previous monochrome bits (7) must NOT bleed in! Only Red (1) should be present!
|
||||
assertEquals(1, colorSlot.getPixelData()[0] & 0xFF);
|
||||
assertEquals(GocaConstants.GOCA_COLORS[2], colorSlot.getRgbPixels(0, 0)[0]);
|
||||
assertFalse(colorSlot.isAllPlanesLoaded());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,6 +218,32 @@ public class InputProcessorTest {
|
||||
assertEquals(2, screen.getCursorAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProtectedFieldSpaceDesignatorNoLightPenSelection() {
|
||||
screen.erase(false);
|
||||
// Field attribute at pos 0: PROTECTED, intensified selectable (0x28) as found in ISPF screen titles
|
||||
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_PROTECT | FA_INT_HIGH_SEL));
|
||||
// Designator character at pos 1: space
|
||||
screen.getCell(1).ec = 0x40;
|
||||
screen.getCell(1).ucs4 = ' ';
|
||||
screen.getCell(2).ec = (byte) 0xC1;
|
||||
screen.getCell(2).ucs4 = 'A';
|
||||
|
||||
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
|
||||
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||
@Override
|
||||
public void sendAid(int aidCode) {
|
||||
sentAid.set(aidCode);
|
||||
}
|
||||
};
|
||||
|
||||
// Per IBM GA23-0059, clicking/light-pen on protected field with space/null must have no effect
|
||||
boolean result = input.lightPenSelect(2);
|
||||
assertFalse(result, "Protected field with space designator must not trigger light-pen selection");
|
||||
assertEquals(-1, sentAid.get(), "Must not send AID_SELECT");
|
||||
assertFalse((screen.getCell(0).fa & FA_MODIFY) != 0, "Must not set FA_MODIFY");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLightPenSelectEnterImmediateAmpersand() {
|
||||
screen.erase(false);
|
||||
|
||||
Reference in New Issue
Block a user