This commit is contained in:
2026-08-28 13:11:28 -04:00
parent 037ad1f941
commit a44ea00d3a
17 changed files with 1841 additions and 12 deletions
@@ -0,0 +1,91 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.ft.FTConstants;
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class ECLXferTest {
private Telnet3270Client client;
private ECLXfer xfer;
@BeforeEach
public void setUp() {
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
client = new Telnet3270Client(config);
xfer = client.getXfer();
}
@Test
public void testXferInstanceAndMtu() {
assertNotNull(xfer);
assertEquals(FTConstants.DFT_BUF, xfer.getMTUSize());
xfer.setMTUSize(8192);
assertEquals(8192, xfer.getMTUSize());
assertFalse(xfer.isTransferActive());
}
@Test
public void testListenerRegistrationAndEvents() {
List<ECLXferEvent> receivedEvents = new ArrayList<>();
ECLXferListener listener = receivedEvents::add;
xfer.addXferListener(listener);
// Attempt SendFile with invalid local file / params
int rc = xfer.SendFile("", "TEST.DATA", "ASCII CRLF");
assertNotEquals(0, rc);
assertTrue(receivedEvents.size() > 0);
ECLXferEvent ev = receivedEvents.get(0);
assertEquals(ECLXferEvent.XFER_ABORTED, ev.getEventType());
assertEquals(FTConstants.ECL_ERR_XFER_INVALID_PARAM, ev.getErrorCode());
xfer.removeXferListener(listener);
}
@Test
public void testDirectoryParsingThroughEclXfer() {
String cmsText = "PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01\n";
List<CMSDirectoryEntry> cmsEntries = xfer.getCmsDirectory(cmsText);
assertEquals(1, cmsEntries.size());
assertEquals("PROFILE", cmsEntries.get(0).getFilename());
String tsoText = "TSOUSER.TEST.CNTL 15 50 1 3390 PO FB 80 3120 TSO001\n";
List<TSODirectoryEntry> tsoEntries = xfer.getTsoDirectory(tsoText);
assertEquals(1, tsoEntries.size());
assertEquals("TSOUSER.TEST.CNTL", tsoEntries.get(0).getName());
}
@Test
public void testGetFilesCallback() {
List<HostDirectoryEntry> fileList = new ArrayList<>();
boolean[] loaded = new boolean[1];
xfer.getFiles("PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01\n", fileList, new haus.nightmare.lib3270j.ft.dir.FileTransferHostDirectoryInterface() {
@Override
public void onDirectoryLoaded(List<? extends HostDirectoryEntry> entries) {
loaded[0] = true;
}
@Override
public void onDirectoryError(String errorMessage) {}
});
assertTrue(loaded[0]);
assertEquals(1, fileList.size());
}
}
@@ -0,0 +1,177 @@
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 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 haus.nightmare.lib3270j.ft.FTConstants.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class FTPhase3Test {
private EbcdicTranslator translator;
private ScreenBuffer screen;
private InputProcessor inputProcessor;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
inputProcessor = new InputProcessor(screen, translator, null);
}
@Test
public void testTsoDatasetValidationRules() {
// Valid unqualified dataset
assertNull(FTConfig.validateTsoDatasetName("USER.TEST.DATA"));
// Valid fully qualified dataset
assertNull(FTConfig.validateTsoDatasetName("'USER.TEST.DATA'"));
// Valid PDS member
assertNull(FTConfig.validateTsoDatasetName("USER.TEST.CNTL(MEMBER1)"));
// Valid national characters
assertNull(FTConfig.validateTsoDatasetName("'USER.TEST.@#$'"));
// Invalid: missing/empty
assertNotNull(FTConfig.validateTsoDatasetName(""));
assertNotNull(FTConfig.validateTsoDatasetName(null));
// Invalid: mismatched quote
assertNotNull(FTConfig.validateTsoDatasetName("'USER.TEST.DATA"));
assertNotNull(FTConfig.validateTsoDatasetName("USER.TEST.DATA'"));
// Invalid: qualifier begins with numeric
assertNotNull(FTConfig.validateTsoDatasetName("USER.123TEST.DATA"));
// Invalid: qualifier too long (> 8 chars)
assertNotNull(FTConfig.validateTsoDatasetName("USER.VERYLONGQUALIFIER.DATA"));
// Invalid: member too long
assertNotNull(FTConfig.validateTsoDatasetName("USER.TEST.DATA(VERYLONGMEMBERNAME)"));
}
@Test
public void testCmsFilenameValidationRules() {
assertNull(FTConfig.validateCmsFilename("PROFILE EXEC"));
assertNull(FTConfig.validateCmsFilename("PROFILE EXEC A1"));
assertNull(FTConfig.validateCmsFilename("GENPASS REXX A"));
// Invalid: single token
assertNotNull(FTConfig.validateCmsFilename("PROFILE"));
// Invalid: filename > 8 chars
assertNotNull(FTConfig.validateCmsFilename("LONGNAME123 EXEC A"));
// Invalid: filetype > 8 chars
assertNotNull(FTConfig.validateCmsFilename("TEST LONGTYPE123 A"));
// Invalid: filemode > 2 chars
assertNotNull(FTConfig.validateCmsFilename("TEST DATA A123"));
}
@Test
public void testMtuClampingAndCommandBuilding() {
FTConfig config = new FTConfig();
config.setDftBufferSize(100);
assertEquals(DFT_MIN_BUF, config.getDftBufferSize()); // clamped to 256
config.setDftBufferSize(50000);
assertEquals(DFT_MAX_BUF, config.getDftBufferSize()); // clamped to 32768
config.setHostType(FTConfig.HostType.CMS);
config.setDirection(FTConfig.Direction.SEND);
config.setHostFilename("TEST DATA A");
config.setOverwrite(true);
config.setBlksize(4096);
config.setRecfm("F");
config.setLrecl(80);
String cmd = config.buildCommand();
assertTrue(cmd.contains("REPLACE"));
assertTrue(cmd.contains("BLOCK 4096"));
assertTrue(cmd.contains("RECFM F"));
assertTrue(cmd.contains("LRECL 80"));
}
@Test
public void testCutRetransmitRecovery() {
FTCutTest.TestFTCutListener listener = new FTCutTest.TestFTCutListener();
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);
listener.config = config;
FTCut ftCut = new FTCut(screen, inputProcessor, translator, listener);
ByteArrayInputStream in = new ByteArrayInputStream("DATA LINE\n".getBytes(StandardCharsets.UTF_8));
ftCut.initTransfer(in, null);
// 1. Host requests data
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();
assertEquals(0, ftCut.getRetryCount());
// 2. Host sends RETRANSMIT frame (0x4C)
screen.setCell(O_FRAME_TYPE, FT_RETRANSMIT);
ftCut.processScreenUpdate();
assertEquals(1, ftCut.getRetryCount());
assertTrue(ftCut.isTransferActive());
// 3. Complete transfer
screen.setCell(O_FRAME_TYPE, FT_CONTROL_CODE);
screen.setCell(O_CC_STATUS_CODE, (SC_XFER_COMPLETE >> 8) & 0xFF);
screen.setCell(O_CC_STATUS_CODE + 1, SC_XFER_COMPLETE & 0xFF);
ftCut.processScreenUpdate();
assertTrue(listener.completed);
}
@Test
public void testDftDdmAttributeParsing() {
FTDftTest.TestInputProcessor ip = new FTDftTest.TestInputProcessor(screen, translator);
FTDftTest.TestFTDftListener listener = new FTDftTest.TestFTDftListener();
FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.RECEIVE);
listener.config = config;
FTDft dft = new FTDft(ip, translator, listener);
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
dft.initTransfer(null, outStream);
// Craft Open Request with DDM attributes: LRECL=80, RECFM=F (1), BLKSIZE=3120, FILESIZE=12345
ByteArrayOutputStream sf = new ByteArrayOutputStream();
sf.write(0); sf.write(0); // placeholder length
sf.write(FTConstants.SF_TRANSFER_DATA);
sf.write((TR_OPEN_REQ >> 8) & 0xFF);
sf.write(TR_OPEN_REQ & 0xFF);
// LRECL (0x01, len=5, val=80)
sf.write(DDM_HDR_LRECL); sf.write(0); sf.write(5); sf.write(0); sf.write(80);
// RECFM (0x02, len=4, val=1)
sf.write(DDM_HDR_RECFM); sf.write(0); sf.write(4); sf.write(1);
// BLKSIZE (0x03, len=5, val=3120)
sf.write(DDM_HDR_BLKSIZE); sf.write(0); sf.write(5); sf.write((3120 >> 8) & 0xFF); sf.write(3120 & 0xFF);
// FILESIZE (0x04, len=7, val=12345)
sf.write(DDM_HDR_FILESIZE); sf.write(0); sf.write(7);
sf.write(0); sf.write(0); sf.write((12345 >> 8) & 0xFF); sf.write(12345 & 0xFF);
byte[] raw = sf.toByteArray();
raw[0] = (byte) ((raw.length >> 8) & 0xFF);
raw[1] = (byte) (raw.length & 0xFF);
dft.processStructuredField(raw, 0, raw.length);
assertEquals(80, dft.getHostLrecl());
assertEquals("F", dft.getHostRecfm());
assertEquals(3120, dft.getHostBlksize());
assertEquals(12345, dft.getEstimatedTotalBytes());
}
}
@@ -0,0 +1,124 @@
package haus.nightmare.lib3270j.ft.dir;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class DirectoryParserTest {
@Test
public void testTsoIspfDslistParsing() {
String ispfOutput =
"Command ===> Scroll ===> PAGE\n" +
"Dslist - Data Sets Matching TSOUSER.* Row 1 of 4\n" +
"Command - Enter \"/\" to select action Tracks %Used XT Device Dsorg Recfm Lrecl Blksz Volume\n" +
"-------------------------------------------------------------------------------------------------------\n" +
" TSOUSER.TEST.CNTL 15 50 1 3390 PO FB 80 3120 TSO001\n" +
" TSOUSER.TEST.COBOL 30 80 2 3390 PO FB 80 6160 TSO002\n" +
" TSOUSER.TEST.DATA 10 100 1 3390 PS FB 80 3120 TSO001\n" +
" TSOUSER.TEST.LOAD 45 60 3 3390 PO-E U 0 6144 TSO003\n";
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(ispfOutput);
assertEquals(4, entries.size());
TSODirectoryEntry e0 = entries.get(0);
assertEquals("TSOUSER.TEST.CNTL", e0.getName());
assertEquals("TSO001", e0.getVolume());
assertEquals("PO", e0.getDsorg());
assertEquals("FB", e0.getRecfm());
assertEquals(80, e0.getLrecl());
assertEquals(3120, e0.getBlksize());
assertEquals(15, e0.getTracksAllocated());
TSODirectoryEntry e2 = entries.get(2);
assertEquals("TSOUSER.TEST.DATA", e2.getName());
assertEquals("PS", e2.getDsorg());
assertEquals("FB", e2.getRecfm());
assertNotNull(e0.formatListing());
}
@Test
public void testTsoListcatParsing() {
String listcatOutput =
"IDCAMS SYSTEM SERVICES TIME: 12:00:00\n" +
"NONVSAM ------- TSOUSER.SAMPLE.DATA\n" +
" IN-CAT --- CATALOG.USER\n" +
" HISTORY\n" +
" RELEASE----------------2\n" +
" VOLUMES\n" +
" VOLSER------------TSO005 DEVTYPE------X'3010200F' FSEQN------------------0\n" +
" ATTRIBUTES\n" +
" RECFM------------FB LRECL-----------------80 BLKSIZE-------------3120\n" +
" DSORG------------PS ALLOC-PRM-------------15 ALLOC-SEC--------------5\n" +
"NONVSAM ------- TSOUSER.OTHER.DATA\n" +
" VOLUMES\n" +
" VOLSER------------TSO006\n" +
" ATTRIBUTES\n" +
" RECFM------------VB LRECL----------------255 BLKSIZE-------------6144\n" +
" DSORG------------PS\n";
List<TSODirectoryEntry> entries = TSODirectoryParser.parse(listcatOutput);
assertEquals(2, entries.size());
TSODirectoryEntry e0 = entries.get(0);
assertEquals("TSOUSER.SAMPLE.DATA", e0.getName());
assertEquals("TSO005", e0.getVolume());
assertEquals("FB", e0.getRecfm());
assertEquals(80, e0.getLrecl());
assertEquals(3120, e0.getBlksize());
assertEquals("PS", e0.getDsorg());
TSODirectoryEntry e1 = entries.get(1);
assertEquals("TSOUSER.OTHER.DATA", e1.getName());
assertEquals("TSO006", e1.getVolume());
assertEquals("VB", e1.getRecfm());
assertEquals(255, e1.getLrecl());
assertEquals(6144, e1.getBlksize());
}
@Test
public void testCmsFileListParsing() {
String cmsFileList =
"FILENAME FILETYPE FM FORMAT LRECL RECS BLOCKS DATE TIME\n" +
"PROFILE EXEC A1 V 80 25 1 2026-05-10 14:22:01\n" +
"GENPASS REXX A1 V 80 500 3 2026-08-28 11:00:30\n" +
"TEST DATA A1 F 80 100 2 2026-08-01 09:15:00\n";
List<CMSDirectoryEntry> entries = CMSDirectoryParser.parse(cmsFileList);
assertEquals(3, entries.size());
CMSDirectoryEntry e0 = entries.get(0);
assertEquals("PROFILE", e0.getFilename());
assertEquals("EXEC", e0.getFiletype());
assertEquals("A1", e0.getFilemode());
assertEquals("V", e0.getRecfm());
assertEquals(80, e0.getLrecl());
assertEquals(25, e0.getNumRecords());
assertEquals(1, e0.getNumBlocks());
assertEquals("2026-05-10", e0.getDate());
assertEquals("14:22:01", e0.getTime());
assertEquals("PROFILE EXEC A1", e0.getName());
CMSDirectoryEntry e1 = entries.get(1);
assertEquals("GENPASS", e1.getFilename());
assertEquals("REXX", e1.getFiletype());
assertEquals(500, e1.getNumRecords());
assertNotNull(e0.formatListing());
}
@Test
public void testCmsListfileExecParsing() {
String execList =
"&1 &2 PROFILE EXEC A1\n" +
"&1 &2 GENPASS REXX A1\n";
List<CMSDirectoryEntry> entries = CMSDirectoryParser.parse(execList);
assertEquals(2, entries.size());
assertEquals("PROFILE", entries.get(0).getFilename());
assertEquals("EXEC", entries.get(0).getFiletype());
assertEquals("GENPASS", entries.get(1).getFilename());
assertEquals("REXX", entries.get(1).getFiletype());
}
}