IND testing
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m5s

This commit is contained in:
2026-08-27 22:05:56 -04:00
parent 2e9b6d325a
commit 8654b72d8c
3 changed files with 442 additions and 36 deletions
@@ -42,6 +42,7 @@ public class FTDft {
private boolean dftEof = false;
private boolean messageFlag = false;
private long bytesTransferred = 0;
private int pendingByte = -1;
// Savebuf for Read Modified retransmit
private byte[] dftSaveBuf = null;
@@ -92,6 +93,7 @@ public class FTDft {
dftSaveBuf = null;
dftSaveBufLen = 0;
lastCr = false;
pendingByte = -1;
}
/**
@@ -403,7 +405,7 @@ public class FTDft {
try {
while (!dftEof && totalRead < numbytes) {
if (config.isAscii() && (config.isRemapFlag() || config.isCrFlag())) {
if (config.isAscii()) {
int b = dftAsciiRead(config);
if (b == -1) {
dftEof = true;
@@ -490,6 +492,12 @@ public class FTDft {
* Matching x3270 dft_ascii_read logic.
*/
private int dftAsciiRead(FTConfig config) throws IOException {
if (pendingByte != -1) {
int b = pendingByte;
pendingByte = -1;
return b;
}
if (inputStream == null) return -1;
int c = inputStream.read();
@@ -497,15 +505,18 @@ public class FTDft {
if (config.isCrFlag() && !lastCr && c == '\n') {
lastCr = false;
// Expand \n to \r\n: return \r byte now
int rEbc = translator.unicodeToEbcdic('\r');
if (rEbc < 0) rEbc = 0x0D;
return config.isRemapFlag() ? FT2ASC[rEbc & 0xFF] : rEbc;
// Expand \n to \r\n: buffer \n in pendingByte, return \r now
pendingByte = convertAsciiByte('\n', config);
return convertAsciiByte('\r', config);
}
lastCr = (c == '\r');
return convertAsciiByte((char) c, config);
}
private int convertAsciiByte(char c, FTConfig config) {
if (!config.isRemapFlag()) {
int ebc = translator.unicodeToEbcdic((char) c);
int ebc = translator.unicodeToEbcdic(c);
return ebc >= 0 ? ebc : 0x40;
}
@@ -519,7 +530,7 @@ public class FTDft {
} else if (c == 0x9F) {
ebc = 0xFF;
} else {
ebc = translator.unicodeToEbcdic((char) c);
ebc = translator.unicodeToEbcdic(c);
}
if (ebc < 0) ebc = 0x40;
@@ -0,0 +1,116 @@
package haus.nightmare.lib3270j.ft;
import haus.nightmare.lib3270j.TerminalModel;
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 static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
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 static org.junit.jupiter.api.Assertions.*;
public class FTCutTest {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor inputProcessor;
private TestFTCutListener listener;
private FTCut ftCut;
static class TestFTCutListener implements FTCut.FTCutListener {
FTConfig config;
FTState state = FTState.RUNNING;
long bytesTransferred = 0;
boolean completed = false;
String errorMessage = null;
@Override
public void onCutRunning() {
state = FTState.RUNNING;
}
@Override
public void onTransferComplete(String error) {
state = FTState.NONE;
completed = true;
errorMessage = error;
}
@Override
public void onTransferAborted(String error) {
state = FTState.NONE;
completed = false;
errorMessage = error;
}
@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_3279_4, translator);
inputProcessor = new InputProcessor(screen, translator, null);
listener = new TestFTCutListener();
ftCut = new FTCut(screen, inputProcessor, translator, listener);
}
@Test
public void testCutUploadAsciiExpandsUnixNewlines() {
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.SEND);
config.setHostType(FTConfig.HostType.CMS);
config.setHostFilename("GENPASS REXX A");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
config.setRemapFlag(true);
listener.config = config;
String originalText = "WORDS.1 = 'aah'\n";
ByteArrayInputStream in = new ByteArrayInputStream(originalText.getBytes(StandardCharsets.UTF_8));
ftCut.initTransfer(in, null);
// Setup CUT screen frame for DATA_REQUEST
screen.setCellFA(O_SF, (byte) (FA_PROTECT | FA_NUMERIC));
screen.setCell(O_FRAME_TYPE, FT_DATA_REQUEST);
screen.setCell(O_DR_FRAME_SEQ, FTConstants.to6(0, translator));
ftCut.processScreenUpdate();
// Read uploaded data from screen
int len = (FTConstants.from6(screen.getCellEC(O_UP_LEN), translator) << 6) |
FTConstants.from6(screen.getCellEC(O_UP_LEN + 1), translator);
assertTrue(len > 0);
}
}
@@ -1,68 +1,85 @@
package haus.nightmare.lib3270j.ft;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.TerminalModel;
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.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class FTDftTest {
private EbcdicTranslator translator;
private ScreenBuffer screen;
private InputProcessor input;
private TestListener listener;
private FTDft dft;
private FTConfig config;
private TestInputProcessor inputProcessor;
private TestFTDftListener listener;
private FTDft ftDft;
private static class TestListener implements FTDft.FTDftListener {
static class TestInputProcessor extends InputProcessor {
final List<byte[]> sentStructuredFields = new ArrayList<>();
public TestInputProcessor(ScreenBuffer screen, EbcdicTranslator translator) {
super(screen, translator, null);
}
@Override
public void sendStructuredFieldData(byte[] data) {
sentStructuredFields.add(data.clone());
}
}
static class TestFTDftListener implements FTDft.FTDftListener {
FTConfig config;
FTState state = FTState.RUNNING;
long bytesTransferred = 0;
boolean dftRunningCalled = false;
boolean completeCalled = false;
boolean abortCalled = false;
String completeMsg = null;
String abortMsg = null;
long bytesTransferred = 0;
FTConstants.FTState state = FTConstants.FTState.RUNNING;
FTConfig config;
@Override
public void onDftRunning() {
dftRunningCalled = true;
state = FTState.RUNNING;
}
@Override
public void onTransferComplete(String errorMessage) {
public void onTransferComplete(String error) {
state = FTState.NONE;
completeCalled = true;
completeMsg = errorMessage;
completeMsg = error;
}
@Override
public void onTransferAborted(String errorMessage) {
public void onTransferAborted(String error) {
state = FTState.NONE;
abortCalled = true;
abortMsg = errorMessage;
abortMsg = error;
}
@Override
public void onBytesTransferred(long bytes) {
bytesTransferred = bytes;
this.bytesTransferred = bytes;
}
@Override
public FTConstants.FTState getCurrentState() {
public FTState getCurrentState() {
return state;
}
@Override
public void setState(FTConstants.FTState state) {
public void setState(FTState state) {
this.state = state;
}
@@ -80,21 +97,35 @@ public class FTDftTest {
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
input = new InputProcessor(screen, translator, null);
listener = new TestListener();
config = new FTConfig();
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
inputProcessor = new TestInputProcessor(screen, translator);
listener = new TestFTDftListener();
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.RECEIVE);
config.setTransferMode(FTConfig.TransferMode.BINARY);
listener.config = config;
ftDft = new FTDft(inputProcessor, translator, listener);
}
dft = new FTDft(input, translator, listener);
private byte[] extractDataFromGetReply(byte[] sf) {
// SF format: AID_SF(1) + Len(2) + SF_TRANSFER_DATA(1) + TR_GET_REPLY(2) + TR_RECNUM_HDR(2) + Recnum(4) + TR_NOT_COMPRESSED(2) + TR_BEGIN_DATA(1) + DataLen(2) + Data(...)
// Find TR_BEGIN_DATA (0x61)
for (int i = 0; i < sf.length - 3; i++) {
if ((sf[i] & 0xFF) == TR_BEGIN_DATA) {
int dataLen = ((sf[i + 1] & 0xFF) << 8) | (sf[i + 2] & 0xFF);
int actualLen = dataLen - 5;
byte[] data = new byte[actualLen];
System.arraycopy(sf, i + 3, data, 0, actualLen);
return data;
}
}
return new byte[0];
}
@Test
public void testOpenMessageStreamAsciiAndHandleTrans14Error() {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
dft.initTransfer(null, outStream);
ftDft.initTransfer(null, outStream);
// 1. Simulate TR_OPEN_REQ with ASCII "FT:MSG" matching j3270.log:
// SF length = 0x0023, SF type = 0xD0, Request code = 0x0012, stream name "FT:MSG " at offset 28
@@ -105,7 +136,7 @@ public class FTDftTest {
0x46, 0x54, 0x3A, 0x4D, 0x53, 0x47, 0x20 // "FT:MSG "
};
dft.processStructuredField(openReq, 0, openReq.length);
ftDft.processStructuredField(openReq, 0, openReq.length);
// Should NOT trigger onDftRunning because it's a message stream
assertFalse(listener.dftRunningCalled);
@@ -132,7 +163,7 @@ public class FTDftTest {
insertData[0] = (byte) ((sfLen >> 8) & 0xFF);
insertData[1] = (byte) (sfLen & 0xFF);
dft.processStructuredField(insertData, 0, insertData.length);
ftDft.processStructuredField(insertData, 0, insertData.length);
// Verify that onTransferAborted was called with the error message
assertTrue(listener.abortCalled, "onTransferAborted should be called for TRANS14 error");
@@ -147,7 +178,7 @@ public class FTDftTest {
@Test
public void testOpenDataStreamAndWriteData() {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
dft.initTransfer(null, outStream);
ftDft.initTransfer(null, outStream);
// 1. Simulate TR_OPEN_REQ with ASCII "FT:DATA"
byte[] openReq = new byte[] {
@@ -157,7 +188,7 @@ public class FTDftTest {
0x46, 0x54, 0x3A, 0x44, 0x41, 0x54, 0x41 // "FT:DATA"
};
dft.processStructuredField(openReq, 0, openReq.length);
ftDft.processStructuredField(openReq, 0, openReq.length);
assertTrue(listener.dftRunningCalled, "onDftRunning should be called for file data stream");
// 2. Simulate TR_DATA_INSERT with binary file payload
@@ -181,10 +212,258 @@ public class FTDftTest {
insertData[0] = (byte) ((sfLen >> 8) & 0xFF);
insertData[1] = (byte) (sfLen & 0xFF);
dft.processStructuredField(insertData, 0, insertData.length);
ftDft.processStructuredField(insertData, 0, insertData.length);
// Data must be written to file stream
assertArrayEquals(fileData, outStream.toByteArray());
assertEquals(8, listener.bytesTransferred);
}
@Test
public void testDftUploadAsciiExpandsUnixNewlines() {
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.SEND);
config.setHostType(FTConfig.HostType.CMS);
config.setHostFilename("GENPASS REXX A");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
config.setRemapFlag(true);
listener.config = config;
String originalText = "WORDS.1 = 'aah'\nWORDS.2 = 'aba'\nWORDS.3 = 'abo'\n";
ByteArrayInputStream in = new ByteArrayInputStream(originalText.getBytes(StandardCharsets.UTF_8));
ftDft.initTransfer(in, null);
// Host sends TR_GET_REQ
byte[] getReq = new byte[] {
(byte) 0x00, (byte) 0x05, (byte) SF_TRANSFER_DATA,
(byte) (TR_GET_REQ >> 8), (byte) (TR_GET_REQ & 0xFF)
};
ftDft.processStructuredField(getReq, 0, getReq.length);
assertEquals(1, inputProcessor.sentStructuredFields.size());
byte[] reply = inputProcessor.sentStructuredFields.get(0);
byte[] payload = extractDataFromGetReply(reply);
// Verify payload has CRLF (\r\n) after each line
// Translate payload back:
// Host pseudo-ASCII -> EBCDIC -> Unicode
StringBuilder sb = new StringBuilder();
for (byte b : payload) {
int ub = b & 0xFF;
if (ub == 0x0D) {
sb.append("\r");
} else if (ub == 0x0A) {
sb.append("\n");
} else {
int ebc = ASC2FT[ub];
sb.append(translator.ebcdicToUnicode(ebc));
}
}
String expected = "WORDS.1 = 'aah'\r\nWORDS.2 = 'aba'\r\nWORDS.3 = 'abo'\r\n";
assertEquals(expected, sb.toString());
}
@Test
public void testDftUploadAsciiPreservesWindowsNewlines() {
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.SEND);
config.setHostType(FTConfig.HostType.CMS);
config.setHostFilename("TEST FILE A");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
config.setRemapFlag(true);
listener.config = config;
String originalText = "LINE1\r\nLINE2\r\n";
ByteArrayInputStream in = new ByteArrayInputStream(originalText.getBytes(StandardCharsets.UTF_8));
ftDft.initTransfer(in, null);
byte[] getReq = new byte[] {
(byte) 0x00, (byte) 0x05, (byte) SF_TRANSFER_DATA,
(byte) (TR_GET_REQ >> 8), (byte) (TR_GET_REQ & 0xFF)
};
ftDft.processStructuredField(getReq, 0, getReq.length);
assertEquals(1, inputProcessor.sentStructuredFields.size());
byte[] payload = extractDataFromGetReply(inputProcessor.sentStructuredFields.get(0));
StringBuilder sb = new StringBuilder();
for (byte b : payload) {
int ub = b & 0xFF;
if (ub == 0x0D) {
sb.append("\r");
} else if (ub == 0x0A) {
sb.append("\n");
} else {
int ebc = ASC2FT[ub];
sb.append(translator.ebcdicToUnicode(ebc));
}
}
String expected = "LINE1\r\nLINE2\r\n";
assertEquals(expected, sb.toString());
}
@Test
public void testDftUploadAsciiKeepCr() {
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.SEND);
config.setHostType(FTConfig.HostType.CMS);
config.setHostFilename("TEST FILE A");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.KEEP);
config.setRemapFlag(true);
listener.config = config;
String originalText = "LINE1\nLINE2\n";
ByteArrayInputStream in = new ByteArrayInputStream(originalText.getBytes(StandardCharsets.UTF_8));
ftDft.initTransfer(in, null);
byte[] getReq = new byte[] {
(byte) 0x00, (byte) 0x05, (byte) SF_TRANSFER_DATA,
(byte) (TR_GET_REQ >> 8), (byte) (TR_GET_REQ & 0xFF)
};
ftDft.processStructuredField(getReq, 0, getReq.length);
assertEquals(1, inputProcessor.sentStructuredFields.size());
byte[] payload = extractDataFromGetReply(inputProcessor.sentStructuredFields.get(0));
StringBuilder sb = new StringBuilder();
for (byte b : payload) {
int ub = b & 0xFF;
if (ub == 0x0D) {
sb.append("\r");
} else if (ub == 0x0A) {
sb.append("\n");
} else {
int ebc = ASC2FT[ub];
sb.append(translator.ebcdicToUnicode(ebc));
}
}
String expected = "LINE1\nLINE2\n";
assertEquals(expected, sb.toString());
}
@Test
public void testDftDownloadAsciiStripsCR() {
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.RECEIVE);
config.setHostType(FTConfig.HostType.CMS);
config.setHostFilename("TEST FILE A");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
config.setRemapFlag(true);
listener.config = config;
ByteArrayOutputStream out = new ByteArrayOutputStream();
ftDft.initTransfer(null, out);
// Prepare host insert data with CRLF
String hostData = "RECORD1\r\nRECORD2\r\n";
ByteArrayOutputStream hostStream = new ByteArrayOutputStream();
for (char c : hostData.toCharArray()) {
if (c == '\r' || c == '\n') {
hostStream.write(c);
} else {
int ebc = translator.unicodeToEbcdic(c);
hostStream.write(FT2ASC[ebc & 0xFF]);
}
}
byte[] hostBytes = hostStream.toByteArray();
// Build SF_TRANSFER_DATA with TR_DATA_INSERT
ByteArrayOutputStream sf = new ByteArrayOutputStream();
sf.write(0); sf.write(0); // length placeholder
sf.write(SF_TRANSFER_DATA);
sf.write((TR_DATA_INSERT >> 8) & 0xFF);
sf.write(TR_DATA_INSERT & 0xFF);
sf.write(TR_BEGIN_DATA);
int dataLen = hostBytes.length + 5;
sf.write((dataLen >> 8) & 0xFF);
sf.write(dataLen & 0xFF);
sf.write(hostBytes, 0, hostBytes.length);
byte[] sfBytes = sf.toByteArray();
int totalLen = sfBytes.length;
sfBytes[0] = (byte) ((totalLen >> 8) & 0xFF);
sfBytes[1] = (byte) (totalLen & 0xFF);
ftDft.processStructuredField(sfBytes, 0, totalLen);
String downloaded = new String(out.toByteArray(), StandardCharsets.UTF_8);
assertEquals("RECORD1\nRECORD2\n", downloaded);
}
@Test
public void testDftUploadLargeFileMultiBufferSpan() {
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.SEND);
config.setHostType(FTConfig.HostType.CMS);
config.setHostFilename("GENPASS REXX A");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
config.setRemapFlag(true);
config.setDftBufferSize(512); // Use small buffer size to test multi-buffer splitting
listener.config = config;
// Build 500 lines resembling GENPASS.rexx
StringBuilder originalSb = new StringBuilder();
originalSb.append("/* REXX */\n");
for (int i = 1; i <= 500; i++) {
originalSb.append("WORDS.").append(i).append(" = 'word_").append(i).append("'\n");
}
String originalText = originalSb.toString();
ByteArrayInputStream in = new ByteArrayInputStream(originalText.getBytes(StandardCharsets.UTF_8));
ftDft.initTransfer(in, null);
byte[] getReq = new byte[] {
(byte) 0x00, (byte) 0x05, (byte) SF_TRANSFER_DATA,
(byte) (TR_GET_REQ >> 8), (byte) (TR_GET_REQ & 0xFF)
};
// Repeatedly process getReq until EOF
while (listener.state != FTState.NONE && inputProcessor.sentStructuredFields.size() < 100) {
int beforeCount = inputProcessor.sentStructuredFields.size();
ftDft.processStructuredField(getReq, 0, getReq.length);
if (inputProcessor.sentStructuredFields.size() == beforeCount) break;
byte[] lastSent = inputProcessor.sentStructuredFields.get(inputProcessor.sentStructuredFields.size() - 1);
// Check if EOF reply (0x4608 / TR_ERROR_REPLY)
if (lastSent.length >= 6 && (lastSent[4] & 0xFF) == 0x46 && (lastSent[5] & 0xFF) == 0x08) {
break;
}
}
// Reconstruct full stream from all data buffers
ByteArrayOutputStream fullStream = new ByteArrayOutputStream();
for (byte[] sf : inputProcessor.sentStructuredFields) {
byte[] payload = extractDataFromGetReply(sf);
fullStream.write(payload, 0, payload.length);
}
byte[] allBytes = fullStream.toByteArray();
StringBuilder decodedSb = new StringBuilder();
for (byte b : allBytes) {
int ub = b & 0xFF;
if (ub == 0x0D) {
decodedSb.append("\r");
} else if (ub == 0x0A) {
decodedSb.append("\n");
} else {
int ebc = ASC2FT[ub];
decodedSb.append(translator.ebcdicToUnicode(ebc));
}
}
String expected = originalText.replace("\n", "\r\n");
assertEquals(expected, decodedSb.toString());
// Verify specific line structure
String decodedStr = decodedSb.toString();
assertTrue(decodedStr.contains("WORDS.160 = 'word_160'\r\nWORDS.161 = 'word_161'\r\n"));
}
}