Overnight churnings
Build and Test j3270 / Build JAR & Run Tests (push) Failing after 1m23s
Release j3270 / Build & Publish Release (push) Failing after 1m15s

This commit is contained in:
2026-09-01 09:45:40 -04:00
parent a56aa5f059
commit 161364a95f
15 changed files with 1489 additions and 33 deletions
@@ -0,0 +1,113 @@
package haus.nightmare.lib3270j.ecl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.ecl.ECLConstants.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ECLOIAPhase4Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLOIA oia;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
oia = new ECLOIA(screen, input, null);
}
@Test
public void testInhibitionCodesAndStatusStrings() {
screen.erase(false);
assertEquals(INHIBIT_NOT_INHIBITED, oia.GetInputInhibited());
assertEquals(INHIBIT_NOT_INHIBITED, oia.GetInputInhibitedType());
assertEquals("READY", oia.GetStatusString());
// Test X-SYSTEM
oia.SetInputInhibited(INHIBIT_SYSTEM_LOCK);
assertTrue(oia.IsXSystem());
assertTrue(oia.IsXWait());
assertEquals("X-SYSTEM", oia.GetStatusString());
// Test X-NUM
oia.SetInputInhibited(INHIBIT_NUMERIC_ONLY);
assertTrue(oia.IsXNum());
assertEquals("X-NUM", oia.GetStatusString());
// Test X-PROT
oia.SetInputInhibited(INHIBIT_PROTECTED_FIELD);
assertTrue(oia.IsXProt());
assertEquals("X-PROT", oia.GetStatusString());
// Test X-OVERFLOW
oia.SetInputInhibited(INHIBIT_OVERFLOW);
assertTrue(oia.IsXOverflow());
assertEquals("X-OVERFLOW", oia.GetStatusString());
// Test X-COMM
oia.SetInputInhibited(INHIBIT_COMM_CHECK);
assertTrue(oia.IsXComm());
assertEquals("X-COMM", oia.GetStatusString());
// Test X-OP
oia.SetInputInhibited(INHIBIT_OPERATOR_DUE);
assertTrue(oia.IsXOperatorDue());
assertEquals("X-OP", oia.GetStatusString());
// Test X-INSERT
oia.SetInputInhibited(INHIBIT_NOT_INHIBITED);
input.setInsertMode(true);
assertTrue(oia.IsInsertMode());
assertTrue(oia.isXInsert());
assertEquals("X-INSERT", oia.GetStatusString());
}
@Test
public void testAlphanumericTypeGetters() {
screen.erase(false);
assertEquals(TYPE_ALPHANUMERIC, oia.GetAlphanumericType());
assertEquals("A", oia.GetAlphanumericTypeString());
assertTrue(oia.IsAlphanumeric());
assertFalse(oia.IsNumeric());
assertFalse(oia.IsDBCS());
// Numeric field
screen.setFieldAttribute(0, (byte) (FA_PRINTABLE | FA_NUMERIC));
screen.setCursorAddress(1);
assertEquals(TYPE_NUMERIC, oia.GetAlphanumericType());
assertEquals("N", oia.GetAlphanumericTypeString());
assertTrue(oia.IsNumeric());
assertFalse(oia.IsAlphanumeric());
// DBCS cell
ExtendedAttribute ea = screen.getCell(1);
ea.cs = ExtendedAttribute.CS_DBCS;
assertEquals(TYPE_DBCS, oia.GetAlphanumericType());
assertEquals("D", oia.GetAlphanumericTypeString());
assertTrue(oia.IsDBCS());
}
@Test
public void testSynchronizationPrimitives() {
screen.erase(false);
assertTrue(oia.WaitForInput(100));
assertTrue(oia.WaitForSystemAvailable(100));
assertTrue(oia.WaitForAppAvailable(100));
// Lock keyboard, wait should timeout
input.setKeyboardLocked(true);
assertFalse(oia.WaitForInput(50));
input.setKeyboardLocked(false);
}
}
@@ -0,0 +1,166 @@
package haus.nightmare.lib3270j.ecl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
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.ecl.ECLConstants.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ECLPSPhase4Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLPS ps;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
ps = new ECLPS(screen, input, translator);
}
@Test
public void testSearchPSComprehensiveOverloads() {
screen.erase(false);
screen.setText("IBM Host On-Demand ECL Automation Presentation Space");
// 1-based indexing overloads
assertEquals(5, ps.SearchPS("Host"));
assertEquals(5, ps.searchPS("Host"));
assertEquals(5, ps.SearchPS("Host", 1));
assertEquals(5, ps.SearchPS("Host", 1, SEARCH_FORWARD));
assertEquals(5, ps.SearchPS("Host", 1, 1));
assertEquals(5, ps.SearchPS("host", 1, 1, SEARCH_FORWARD, true));
assertEquals(0, ps.SearchPS("host", 1, 1, SEARCH_FORWARD, false));
// Bounded range searches
assertEquals(5, ps.SearchPS("Host", 1, 1, 1, 30));
assertEquals(0, ps.SearchPS("Host", 1, 10, 1, 30)); // starts after "Host"
assertEquals(5, ps.SearchPS("host", 1, 1, 1, 30, DIR_FORWARD, true));
// SearchPSExt
int pos1 = ps.SearchPSExt("ECL", 1, 50, SEARCH_FORWARD, false, false);
assertEquals(20, pos1);
int pos2 = ps.SearchPSExt("ecl", 1, 1, 1, 50, DIR_FORWARD, true, false);
assertEquals(20, pos2);
// Backward search
screen.setChar(1, 10, 'Z');
screen.setChar(1, 11, 'O');
screen.setChar(1, 12, 'O');
int backFound = ps.SearchPSExt("ZOO", 200, 1, SEARCH_BACKWARD, false, false);
assertEquals(91, backFound); // row 1 col 10 is 80 + 10 + 1 = 91
}
@Test
public void testRectangularCopyAndPaste() {
screen.erase(false);
// Fill a small grid across rows 0, 1, 2
String r0 = "ROW0_DATA_TEST";
for (int c = 0; c < r0.length(); c++) screen.setChar(0, c, r0.charAt(c));
String r1 = "ROW1_DATA_TEST";
for (int c = 0; c < r1.length(); c++) screen.setChar(1, c, r1.charAt(c));
String r2 = "ROW2_DATA_TEST";
for (int c = 0; c < r2.length(); c++) screen.setChar(2, c, r2.charAt(c));
// Copy rectangular region rows 0..2, cols 0..3
String copied = ps.copyString(0, 0, 2, 3);
assertEquals("ROW0\nROW1\nROW2", copied);
// Test inverted coordinate normalization
String inverted = ps.CopyString(2, 3, 0, 0);
assertEquals("ROW0\nROW1\nROW2", inverted);
// Paste rectangular text
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
screen.setFieldAttribute(160, (byte) FA_PRINTABLE);
int pasted = ps.PasteString("ABC\nDEF\nGHI", 0, 1);
assertEquals(9, pasted);
assertEquals("ABC", ps.getString(1, 3));
assertEquals("DEF", ps.getString(81, 3));
assertEquals("GHI", ps.getString(161, 3));
}
@Test
public void testSendCharactersWithMnemonicsAndEscaping() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(20, (byte) FA_PRINTABLE);
screen.setCursorAddress(1);
// Test mnemonic [tab] and escaped bracket [[
ps.sendCharacters("A[[B[tab]C", 1);
assertEquals('A', screen.getChar(0, 1));
assertEquals('[', screen.getChar(0, 2));
assertEquals('B', screen.getChar(0, 3));
// After tab, at pos 21
assertEquals('C', screen.getChar(0, 21));
}
@Test
public void testEntryAssistPassThroughMethods() {
screen.erase(false);
ps.SetEntryAssistDOCmode(true);
ps.SetEntryAssistWordWrap(true);
ps.SetEntryAssistStartColumn(4);
ps.SetEntryAssistEndColumn(72);
ps.SetEntryAssistTabStops(new int[]{ 8, 16, 24, 32 });
assertTrue(ps.IsEntryAssistDOCmode());
assertTrue(ps.IsEntryAssistWordWrap());
assertEquals(4, ps.GetEntryAssistStartColumn());
assertEquals(72, ps.GetEntryAssistEndColumn());
assertArrayEquals(new int[]{ 8, 16, 24, 32 }, ps.GetEntryAssistTabStops());
ps.setCursorPos(0, 0);
ps.WordTab(true);
assertEquals(8, ps.getCursorCol());
ps.WordTab(true);
assertEquals(16, ps.getCursorCol());
ps.WordTab(false);
assertEquals(8, ps.getCursorCol());
}
@Test
public void testFieldManagementAccessors() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(50, (byte) (FA_PRINTABLE | FA_PROTECT));
assertNotNull(ps.GetFieldList());
assertEquals(2, ps.GetFieldList().GetFieldCount());
ECLField f0 = ps.GetFirstField();
assertNotNull(f0);
assertEquals(0, f0.GetStart());
assertEquals(1, f0.GetDataStart());
assertEquals(49, f0.GetEnd());
assertEquals(49, f0.GetLength());
assertFalse(f0.IsProtected());
ECLField f1 = ps.GetNextField(f0);
assertNotNull(f1);
assertEquals(50, f1.GetStart());
assertTrue(f1.IsProtected());
assertEquals(f0.GetStart(), ps.GetPreviousField(f1).GetStart());
assertEquals(f0.GetStart(), ps.GetField(25).GetStart());
assertEquals(f1.GetStart(), ps.GetField(60).GetStart());
}
}
@@ -0,0 +1,304 @@
package haus.nightmare.lib3270j.graphics;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.awt.Point;
import static org.junit.jupiter.api.Assertions.*;
/**
* Verification test suite for Phase 3: Host Graphics (GOCA / 3179G / Programmed Symbols) (GDDM).
* Covers items 3.1 through 3.6 of the PhasedUpdates specification.
*/
public class GocaPhase3Test {
// ====================================================================================================
// ITEM 3.1: GOCA Order 0x23 / 0x27 - Viewing Window (Clipping Viewport)
// ====================================================================================================
@Test
@DisplayName("Item 3.1: Viewing Window (0x23 / 0x27) clipping bounds and raster isolation")
public void testItem3_1_ViewingWindowClipping() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
assertFalse(plane.isViewingWindowActive());
// Define Viewing Window: x in [-100, 100], y in [-100, 100]
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GSVW_DEF); // 0x23
out.write(0x08); // 8-byte payload
out.write((byte) 0xFF); out.write((byte) 0x9C); // xMin = -100
out.write((byte) 0xFF); out.write((byte) 0x9C); // yMin = -100
out.write(0x00); out.write(100); // xMax = 100
out.write(0x00); out.write(100); // yMax = 100
byte[] vwOrder = out.toByteArray();
decoder.decodeGoca(vwOrder, 0, vwOrder.length);
assertTrue(plane.isViewingWindowActive());
assertEquals(-100, plane.getViewingWindowXMin());
assertEquals(-100, plane.getViewingWindowYMin());
assertEquals(100, plane.getViewingWindowXMax());
assertEquals(100, plane.getViewingWindowYMax());
// 1. Draw a line entirely outside viewing window: (-300, -150) to (-200, -150)
ByteArrayOutputStream lineOutside = new ByteArrayOutputStream();
lineOutside.write(GocaConstants.G_GSCOL); lineOutside.write(0x02); // Red
lineOutside.write(GocaConstants.G_GLINE); lineOutside.write(0x08);
lineOutside.write((byte) 0xFE); lineOutside.write((byte) 0xD4); // -300
lineOutside.write((byte) 0xFF); lineOutside.write((byte) 0x6A); // -150
lineOutside.write((byte) 0xFF); lineOutside.write((byte) 0x38); // -200
lineOutside.write((byte) 0xFF); lineOutside.write((byte) 0x6A); // -150
byte[] lineOutBytes = lineOutside.toByteArray();
decoder.decodeGoca(lineOutBytes, 0, lineOutBytes.length);
int pxClipped = plane.mapX(-300);
int pyClipped = plane.mapY(-150);
int pixelVal = plane.getRgbBuffer()[pyClipped * plane.getCanvasWidth() + pxClipped];
assertEquals(0, pixelVal, "Pixel outside viewing window must be clipped (0)");
// 2. Draw a line inside viewing window: (0, 0) to (50, 50)
ByteArrayOutputStream lineInside = new ByteArrayOutputStream();
lineInside.write(GocaConstants.G_GSCOL); lineInside.write(0x04); // Green
lineInside.write(GocaConstants.G_GLINE); lineInside.write(0x08);
lineInside.write(0x00); lineInside.write(0); lineInside.write(0x00); lineInside.write(0);
lineInside.write(0x00); lineInside.write(50); lineInside.write(0x00); lineInside.write(50);
byte[] lineInBytes = lineInside.toByteArray();
decoder.decodeGoca(lineInBytes, 0, lineInBytes.length);
int pxIn = plane.mapX(0);
int pyIn = plane.mapY(0);
int pixelInVal = plane.getRgbBuffer()[pyIn * plane.getCanvasWidth() + pxIn];
assertNotEquals(0, pixelInVal, "Pixel inside viewing window must be rendered");
// 3. Clear viewing window via zero-length payload
ByteArrayOutputStream clearVw = new ByteArrayOutputStream();
clearVw.write(GocaConstants.G_GSVW);
clearVw.write(0x00);
byte[] clearVwBytes = clearVw.toByteArray();
decoder.decodeGoca(clearVwBytes, 0, clearVwBytes.length);
assertFalse(plane.isViewingWindowActive());
}
// ====================================================================================================
// ITEM 3.2: GOCA Order 0x22 - Segment Characteristics
// ====================================================================================================
@Test
@DisplayName("Item 3.2: Segment Characteristics (0x22) flags decoding")
public void testItem3_2_SegmentCharacteristics() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// Chained (0x80) | Dynamic (0x40) | Visible (0x00) -> 0xC0
decoder.processSegmentCharacteristics(new byte[]{(byte) 0xC0});
assertTrue(decoder.isSegChained());
assertTrue(decoder.isSegDynamic());
assertTrue(decoder.isSegVisible());
// Non-chained (0x00) | Static (0x00) | Invisible (0x20) -> 0x20
decoder.processSegmentCharacteristics(new byte[]{(byte) 0x20});
assertFalse(decoder.isSegChained());
assertFalse(decoder.isSegDynamic());
assertFalse(decoder.isSegVisible());
// Unchained (0x00) | Dynamic (0x40) | Visible (0x00) -> 0x40
decoder.processSegmentCharacteristics(new byte[]{(byte) 0x40});
assertFalse(decoder.isSegChained());
assertTrue(decoder.isSegDynamic());
assertTrue(decoder.isSegVisible());
}
// ====================================================================================================
// ITEM 3.3: GOCA Order 0x11 - Fractional Line Width Calculation
// ====================================================================================================
@Test
@DisplayName("Item 3.3: Fractional Line Width (0x11) fixed-point ratio and plane synchronization")
public void testItem3_3_FractionalLineWidth() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// 1. Fixed point: integer = 3, fraction = 128/256 (0.5) -> 3.5
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GSFLW); // 0x11
out.write(0x03); // Integer part
out.write(0x80); // Fractional part (128 / 256 = 0.5)
byte[] flwBytes = out.toByteArray();
decoder.decodeGoca(flwBytes, 0, flwBytes.length);
assertEquals(3.5, decoder.getFractionalLineWidth(), 0.001);
assertEquals(3.5, plane.getFractionalLineWidth(), 0.001);
// 2. Single byte operand: integer = 2, frac = 0 -> 2.0
ByteArrayOutputStream out2 = new ByteArrayOutputStream();
out2.write(GocaConstants.G_GSFLW);
out2.write(0x02);
byte[] flwBytes2 = out2.toByteArray();
decoder.decodeGoca(flwBytes2, 0, flwBytes2.length);
assertEquals(2.0, decoder.getFractionalLineWidth(), 0.001);
assertEquals(2.0, plane.getFractionalLineWidth(), 0.001);
// 3. Reset defaults restores 1.0
decoder.resetDefaults();
assertEquals(1.0, decoder.getFractionalLineWidth(), 0.001);
assertEquals(1.0, plane.getFractionalLineWidth(), 0.001);
}
// ====================================================================================================
// ITEM 3.4: GCS Character Baseline Angle and Shear Vector Transforms
// ====================================================================================================
@Test
@DisplayName("Item 3.4: Character Baseline Angle (GSCA 0x34) and Shear (GSCR 0x35) transforms")
public void testItem3_4_CharacterAngleAndShear() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// 1. GSCA 0x34: Character Angle vector (ax=0, ay=10) -> 90 degrees vertical
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GSCA);
out.write(0x04);
out.write(0x00); out.write(0); // ax = 0
out.write(0x00); out.write(10); // ay = 10
// GSCR 0x35: Character Shear vector (sx=10, sy=10) -> 45 degrees slant
out.write(GocaConstants.G_GSCR);
out.write(0x04);
out.write(0x00); out.write(10); // sx = 10
out.write(0x00); out.write(10); // sy = 10
byte[] stream = out.toByteArray();
decoder.decodeGoca(stream, 0, stream.length);
assertEquals(90.0, decoder.getCharAngle(), 0.01);
assertEquals(45.0, decoder.getCharShear(), 0.01);
// 2. Draw stroked vector text with active angle and shear
ByteArrayOutputStream textStream = new ByteArrayOutputStream();
textStream.write(GocaConstants.G_GSCS); textStream.write(0xF8); // Vector font
textStream.write(GocaConstants.G_GCHST); // Character String Absolute
textStream.write(0x05); // 4 bytes coord + 1 byte text 'A' (EBCDIC 0xC1)
textStream.write(0x00); textStream.write(50);
textStream.write(0x00); textStream.write(50);
textStream.write((byte) 0xC1); // EBCDIC 'A'
byte[] textBytes = textStream.toByteArray();
decoder.decodeGoca(textBytes, 0, textBytes.length);
assertTrue(plane.hasContent(), "Vector text with angle/shear must render to GraphicsPlane");
}
// ====================================================================================================
// ITEM 3.5: Multi-Plane Programmed Symbols (RWS 4..7) & Multi-Color PS Cells
// ====================================================================================================
@Test
@DisplayName("Item 3.5: Multi-Plane Programmed Symbols (RWS 4..7) & Triple-Plane Composite Color Cells")
public void testItem3_5_ProgrammedSymbolsTriplePlane() {
ProgramSymbolManager psm = new ProgramSymbolManager(9, 16);
// 1. Single-plane set (RWS 2)
byte[] loadpsSingle = new byte[4 + 18];
loadpsSingle[0] = 0x01; // Format 1
loadpsSingle[1] = 0x41; // LCID 0x41
loadpsSingle[2] = 0x40; // Start codepoint
loadpsSingle[3] = 0x02; // RWS 2
for (int i = 0; i < 18; i++) loadpsSingle[4 + i] = (byte) 0x55;
psm.loadps(loadpsSingle);
ProgramSymbolSet singleSet = psm.getSymbolSet(0x41);
assertNotNull(singleSet);
assertFalse(singleSet.isTriplePlane());
// 2. Triple-plane set (RWS 4, LCID 0x42) loaded with all 3 planes simultaneously (54 bytes)
byte[] loadpsTriple = new byte[4 + 54];
loadpsTriple[0] = 0x01; // Format 1
loadpsTriple[1] = 0x42; // LCID 0x42
loadpsTriple[2] = 0x40; // Start codepoint
loadpsTriple[3] = 0x04; // RWS 4 (Triple Plane)
// Red slice: plane 1 (0x01)
for (int i = 0; i < 18; i++) loadpsTriple[4 + i] = (byte) 0xFF;
// Green slice: plane 2 (0x02)
for (int i = 0; i < 18; i++) loadpsTriple[4 + 18 + i] = (byte) 0xFF;
// Blue slice: plane 4 (0x04)
for (int i = 0; i < 18; i++) loadpsTriple[4 + 36 + i] = (byte) 0xFF;
psm.loadps(loadpsTriple);
ProgramSymbolSet tripleSet = psm.getSymbolSet(0x42);
assertNotNull(tripleSet);
assertTrue(tripleSet.isTriplePlane());
ProgramSymbolSet.SymbolSlot slot = psm.getSymbol(0x42, 0x40);
assertNotNull(slot);
assertTrue(slot.isTriplePlane());
// Verify multi-color RGB mapping: composite plane (1|2|4 = 7 -> White)
int[] rgb = slot.getRgbPixels(GocaConstants.GOCA_COLORS[0], 0);
assertNotNull(rgb);
assertEquals(9 * 16, rgb.length);
assertEquals(0xFFFFFFFF, rgb[0]); // White pixel (all 3 planes set)
}
// ====================================================================================================
// ITEM 3.6: Pick Correlation for Graphical Segment Selection
// ====================================================================================================
@Test
@DisplayName("Item 3.6: Pick Correlation & 56-Byte Graphic Input Structured Field generation")
public void testItem3_6_PickCorrelationAndGraphicInput() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// 1. Define Segment 100: drawing a line from (-50, -50) to (50, 50)
ByteArrayOutputStream segStream = new ByteArrayOutputStream();
segStream.write(GocaConstants.G_BEGSEGM);
segStream.write(0x0C);
segStream.write(0x00); segStream.write(0x00); segStream.write(0x00); segStream.write(100); // Seg ID 100
segStream.write(0x00); segStream.write(0x00);
segStream.write(0x00); segStream.write(0x00);
segStream.write(0x00); segStream.write(0x00); segStream.write(0x00); segStream.write(0x00);
segStream.write(GocaConstants.G_GSCOL); segStream.write(0x06); // Yellow
segStream.write(GocaConstants.G_GLINE); segStream.write(0x08);
segStream.write((byte) 0xFF); segStream.write((byte) 0xCE); // -50
segStream.write((byte) 0xFF); segStream.write((byte) 0xCE); // -50
segStream.write(0x00); segStream.write(50);
segStream.write(0x00); segStream.write(50);
segStream.write(GocaConstants.G_ENDSEGM); segStream.write(0x00);
byte[] segBytes = segStream.toByteArray();
decoder.decodeGoca(segBytes, 0, segBytes.length);
// Hit test inside segment bounds: (0, 0)
int pickedSeg = decoder.findPickedSegment(0, 0);
assertEquals(100, pickedSeg, "Segment 100 should be hit at (0, 0)");
// Hit test outside segment bounds: (200, 200)
int pickedMiss = decoder.findPickedSegment(200, 200);
assertEquals(0, pickedMiss, "Missed click should default to segId 0");
// 2. Build 56-byte Graphic Input Structured Field via GraphicInputBuilder
byte[] inputSF = GraphicInputBuilder.buildPickCorrelation(25, -25, 5);
assertEquals(56, inputSF.length);
// Header check: 0x0034 (52 byte payload length per IBM HOD)
assertEquals(0x00, inputSF[0]);
assertEquals(0x34, inputSF[1]);
assertEquals(0x0F, inputSF[2]);
assertEquals(0x0F, inputSF[3]);
// Coordinate checks: X = 25, Y = -25
int gX = ((inputSF[24] & 0xFF) << 8) | (inputSF[25] & 0xFF);
int gY = (short) (((inputSF[26] & 0xFF) << 8) | (inputSF[27] & 0xFF));
assertEquals(25, gX);
assertEquals(-25, gY);
// Mouse action flags: trigger class = 4, correlation class = 4, Button 1 = 0x01
assertEquals(0x04, inputSF[31]);
assertEquals(0x04, inputSF[33]);
assertEquals(0x01, inputSF[35]);
}
}
@@ -0,0 +1,223 @@
package haus.nightmare.lib3270j.screen;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.AbstractDBCSCodePage;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLField;
import haus.nightmare.lib3270j.ecl.ECLFieldList;
import haus.nightmare.lib3270j.input.InputProcessor;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ScreenBufferPhase4Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
}
@Test
public void testEntryAssistDOCModeAndWordWrap() {
screen.erase(false);
screen.setEntryAssistDOCmode(true);
screen.setEntryAssistWordWrap(true);
screen.setEntryAssistStartColumn(0);
screen.setEntryAssistEndColumn(20);
assertTrue(screen.isEntryAssistDOCmode());
assertTrue(screen.isEntryAssistWordWrap());
assertEquals(0, screen.getEntryAssistStartColumn());
assertEquals(20, screen.getEntryAssistEndColumn());
// Type "HELLO WORLD " near column 20
screen.setCursorPosition(0, 10);
String s1 = "HELLO ";
for (int i = 0; i < s1.length(); i++) {
input.typeCharacter(s1.charAt(i));
}
assertEquals(16, screen.getCursorCol());
assertEquals(0, screen.getCursorRow());
// Now type "TESTING" which starts at col 16 and crosses col 20
String s2 = "TESTING";
for (int i = 0; i < s2.length(); i++) {
input.typeCharacter(s2.charAt(i));
}
// "HELLO " should remain on row 0 (cols 10..15)
assertEquals("HELLO ", screen.getString(10, 6));
// "TESTING" should be soft-wrapped to row 1 (cols 0..6)
assertEquals("TESTING", screen.getString(80, 7));
assertEquals(1, screen.getCursorRow());
assertEquals(7, screen.getCursorCol());
}
@Test
public void testEntryAssistWordWrapTypingSpaceAtEndMargin() {
screen.erase(false);
screen.setEntryAssistWordWrap(true);
screen.setEntryAssistStartColumn(5);
screen.setEntryAssistEndColumn(20);
screen.setCursorPosition(0, 20);
input.typeCharacter(' ');
// Cursor should wrap to next line at startCol 5
assertEquals(1, screen.getCursorRow());
assertEquals(5, screen.getCursorCol());
}
@Test
public void testEntryAssistTabStopsAndNavigation() {
screen.erase(false);
screen.setEntryAssistTabStops(new int[]{ 10, 25, 50, 70 });
assertArrayEquals(new int[]{ 10, 25, 50, 70 }, screen.getEntryAssistTabStops());
screen.setCursorPosition(0, 0);
screen.processWordTab(true);
assertEquals(10, screen.getCursorCol());
screen.processWordTab(true);
assertEquals(25, screen.getCursorCol());
screen.processWordTab(true);
assertEquals(50, screen.getCursorCol());
screen.processWordTab(false);
assertEquals(25, screen.getCursorCol());
screen.processWordTab(false);
assertEquals(10, screen.getCursorCol());
}
@Test
public void testProcessDeleteWord() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(40, (byte) (FA_PRINTABLE | FA_PROTECT));
// Field contains "ALPHA BRAVO CHARLIE"
String text = "ALPHA BRAVO CHARLIE";
for (int i = 0; i < text.length(); i++) {
screen.setChar(0, 1 + i, text.charAt(i));
}
// Place cursor at "BRAVO" (col 7)
screen.setCursorPosition(0, 7);
screen.processDeleteWord();
// "BRAVO " should be deleted, leaving "ALPHA CHARLIE"
assertEquals("ALPHA CHARLIE", screen.getString(1, 13));
assertTrue(screen.isModified(1));
}
@Test
public void testDBCSSOSIPairBalancing() {
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) dbcsTranslator.getCodePage();
cp.registerDbcsPair(0x4341, '\u6771');
ScreenBuffer dbcsScreen = new ScreenBuffer(TerminalModel.IBM_3279_2, dbcsTranslator);
dbcsScreen.erase(false);
dbcsScreen.setFieldAttribute(0, (byte) FA_PRINTABLE);
dbcsScreen.setFieldAttribute(20, (byte) (FA_PRINTABLE | FA_PROTECT));
// Insert DBCS character
boolean inserted = dbcsScreen.insertChar(1, '\u6771');
assertTrue(inserted);
assertEquals(ExtendedAttribute.CS_DBCS, dbcsScreen.getCell(1).cs);
assertEquals(ExtendedAttribute.DB_LEFT, dbcsScreen.getCell(1).db);
assertEquals(ExtendedAttribute.DB_RIGHT, dbcsScreen.getCell(2).db);
// Delete DBCS character at left half
boolean deleted = dbcsScreen.deleteChar(1);
assertTrue(deleted);
assertEquals(0, dbcsScreen.getCell(1).ec);
assertEquals(0, dbcsScreen.getCell(2).ec);
// Test balanceSOSI removes adjacent empty SO SI
dbcsScreen.setCell(3, 0x0E); // SO
dbcsScreen.setCell(4, 0x0F); // SI
dbcsScreen.balanceSOSI();
assertEquals(0, dbcsScreen.getCell(3).ec);
assertEquals(0, dbcsScreen.getCell(4).ec);
}
@Test
public void testWrappedFieldListHandling() {
screen.erase(false);
// Field 1 at pos 100..499
screen.setFieldAttribute(100, (byte) FA_PRINTABLE);
// Field 2 at pos 500..99 (wrapping around end of buffer)
screen.setFieldAttribute(500, (byte) (FA_PRINTABLE | FA_PROTECT));
ECLFieldList list = screen.buildFieldList();
assertEquals(2, list.getFieldCount());
ECLField f1 = list.getFirstField();
assertNotNull(f1);
assertEquals(100, f1.getStart());
assertEquals(101, f1.getDataStart());
assertEquals(499, f1.getEnd());
assertEquals(399, f1.getLength());
assertFalse(f1.isWrapped());
assertFalse(f1.isProtected());
ECLField f2 = list.getNextField(f1);
assertNotNull(f2);
assertEquals(500, f2.getStart());
assertEquals(501, f2.getDataStart());
assertEquals(99, f2.getEnd());
assertEquals(1920 - 500 - 1 + 100, f2.getLength());
assertTrue(f2.isWrapped());
assertTrue(f2.isProtected());
// Test containment across wrap
assertTrue(f2.contains(500));
assertTrue(f2.contains(501));
assertTrue(f2.contains(1919));
assertTrue(f2.contains(0));
assertTrue(f2.contains(99));
assertFalse(f2.contains(100));
assertFalse(f2.contains(200));
// Test field lookup by position
assertEquals(f2.getStart(), list.findField(50).getStart());
assertEquals(f1.getStart(), list.findField(200).getStart());
assertEquals(f2.getStart(), list.findField(600).getStart());
// Test navigation
assertEquals(f2.getStart(), list.getPreviousField(f1).getStart());
assertNull(list.getNextField(f2));
}
@Test
public void testSingleFieldFullBufferWrap() {
screen.erase(false);
screen.setFieldAttribute(250, (byte) FA_PRINTABLE);
ECLFieldList list = screen.buildFieldList();
assertEquals(1, list.getFieldCount());
ECLField f = list.getFirstField();
assertNotNull(f);
assertEquals(250, f.getStart());
assertEquals(251, f.getDataStart());
assertEquals(249, f.getEnd());
assertEquals(1919, f.getLength());
assertTrue(f.isWrapped());
assertTrue(f.contains(0));
assertTrue(f.contains(250));
assertTrue(f.contains(1919));
}
}
@@ -241,4 +241,51 @@ public class ProxyConnectionTest {
connection.disconnect();
}
}
@Test
public void testSystemPropertiesProxyFallback() throws Exception {
try (ServerSocket proxyServer = new ServerSocket(0)) {
int proxyPort = proxyServer.getLocalPort();
CountDownLatch serverHandshakeDone = new CountDownLatch(1);
Thread serverThread = new Thread(() -> {
try (Socket clientSock = proxyServer.accept()) {
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSock.getInputStream(), StandardCharsets.US_ASCII));
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) break;
}
OutputStream out = clientSock.getOutputStream();
out.write("HTTP/1.1 200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII));
out.flush();
serverHandshakeDone.countDown();
} catch (Exception ignored) {}
});
serverThread.setDaemon(true);
serverThread.start();
String origHost = System.getProperty("http.proxyHost");
String origPort = System.getProperty("http.proxyPort");
try {
System.setProperty("http.proxyHost", "127.0.0.1");
System.setProperty("http.proxyPort", String.valueOf(proxyPort));
ConnectionConfig config = new ConnectionConfig("test.fallback.org", 23);
config.setConnectTimeoutMs(5000);
TelnetFSM fsm = new TelnetFSM(config, new haus.nightmare.lib3270j.screen.ScreenBuffer(TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator()), null);
TelnetConnection connection = new TelnetConnection(config, fsm);
connection.connect();
assertTrue(connection.isConnected());
assertTrue(serverHandshakeDone.await(3, TimeUnit.SECONDS));
connection.disconnect();
} finally {
if (origHost != null) System.setProperty("http.proxyHost", origHost);
else System.clearProperty("http.proxyHost");
if (origPort != null) System.setProperty("http.proxyPort", origPort);
else System.clearProperty("http.proxyPort");
}
}
}
}