Phase 3
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user