Fix gradle and add workflows
Build and Test j3270 / Build JAR & Run Tests (push) Failing after 2m24s

This commit is contained in:
2026-08-20 18:24:35 -04:00
parent 80166c477b
commit a1b1273022
68 changed files with 1102 additions and 153 deletions
@@ -196,6 +196,18 @@ public class Telnet3270Client {
public void tab() { inputProcessor.tab(); }
/** Back-tab to previous unprotected field. */
public void backTab() { inputProcessor.backTab(); }
/** Move cursor to next line. */
public void newline() { inputProcessor.newline(); }
/** Erase all unprotected fields. */
public void eraseInput() { inputProcessor.eraseInput(); }
/** Insert Duplicate order. */
public void dup() { inputProcessor.dup(); }
/** Insert Field Mark order. */
public void fieldMark() { inputProcessor.fieldMark(); }
/** Attention key. */
public void attn() { inputProcessor.attn(); }
/** SysReq key. */
public void sysReq() { inputProcessor.sysReq(); }
/** Reset (unlock keyboard). */
public void reset() { inputProcessor.reset(); }
}
@@ -816,8 +816,18 @@ public class DataStreamProcessor {
}
private void sendRequestedQueryReplies(byte[] codes) {
// For now, send all — refinement can come later
sendAllQueryReplies();
byte[] qr = qrBuilder.buildQueryReplies(codes, screen.getMaxCols(), screen.getMaxRows(),
screen.getMaxCols() * screen.getMaxRows());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < qr.length; i++) {
sb.append(String.format("%02x ", qr[i] & 0xFF));
if ((i + 1) % 32 == 0)
sb.append("\n ");
}
log.info("Requested query reply (" + qr.length + " bytes):\n " + sb.toString().trim());
if (outputSender != null) {
outputSender.send3270Data(qr);
}
}
// ========== Output helpers ==========
@@ -70,13 +70,69 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
// Distributed Data Management (DFT File Transfer)
// Hardcoded limit of 4096 is standard; the client can configure it during transfer.
appendQueryReply(out, QR_DDM, buildDdm(4096));
// Implicit Partition
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
log.info("Built " + out.size() + " bytes of query replies");
log.info("Built " + out.size() + " bytes of all query replies");
return out.toByteArray();
}
/**
* Build specific query replies in response to a Read Partition Query List (SF_RPQ_LIST).
* For any unsupported requested query code, emits a QR_NULL (0xFF) structured field
* matching x3270 sf.c behavior.
*/
public byte[] buildQueryReplies(byte[] requestedCodes, int maxCols, int maxRows, int bufferSize) {
if (requestedCodes == null || requestedCodes.length == 0) {
return buildAllQueryReplies(maxCols, maxRows, bufferSize);
}
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
out.write(AID_SF);
for (byte codeByte : requestedCodes) {
int code = codeByte & 0xFF;
switch (code) {
case QR_SUMMARY:
appendQueryReply(out, QR_SUMMARY, buildSummary());
break;
case QR_USABLE_AREA:
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
break;
case QR_ALPHA_PART:
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
break;
case QR_CHARSETS:
appendQueryReply(out, QR_CHARSETS, buildCharsets());
break;
case QR_COLOR:
appendQueryReply(out, QR_COLOR, buildColor());
break;
case QR_HIGHLIGHTING:
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
break;
case QR_REPLY_MODES:
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
break;
case QR_DDM:
appendQueryReply(out, QR_DDM, buildDdm(4096));
break;
case QR_IMP_PART:
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
break;
case QR_RPQNAMES:
appendQueryReply(out, QR_RPQNAMES, new byte[0]);
break;
default:
// Unsupported query reply code emit QR_NULL
appendQueryReply(out, QR_NULL, new byte[0]);
break;
}
}
log.info("Built " + out.size() + " bytes of requested query replies for " + requestedCodes.length + " codes");
return out.toByteArray();
}
@@ -225,84 +225,96 @@ public class FTConfig {
// IND$FILE GET/PUT hostfile
cmd.append("IND$FILE ");
cmd.append(isReceive() ? "GET " : "PUT ");
cmd.append(hostFilename);
cmd.append(hostFilename != null ? hostFilename.trim() : "");
// CMS/CICS options use ( prefix
if (hostType != HostType.TSO) {
cmd.append(" (");
} else {
cmd.append(" ");
}
// Collect options into a separate buffer
StringBuilder opts = new StringBuilder();
// Mode
if (isAscii()) {
cmd.append("ASCII");
opts.append("ASCII");
} else if (hostType == HostType.CICS) {
cmd.append("BINARY");
opts.append("BINARY");
}
// CR/LF handling
if (isAscii() && isCrFlag()) {
cmd.append(" CRLF");
} else if (hostType == HostType.CICS) {
cmd.append(" NOCRLF");
if (opts.length() > 0) opts.append(" ");
opts.append("CRLF");
} else if (hostType == HostType.CICS && !isAscii()) {
if (opts.length() > 0) opts.append(" ");
opts.append("NOCRLF");
}
// Append (send only)
if (isAppend() && isSend()) {
cmd.append(" APPEND");
if (opts.length() > 0) opts.append(" ");
opts.append("APPEND");
}
// TSO-specific send options
// Host-specific send options
if (isSend()) {
if (hostType == HostType.TSO) {
if (recfm != RecordFormat.DEFAULT) {
cmd.append(" RECFM(");
if (opts.length() > 0) opts.append(" ");
opts.append("RECFM(");
switch (recfm) {
case FIXED: cmd.append("F"); break;
case VARIABLE: cmd.append("V"); break;
case UNDEFINED: cmd.append("U"); break;
case FIXED: opts.append("F"); break;
case VARIABLE: opts.append("V"); break;
case UNDEFINED: opts.append("U"); break;
default: break;
}
cmd.append(")");
opts.append(")");
if (lrecl > 0) {
cmd.append(" LRECL(").append(lrecl).append(")");
opts.append(" LRECL(").append(lrecl).append(")");
}
if (blksize > 0) {
cmd.append(" BLKSIZE(").append(blksize).append(")");
opts.append(" BLKSIZE(").append(blksize).append(")");
}
}
if (units != AllocationUnit.DEFAULT) {
cmd.append(" SPACE(").append(primarySpace);
if (opts.length() > 0) opts.append(" ");
opts.append("SPACE(").append(primarySpace);
if (secondarySpace > 0) {
cmd.append(",").append(secondarySpace);
opts.append(",").append(secondarySpace);
}
cmd.append(")");
opts.append(")");
switch (units) {
case TRACKS: cmd.append(" TRACKS"); break;
case CYLINDERS: cmd.append(" CYLINDERS"); break;
case AVBLOCK: cmd.append(" AVBLOCK(").append(avblock).append(")"); break;
case TRACKS: opts.append(" TRACKS"); break;
case CYLINDERS: opts.append(" CYLINDERS"); break;
case AVBLOCK: opts.append(" AVBLOCK(").append(avblock).append(")"); break;
default: break;
}
}
} else if (hostType == HostType.CMS) {
if (recfm != RecordFormat.DEFAULT) {
cmd.append(" RECFM ");
if (opts.length() > 0) opts.append(" ");
opts.append("RECFM ");
switch (recfm) {
case FIXED: cmd.append("F"); break;
case VARIABLE: cmd.append("V"); break;
case FIXED: opts.append("F"); break;
case VARIABLE: opts.append("V"); break;
default: break;
}
if (lrecl > 0) {
cmd.append(" LRECL ").append(lrecl);
opts.append(" LRECL ").append(lrecl);
}
}
}
}
// Additional options
if (otherOptions != null) {
cmd.append(" ").append(otherOptions);
if (otherOptions != null && !otherOptions.trim().isEmpty()) {
if (opts.length() > 0) opts.append(" ");
opts.append(otherOptions.trim());
}
// Apply options with host-specific prefix only if options are present
if (opts.length() > 0) {
if (hostType != HostType.TSO) {
cmd.append(" (").append(opts.toString());
} else {
cmd.append(" ").append(opts.toString());
}
}
return cmd.toString().trim();
@@ -21,7 +21,7 @@ public class FTCut {
/** Callback for transfer events */
public interface FTCutListener {
void onTransferRunning();
void onCutRunning();
void onTransferComplete(String errorMessage);
void onTransferAborted(String errorMessage);
void onBytesTransferred(long bytes);
@@ -221,7 +221,7 @@ public class FTCut {
xlateBufIx = 0;
cutEof = false;
cutAck();
listener.onTransferRunning();
listener.onCutRunning();
break;
case SC_XFER_COMPLETE:
@@ -23,7 +23,7 @@ public class FTDft {
/** Callback for transfer events (shared interface with CUT) */
public interface FTDftListener {
void onTransferRunning();
void onDftRunning();
void onTransferComplete(String errorMessage);
void onTransferAborted(String errorMessage);
void onBytesTransferred(long bytes);
@@ -178,7 +178,7 @@ public class FTDft {
log.info("DFT: Open request for message stream");
} else {
messageFlag = false;
listener.onTransferRunning();
listener.onDftRunning();
}
dftEof = false;
@@ -4,6 +4,7 @@ import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.screen.ExtendedAttribute;
import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.telnet.TelnetFSM;
import org.lib3270j.protocol.TelnetConstants;
import static org.lib3270j.protocol.DS3270Constants.*;
import java.io.ByteArrayOutputStream;
@@ -362,6 +363,82 @@ public class InputProcessor {
deleteChar();
}
/** Erase All Unprotected fields (Erase Input key in 3270). */
public void eraseInput() {
if (keyboardLocked) return;
screen.eraseAllUnprotected();
screen.markAllChanged();
}
/** Move cursor to first unprotected field on next line (Newline key in 3270). */
public void newline() {
if (keyboardLocked) return;
int cur = screen.getCursorAddress();
int cols = screen.getCols();
int rows = screen.getRows();
int curRow = cur / cols;
int nextRow = (curRow + 1) % rows;
int target = nextRow * cols;
if (screen.isFormatted()) {
target = screen.findNextUnprotected(target == 0 ? (rows * cols - 1) : (target - 1));
}
screen.setCursorAddress(target);
screen.markAllChanged();
}
/** Insert Duplicate (DUP) code and advance to next field. */
public void dup() {
if (keyboardLocked) return;
int baddr = screen.getCursorAddress();
byte faVal = screen.getFieldAttributeAt(baddr);
if (faIsProtected(faVal & 0xFF)) return;
ExtendedAttribute ea = screen.getCell(baddr);
ea.ec = (byte) FCORDER_DUP;
ea.ucs4 = '*';
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
tab();
}
/** Insert Field Mark (FM) code. */
public void fieldMark() {
if (keyboardLocked) return;
int baddr = screen.getCursorAddress();
byte faVal = screen.getFieldAttributeAt(baddr);
if (faIsProtected(faVal & 0xFF)) return;
ExtendedAttribute ea = screen.getCell(baddr);
ea.ec = (byte) FCORDER_FM;
ea.ucs4 = ';';
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
int size = screen.getRows() * screen.getCols();
baddr = (baddr + 1) % size;
while (screen.getCell(baddr).isFieldAttribute()) {
baddr = (baddr + 1) % size;
}
screen.setCursorAddress(baddr);
screen.markAllChanged();
}
/** Attention key (sends Telnet IP). */
public void attn() {
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
byte[] ip = new byte[] { (byte) TelnetConstants.IAC, (byte) TelnetConstants.IP };
fsm.sendRecord(ip);
}
}
/** SysReq key. */
public void sysReq() {
reset();
}
/** Reset (unlock keyboard, cancel insert mode). */
public void reset() {
setKeyboardLocked(false);
@@ -305,6 +305,7 @@ public class ScreenBuffer {
private char getAplGraphic(int ec) {
switch (ec) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
@@ -316,6 +317,24 @@ public class ScreenBuffer {
case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬'
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
case 0xCB: return '\u253C'; // Cross -> '┼'
// Special math and APL symbols (matching x3270 cg.c / apl.c)
case 0x8C: return '\u2264'; // Less-than or equal '≤'
case 0xAE: return '\u2265'; // Greater-than or equal '≥'
case 0xBE: return '\u2260'; // Not equal '≠'
case 0xAD: return '['; // Left bracket
case 0xBD: return ']'; // Right bracket
case 0x8D: return '{'; // Left brace
case 0x9D: return '}'; // Right brace
case 0xB0: return '\u00B0'; // Degree '°'
case 0xB1: return '\u00B1'; // Plus-minus '±'
case 0xB2: return '\u00B2'; // Superscript 2 '²'
case 0xB3: return '\u00B3'; // Superscript 3 '³'
case 0xAF: return '\u00AF'; // Overbar '¯'
case 0xBA: return '\u03A9'; // Omega 'Ω'
case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬'
default: return translator.ebcdicToUnicode(ec);
}
}
@@ -37,13 +37,26 @@ public class TelnetConnection {
* Connect to the host. Blocks until connection is established or fails.
*/
public void connect() throws IOException {
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
socket = new Socket();
socket.setKeepAlive(true);
socket.setOOBInline(true);
socket.setTcpNoDelay(true);
socket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
config.getConnectTimeoutMs());
if (config.isUseTls()) {
log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort());
javax.net.ssl.SSLSocketFactory ssf = (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault();
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
sslSocket.setKeepAlive(true);
sslSocket.setOOBInline(true);
sslSocket.setTcpNoDelay(true);
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
config.getConnectTimeoutMs());
sslSocket.startHandshake();
socket = sslSocket;
} else {
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
socket = new Socket();
socket.setKeepAlive(true);
socket.setOOBInline(true);
socket.setTcpNoDelay(true);
socket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
config.getConnectTimeoutMs());
}
inputStream = new BufferedInputStream(socket.getInputStream(), READ_BUFFER_SIZE);
outputStream = new BufferedOutputStream(socket.getOutputStream());
@@ -0,0 +1,66 @@
package org.lib3270j.charset;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class EbcdicTranslatorTest {
private final EbcdicTranslator translator = new EbcdicTranslator();
@Test
public void testBasicAlphanumericTranslation() {
assertEquals('A', translator.ebcdicToUnicode(0xC1));
assertEquals('B', translator.ebcdicToUnicode(0xC2));
assertEquals('Z', translator.ebcdicToUnicode(0xE9));
assertEquals('a', translator.ebcdicToUnicode(0x81));
assertEquals('z', translator.ebcdicToUnicode(0xA9));
assertEquals('0', translator.ebcdicToUnicode(0xF0));
assertEquals('9', translator.ebcdicToUnicode(0xF9));
assertEquals(' ', translator.ebcdicToUnicode(0x40));
}
@Test
public void testReverseTranslation() {
assertEquals(0xC1, translator.unicodeToEbcdic('A'));
assertEquals(0x81, translator.unicodeToEbcdic('a'));
assertEquals(0xF0, translator.unicodeToEbcdic('0'));
assertEquals(0x40, translator.unicodeToEbcdic(' '));
assertEquals(0x4B, translator.unicodeToEbcdic('.'));
assertEquals(0x6B, translator.unicodeToEbcdic(','));
}
@Test
public void testSpecialCharactersCP037() {
// [ and ]
assertEquals(0xBA, translator.unicodeToEbcdic('['));
assertEquals(0xBB, translator.unicodeToEbcdic(']'));
assertEquals('[', translator.ebcdicToUnicode(0xBA));
assertEquals(']', translator.ebcdicToUnicode(0xBB));
// { and }
assertEquals(0xC0, translator.unicodeToEbcdic('{'));
assertEquals(0xD0, translator.unicodeToEbcdic('}'));
assertEquals('{', translator.ebcdicToUnicode(0xC0));
assertEquals('}', translator.ebcdicToUnicode(0xD0));
// Backslash \
assertEquals(0xE0, translator.unicodeToEbcdic('\\'));
assertEquals('\\', translator.ebcdicToUnicode(0xE0));
// Caret ^
assertEquals(0xB0, translator.unicodeToEbcdic('^'));
assertEquals('^', translator.ebcdicToUnicode(0xB0));
// Tilde ~
assertEquals(0xA1, translator.unicodeToEbcdic('~'));
assertEquals('~', translator.ebcdicToUnicode(0xA1));
}
@Test
public void testStringRoundTrip() {
String test = "Hello World! 123 [TEST] {VAR} \\";
byte[] ebcdic = translator.stringToEbcdic(test);
String restored = translator.ebcdicToString(ebcdic, 0, ebcdic.length);
assertEquals(test, restored);
}
}
@@ -0,0 +1,69 @@
package org.lib3270j.datastream;
import org.junit.jupiter.api.Test;
import org.lib3270j.TerminalModel;
import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.screen.ExtendedAttribute;
import static org.junit.jupiter.api.Assertions.*;
import static org.lib3270j.protocol.DS3270Constants.*;
public class DataStreamProcessorTest {
private final EbcdicTranslator translator = new EbcdicTranslator();
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
private final DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
@Test
public void testEraseWriteAndSBAOrder() {
// Build an EW record:
// Byte 0: CMD_EW (0x05)
// Byte 1: WCC (0xC3) - restore keyboard, reset MDT
// Byte 2: ORDER_SBA (0x11)
// Byte 3, 4: Encoded address for 0 (0x40, 0x40)
// Byte 5: ORDER_SF (0x1D)
// Byte 6: FA (0xC0) - unprotected
// Byte 7..11: EBCDIC "HELLO" (0xC8, 0xC5, 0xD3, 0xD3, 0xD6)
byte[] record = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40,
(byte) ORDER_SF,
(byte) FA_PRINTABLE,
(byte) 0xC8, (byte) 0xC5, (byte) 0xD3, (byte) 0xD3, (byte) 0xD6
};
processor.processRecord(record, 0, record.length, true);
assertTrue(screen.isFormatted());
assertTrue(screen.getCell(0).isFieldAttribute());
// Positions 1..5 should contain "HELLO"
assertEquals('H', screen.getCell(1).ucs4);
assertEquals('E', screen.getCell(2).ucs4);
assertEquals('L', screen.getCell(3).ucs4);
assertEquals('L', screen.getCell(4).ucs4);
assertEquals('O', screen.getCell(5).ucs4);
}
@Test
public void testRepeatToAddressOrder() {
// Write record with RA from 0 to 10 with character 'A' (0xC1)
byte[] record = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40, // Address 0
(byte) ORDER_RA,
(byte) 0x40, (byte) 0x4A, // Address 10 (0x40, 0x4A)
(byte) 0xC1 // EBCDIC 'A'
};
processor.processRecord(record, 0, record.length, true);
for (int i = 0; i < 10; i++) {
assertEquals('A', screen.getCell(i).ucs4, "Cell " + i + " should be 'A'");
}
}
}
@@ -0,0 +1,55 @@
package org.lib3270j.datastream;
import org.junit.jupiter.api.Test;
import org.lib3270j.TerminalModel;
import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.screen.ScreenBuffer;
import static org.junit.jupiter.api.Assertions.*;
import static org.lib3270j.protocol.DS3270Constants.*;
public class QueryReplyBuilderTest {
private final EbcdicTranslator translator = new EbcdicTranslator();
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen);
@Test
public void testBuildAllQueryReplies() {
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
assertNotNull(replies);
assertTrue(replies.length > 0);
assertEquals((byte) AID_SF, replies[0]);
}
@Test
public void testBuildFilteredQueryRepliesWithSupportedCodes() {
byte[] requested = new byte[] { (byte) QR_DDM, (byte) QR_USABLE_AREA };
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
assertNotNull(replies);
assertEquals((byte) AID_SF, replies[0]);
// First SF should be DDM: length (2 bytes), SFID_QREPLY (0x81), QR_DDM (0x95)
int len1 = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
assertEquals(0x81, replies[3] & 0xFF);
assertEquals(QR_DDM, replies[4] & 0xFF);
// Second SF should be Usable Area
int pos2 = 1 + len1;
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
assertEquals(QR_USABLE_AREA, replies[pos2 + 3] & 0xFF);
}
@Test
public void testBuildFilteredQueryRepliesWithUnsupportedCode() {
byte[] requested = new byte[] { (byte) 0x77 }; // Unsupported code
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
assertNotNull(replies);
assertEquals((byte) AID_SF, replies[0]);
// Should emit QR_NULL (0xFF)
assertEquals(0x81, replies[3] & 0xFF);
assertEquals(QR_NULL, replies[4] & 0xFF);
}
}
@@ -0,0 +1,83 @@
package org.lib3270j.ft;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class FTConfigTest {
@Test
public void testTsoReceiveAscii() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.TSO);
config.setDirection(FTConfig.Direction.RECEIVE);
config.setHostFilename("MY.DATASET");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
assertEquals("IND$FILE GET MY.DATASET ASCII CRLF", config.buildCommand());
}
@Test
public void testTsoReceiveBinary() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.TSO);
config.setDirection(FTConfig.Direction.RECEIVE);
config.setHostFilename("'USER.BIN.DATA'");
config.setTransferMode(FTConfig.TransferMode.BINARY);
assertEquals("IND$FILE GET 'USER.BIN.DATA'", config.buildCommand());
}
@Test
public void testTsoSendFixedRecfm() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.TSO);
config.setDirection(FTConfig.Direction.SEND);
config.setHostFilename("MY.DATASET");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
config.setRecfm("F");
config.setLrecl("80");
config.setBlksize("3120");
assertEquals("IND$FILE PUT MY.DATASET ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120)", config.buildCommand());
}
@Test
public void testCmsReceiveBinaryNoNakedParen() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.CMS);
config.setDirection(FTConfig.Direction.RECEIVE);
config.setHostFilename("PROFILE EXEC A");
config.setTransferMode(FTConfig.TransferMode.BINARY);
// In CMS, binary receive has no options, so NO open parenthesis '(' should be generated!
assertEquals("IND$FILE GET PROFILE EXEC A", config.buildCommand());
}
@Test
public void testCmsReceiveAscii() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.CMS);
config.setDirection(FTConfig.Direction.RECEIVE);
config.setHostFilename("TEST DATA A");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
assertEquals("IND$FILE GET TEST DATA A (ASCII CRLF", config.buildCommand());
}
@Test
public void testCmsSendWithRecfmAndLrecl() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.CMS);
config.setDirection(FTConfig.Direction.SEND);
config.setHostFilename("TEST FILE A");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
config.setRecfm("V");
config.setLrecl("132");
assertEquals("IND$FILE PUT TEST FILE A (ASCII CRLF RECFM V LRECL 132", config.buildCommand());
}
}
@@ -0,0 +1,51 @@
package org.lib3270j.protocol;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.lib3270j.protocol.DS3270Constants.*;
public class DS3270ConstantsTest {
@Test
public void test12BitAddressRoundTrip() {
int rows = 24;
int cols = 80;
int maxAddr = rows * cols; // 1920
for (int addr = 0; addr < maxAddr; addr += 17) {
byte[] encoded = encodeAddress(addr, rows, cols);
assertEquals(2, encoded.length);
int decoded = decodeAddress(encoded[0] & 0xFF, encoded[1] & 0xFF);
assertEquals(addr, decoded, "Address " + addr + " failed round trip");
}
}
@Test
public void test14BitAddressRoundTrip() {
int rows = 60;
int cols = 80; // 4800 (> 4096, triggers 14-bit format)
int maxAddr = rows * cols;
for (int addr = 0; addr < maxAddr; addr += 123) {
byte[] encoded = encodeAddress(addr, rows, cols);
assertEquals(2, encoded.length);
int decoded = decodeAddress(encoded[0] & 0xFF, encoded[1] & 0xFF);
assertEquals(addr, decoded, "14-bit address " + addr + " failed round trip");
}
}
@Test
public void testFieldAttributePredicates() {
int protectedHighMod = FA_PRINTABLE | FA_PROTECT | FA_INT_HIGH_SEL | FA_MODIFY;
assertTrue(faIsProtected(protectedHighMod));
assertTrue(faIsHigh(protectedHighMod));
assertTrue(faIsModified(protectedHighMod));
assertFalse(faIsNumeric(protectedHighMod));
assertFalse(faIsSkip(protectedHighMod));
int skipField = FA_PRINTABLE | FA_PROTECT | FA_NUMERIC;
assertTrue(faIsSkip(skipField));
assertTrue(faIsProtected(skipField));
assertTrue(faIsNumeric(skipField));
}
}
@@ -0,0 +1,64 @@
package org.lib3270j.screen;
import org.junit.jupiter.api.Test;
import org.lib3270j.TerminalModel;
import org.lib3270j.charset.EbcdicTranslator;
import static org.junit.jupiter.api.Assertions.*;
import static org.lib3270j.protocol.DS3270Constants.*;
public class ScreenBufferTest {
private final EbcdicTranslator translator = new EbcdicTranslator();
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
@Test
public void testScreenInitialization() {
assertEquals(24, screen.getRows());
assertEquals(80, screen.getCols());
assertEquals(43, screen.getMaxRows());
assertEquals(80, screen.getMaxCols());
assertFalse(screen.isFormatted());
}
@Test
public void testScreenEraseAlternate() {
screen.erase(true);
assertTrue(screen.isScreenAlt());
assertEquals(43, screen.getRows());
assertEquals(80, screen.getCols());
screen.erase(false);
assertFalse(screen.isScreenAlt());
assertEquals(24, screen.getRows());
assertEquals(80, screen.getCols());
}
@Test
public void testFieldNavigationAndFindAttribute() {
screen.erase(false);
// Place field attribute at position 0 (unprotected)
screen.setCellFA(0, (byte) FA_PRINTABLE);
// Place field attribute at position 20 (protected)
screen.setCellFA(20, (byte) (FA_PRINTABLE | FA_PROTECT));
assertTrue(screen.isFormatted());
assertEquals(0, screen.findFieldAttribute(5));
assertEquals(20, screen.findFieldAttribute(25));
// findNextUnprotected
int nextUnprot = screen.findNextUnprotected(0);
assertEquals(1, nextUnprot);
}
@Test
public void testGraphicEscapeTranslation() {
screen.erase(false);
// Set cell at position 10 to horizontal line (0xA2) with CS_GE (0x04)
ExtendedAttribute ea = screen.getCell(10);
ea.ec = (byte) 0xA2;
ea.cs = (byte) CS_GE;
screen.translateToUnicode();
assertEquals('\u2500', ea.ucs4); // '─'
}
}