Cleanup datastream and Graphics
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 36s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m13s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m34s

This commit is contained in:
2026-09-18 00:55:54 +00:00
parent ef70ea0f5a
commit badd026055
9 changed files with 932 additions and 31 deletions
@@ -0,0 +1,258 @@
package haus.nightmare.lib3270j.datastream;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLConstants;
import haus.nightmare.lib3270j.ecl.ECLOIA;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.util.concurrent.atomic.AtomicInteger;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* Phase 19 verification test suite:
* - Field Outlining Extended Attribute (0xC2 / TY_FOUTLINE)
* - Field Validation Extended Attribute (0xC1 / TY_FVALIDATE)
* - Blinking attributes and suppression
* - Light Pen & Cursor Select Field Interaction
*/
public class Phase19ExtendedAttributesTest {
private ScreenBuffer screen;
private DataStreamProcessor processor;
private InputProcessor inputProcessor;
private ECLOIA oia;
private ByteArrayOutputStream output;
private AtomicInteger sentAid;
@BeforeEach
public void setUp() {
EbcdicTranslator translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
processor = new DataStreamProcessor(screen, translator);
output = new ByteArrayOutputStream();
processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length));
inputProcessor = new InputProcessor(screen, translator, null);
oia = new ECLOIA(screen, inputProcessor, null);
inputProcessor.setOIA(oia);
sentAid = new AtomicInteger(-1);
inputProcessor.setOutputSender(bytes -> {
if (bytes.length > 0) {
sentAid.set(bytes[0] & 0xFF);
}
});
}
@Test
public void testOutliningConstantsAndParsing() {
// Verify bitmask constants in ExtendedAttribute
assertEquals((byte) 0x01, ExtendedAttribute.OL_UNDERLINE);
assertEquals((byte) 0x02, ExtendedAttribute.OL_RIGHT);
assertEquals((byte) 0x04, ExtendedAttribute.OL_OVERLINE);
assertEquals((byte) 0x08, ExtendedAttribute.OL_LEFT);
assertEquals((byte) 0x0F, ExtendedAttribute.OL_BOX);
// Parse SFE record with Outlining (0xC2) = 0x0F (Full box)
byte[] sfeRecord = new byte[] {
(byte) CMD_EW, 0x00,
(byte) ORDER_SFE, 0x02,
(byte) XA_3270, (byte) FA_PRINTABLE,
(byte) XA_OUTLINING, (byte) ExtendedAttribute.OL_BOX
};
processor.processRecord(sfeRecord, 0, sfeRecord.length, true);
ExtendedAttribute cell = screen.getCell(0);
assertTrue(cell.isFieldAttribute());
assertEquals(ExtendedAttribute.OL_BOX, cell.ol);
// Parse SA order with Outlining (0xC2) = 0x05 (Underline + Overline)
byte[] saRecord = new byte[] {
(byte) CMD_W, 0x00,
(byte) ORDER_SBA, 0x00, 0x01,
(byte) ORDER_SA, (byte) XA_OUTLINING, (byte) (ExtendedAttribute.OL_UNDERLINE | ExtendedAttribute.OL_OVERLINE),
(byte) 0xC1 // 'A'
};
processor.processRecord(saRecord, 0, saRecord.length, true);
ExtendedAttribute charCell = screen.getCell(1);
assertFalse(charCell.isFieldAttribute());
assertEquals((byte) (ExtendedAttribute.OL_UNDERLINE | ExtendedAttribute.OL_OVERLINE), charCell.ol);
}
@Test
public void testMandatoryEntryValidation() {
// Set up field 1 at pos 0 (unprotected, length 5, Mandatory Entry: 0x02)
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.getCell(0).vl = ExtendedAttribute.VL_MANDATORY_ENTRY;
// Set up field 2 at pos 6 (terminator / protected)
screen.setFieldAttribute(6, (byte) (FA_PROTECT | FA_NUMERIC));
// Screen cursor at start of field 1
screen.setCursorAddress(1);
// Attempt to send AID_ENTER while field is empty -> Must fail validation
inputProcessor.sendAid(AID_ENTER);
assertEquals(-1, sentAid.get(), "AID_ENTER must NOT be transmitted when Mandatory Entry is violated");
assertTrue(inputProcessor.isKeyboardLocked(), "Keyboard must be locked on validation error");
assertEquals(ECLConstants.INHIBIT_OPERATOR_DUE, oia.getInputInhibited(), "OIA must indicate Operator Due inhibit");
// Unlock / reset
inputProcessor.reset();
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
assertFalse(inputProcessor.isKeyboardLocked());
// Now enter a character in field 1
screen.setCursorAddress(1);
inputProcessor.processChar('X');
// Attempt to send AID_ENTER again -> Should succeed
inputProcessor.sendAid(AID_ENTER);
assertEquals(AID_ENTER, sentAid.get(), "AID_ENTER must be transmitted after entering required character");
}
@Test
public void testMandatoryFillValidation() {
// Set up field 1 at pos 0 (unprotected, length 4: pos 1, 2, 3, 4, Mandatory Fill: 0x01)
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.getCell(0).vl = ExtendedAttribute.VL_MANDATORY_FILL;
// Set up field 2 at pos 5 (unprotected, length 4: pos 6, 7, 8, 9)
screen.setFieldAttribute(5, (byte) FA_PRINTABLE);
// Terminator field at pos 10
screen.setFieldAttribute(10, (byte) (FA_PROTECT | FA_NUMERIC));
// Type only 2 characters out of 4 into field 1 (partial fill)
screen.setCursorAddress(1);
inputProcessor.processChar('A');
inputProcessor.processChar('B');
// 1. Attempt to tab away from incomplete field 1 -> Must inhibit
inputProcessor.tab();
assertEquals(ECLConstants.INHIBIT_OPERATOR_DUE, oia.getInputInhibited(), "Tab away from incomplete Mandatory Fill must inhibit");
assertTrue(inputProcessor.isKeyboardLocked());
// Cursor should remain in field 1
int curAddr = screen.getCursorAddress();
assertTrue(curAddr >= 1 && curAddr <= 4, "Cursor must remain within incomplete field");
// Reset
inputProcessor.reset();
// 2. Attempt to send AID_ENTER with incomplete field 1 -> Must inhibit
inputProcessor.sendAid(AID_ENTER);
assertEquals(-1, sentAid.get(), "AID_ENTER must NOT transmit when Mandatory Fill field is incomplete");
assertEquals(ECLConstants.INHIBIT_OPERATOR_DUE, oia.getInputInhibited());
assertTrue(inputProcessor.isKeyboardLocked());
// Reset
inputProcessor.reset();
// 3. Complete field 1 (type remaining 2 characters)
screen.setCursorAddress(3);
inputProcessor.processChar('C');
inputProcessor.processChar('D');
// Set cursor back to field 1 (pos 1) to test tabbing to field 2 (pos 6)
screen.setCursorAddress(1);
inputProcessor.tab();
assertEquals(6, screen.getCursorAddress(), "Tab must succeed to next field once Mandatory Fill is complete");
// Sending AID_ENTER should now succeed
inputProcessor.sendAid(AID_ENTER);
assertEquals(AID_ENTER, sentAid.get(), "AID_ENTER must transmit once all validation requirements are met");
}
@Test
public void testTriggerFieldAutoAid() {
// Set up field at pos 0 (unprotected, length 3: pos 1, 2, 3, Trigger: 0x04)
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.getCell(0).vl = ExtendedAttribute.VL_TRIGGER;
// Terminator at pos 4
screen.setFieldAttribute(4, (byte) (FA_PROTECT | FA_NUMERIC));
screen.setCursorAddress(1);
// Type 1st char: no AID
inputProcessor.processChar('1');
assertEquals(-1, sentAid.get());
// Type 2nd char: no AID
inputProcessor.processChar('2');
assertEquals(-1, sentAid.get());
// Type 3rd (last) char: should automatically trigger AID_ENTER!
inputProcessor.processChar('3');
assertEquals(AID_ENTER, sentAid.get(), "Typing into the last position of a Trigger field must automatically send AID_ENTER");
}
@Test
public void testShortReadAidBypassesValidation() {
// Field with Mandatory Entry at pos 0
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.getCell(0).vl = ExtendedAttribute.VL_MANDATORY_ENTRY;
screen.setFieldAttribute(5, (byte) (FA_PROTECT | FA_NUMERIC));
screen.setCursorAddress(1);
// CLEAR AID must bypass validation
inputProcessor.sendAid(AID_CLEAR);
assertEquals(AID_CLEAR, sentAid.get());
// Reset
inputProcessor.reset();
sentAid.set(-1);
// PA1 AID must bypass validation
inputProcessor.sendAid(AID_PA1);
assertEquals(AID_PA1, sentAid.get());
}
@Test
public void testCursorSelectAndDeferredToggle() {
// Detectable selectable field at pos 0 (FA_SELECTABLE: bits 4-5 = 01 or 11)
screen.setFieldAttribute(0, (byte) (FA_PRINTABLE | 0x08)); // FA_PRINTABLE (0xC0 | 0x00) with detectable bits
// Designator at pos 1: '?' (0x6F)
ExtendedAttribute desCell = screen.getCell(1);
desCell.ec = 0x6F;
desCell.ucs4 = '?';
screen.setCursorAddress(1);
// Toggle via cursorSelect()
boolean selected = inputProcessor.cursorSelect();
assertTrue(selected);
assertEquals('>', desCell.ucs4, "Cursor select on '?' must toggle to '>'");
assertTrue((screen.getCell(0).fa & FA_MODIFY) != 0, "Toggling to '>' must set MDT");
// Toggle back to '?'
selected = inputProcessor.cursorSelect();
assertTrue(selected);
assertEquals('?', desCell.ucs4, "Cursor select on '>' must toggle back to '?'");
assertFalse((screen.getCell(0).fa & FA_MODIFY) != 0, "Toggling to '?' must clear MDT");
// Immediate select space designator at pos 5
screen.setFieldAttribute(5, (byte) (FA_PRINTABLE | 0x08));
ExtendedAttribute spaceDesCell = screen.getCell(6);
spaceDesCell.ec = 0x40;
spaceDesCell.ucs4 = ' ';
screen.setCursorAddress(6);
selected = inputProcessor.cursorSelect();
assertTrue(selected);
assertEquals(AID_SELECT, sentAid.get(), "Immediate space designator selection must transmit AID_SELECT (0x7E)");
}
}
@@ -0,0 +1,343 @@
package haus.nightmare.lib3270j.datastream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.graphics.*;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import java.io.ByteArrayOutputStream;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class QueryReplyPhase18Test {
private EbcdicTranslator translator;
private ScreenBuffer screen;
private QueryReplyBuilder qrBuilder;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
qrBuilder = new QueryReplyBuilder(screen);
}
@Test
public void testBuildCharsetsInBothAndProgrammedSymbolsModes() {
// 1. In GraphicsMode.BOTH, buildCharsets must advertise LoadPS 0x0A (Format 1 & 3) and PSA..PSF
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
byte[] bothCharsets = qrBuilder.buildCharsets();
assertNotNull(bothCharsets);
assertEquals(65, bothCharsets.length, "buildCharsets in BOTH mode must be 65 bytes");
assertEquals((byte) 0xA2, bothCharsets[0], "Flags1 must be 0xA2 (GE | PS | CGCSGID)");
assertEquals(0x0A, bothCharsets[4] & 0xFF, "LoadPS format must be 0x0A (Format 1 and 3)");
assertEquals(7, bothCharsets[8] & 0xFF, "Descriptor length must be 7");
// Verify all 6 loadable sets PSA..PSF
// Descriptor offset starts at index 9:
// Set 0 (Base): idx 9
assertEquals(0x00, bothCharsets[9] & 0xFF);
assertEquals(0x10, bothCharsets[10] & 0xFF);
// Set 1 (APL): idx 16
assertEquals(0x01, bothCharsets[16] & 0xFF);
assertEquals(0x00, bothCharsets[17] & 0xFF);
// Slot 2: PSA (LCID 0x40, single plane 0x80)
assertEquals(0x02, bothCharsets[23] & 0xFF);
assertEquals((byte) 0x80, bothCharsets[24]);
assertEquals(0x40, bothCharsets[25] & 0xFF);
// Slot 3: PSB (LCID 0x41, single plane 0x80)
assertEquals(0x03, bothCharsets[30] & 0xFF);
assertEquals((byte) 0x80, bothCharsets[31]);
assertEquals(0x41, bothCharsets[32] & 0xFF);
// Slot 4: PSC (LCID 0x42, triple plane 0xC0)
assertEquals(0x04, bothCharsets[37] & 0xFF);
assertEquals((byte) 0xC0, bothCharsets[38]);
assertEquals(0x42, bothCharsets[39] & 0xFF);
// Slot 5: PSD (LCID 0x43, triple plane 0xC0)
assertEquals(0x05, bothCharsets[44] & 0xFF);
assertEquals((byte) 0xC0, bothCharsets[45]);
assertEquals(0x43, bothCharsets[46] & 0xFF);
// Slot 6: PSE (LCID 0x44, triple plane 0xC0)
assertEquals(0x06, bothCharsets[51] & 0xFF);
assertEquals((byte) 0xC0, bothCharsets[52]);
assertEquals(0x44, bothCharsets[53] & 0xFF);
// Slot 7: PSF (LCID 0x45, triple plane 0xC0)
assertEquals(0x07, bothCharsets[58] & 0xFF);
assertEquals((byte) 0xC0, bothCharsets[59]);
assertEquals(0x45, bothCharsets[60] & 0xFF);
// 2. In GraphicsMode.PROGRAMMED_SYMBOLS, buildCharsets must also be 65 bytes with LoadPS 0x0A,
// but default charHeight is 12 (3279-2) instead of 16 (3179G)
qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS);
byte[] psCharsets = qrBuilder.buildCharsets();
assertEquals(65, psCharsets.length);
assertEquals((byte) 0xA2, psCharsets[0]);
assertEquals(12, psCharsets[3] & 0xFF, "Programmed Symbols only charHeight is 12 (3279-2)");
assertEquals(0x0A, psCharsets[4] & 0xFF);
// Descriptors (bytes 8..64) should be identical
for (int i = 8; i < 65; i++) {
assertEquals(bothCharsets[i], psCharsets[i], "Descriptor byte at index " + i + " must match");
}
}
@Test
public void testBuildCharsetsInNoneAndVectorOnlyModes() {
qrBuilder.setGraphicsMode(GraphicsMode.NONE);
byte[] noneCharsets = qrBuilder.buildCharsets();
assertEquals(23, noneCharsets.length);
assertEquals((byte) 0x82, noneCharsets[0]);
assertEquals(0x00, noneCharsets[4] & 0xFF, "LoadPS format must be 0x00 in NONE mode");
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
byte[] vecCharsets = qrBuilder.buildCharsets();
assertEquals(23, vecCharsets.length);
assertEquals((byte) 0x82, vecCharsets[0]);
assertEquals(0x00, vecCharsets[4] & 0xFF, "LoadPS format must be 0x00 in VECTOR_GRAPHICS mode");
}
@Test
public void testLoadpsWithRwsAsLcidOrSlotIds() {
ProgramSymbolManager psm = new ProgramSymbolManager();
// 1. Load PSA (LCID 0x40) with RWS = 0x40 (using LCID as slot ID)
byte[] payload1 = new byte[4 + 18];
payload1[0] = 0x01; // Format 1
payload1[1] = 0x40; // LCID 0x40
payload1[2] = 0x41; // Codepoint 0x41
payload1[3] = 0x40; // RWS passed as 0x40 (PSA)
payload1[4] = (byte) 0x80; // Pixel (0,0) = 1
psm.loadps(payload1);
ProgramSymbolSet setA = psm.getSymbolSet(0x40);
assertNotNull(setA, "PSA symbol set must be allocated");
assertFalse(setA.isTriplePlane());
ProgramSymbolSet.SymbolSlot slotA = psm.getSymbol(0x40, 0x41);
assertNotNull(slotA);
assertEquals(1, slotA.getPixelData()[0] & 0xFF);
// 2. Load PSC (LCID 0x42) with RWS = 0x42 (Triple Plane)
byte[] payload2 = new byte[4 + 18];
payload2[0] = 0x01; // Format 1
payload2[1] = 0x42; // LCID 0x42
payload2[2] = 0x41; // Codepoint 0x41
payload2[3] = 0x42; // RWS passed as 0x42 (PSC)
payload2[4] = (byte) 0x80; // Pixel (0,0) = 1
psm.loadps(payload2);
ProgramSymbolSet setC = psm.getSymbolSet(0x42);
assertNotNull(setC, "PSC symbol set must be allocated");
assertTrue(setC.isTriplePlane(), "PSC must be recognized as triple-plane");
ProgramSymbolSet.SymbolSlot slotC = psm.getSymbol(0x42, 0x41);
assertNotNull(slotC);
assertTrue(slotC.isTriplePlane());
}
@Test
public void testLoadpsFormat3VariableDimensions() {
ProgramSymbolManager psm = new ProgramSymbolManager();
// Load symbol set PSE (LCID 0x44) with Format 3 and custom cell dimensions 12x24
int w = 12;
int h = 24;
int bytesPerSymbol = (w * h + 7) / 8; // (288 + 7) / 8 = 36 bytes
byte[] payload = new byte[4 + 6 + bytesPerSymbol];
payload[0] = (byte) 0x83; // Format 3, extHeader = true
payload[1] = 0x44; // LCID 0x44 (PSE)
payload[2] = 0x41; // Codepoint 0x41
payload[3] = 0x06; // RWS 6 (PSE)
// Ext header (6 bytes):
payload[4] = 0x06; // Ext header length = 6
payload[5] = 0x00; // reserved
payload[6] = (byte) w; // LW = 12
payload[7] = (byte) h; // LH = 24
payload[8] = 0x00; // reserved
payload[9] = 0x00; // colorPlane = 0 (all planes)
// Set pixel at (0, 0)
payload[10] = (byte) 0x80;
// Set pixel at (11, 23) -> index 12 * 24 - 1 = 287 -> byte 287 / 8 = 35, bit 7 - (287 % 8) = 0
payload[10 + 35] = (byte) 0x01;
psm.loadps(payload);
ProgramSymbolSet set = psm.getSymbolSet(0x44);
assertNotNull(set);
ProgramSymbolSet.SymbolSlot slot = psm.getSymbol(0x44, 0x41);
assertNotNull(slot);
assertEquals(12, slot.getWidth());
assertEquals(24, slot.getHeight());
assertTrue(slot.isTriplePlane());
byte[] pix = slot.getPixelData();
assertEquals(12 * 24, pix.length);
assertEquals(7, pix[0] & 0xFF, "Pixel (0,0) should be 7 (all planes)");
assertEquals(7, pix[12 * 24 - 1] & 0xFF, "Pixel (11,23) should be 7 (all planes)");
assertEquals(0, pix[1] & 0xFF, "Unset pixel (1,0) should be 0");
}
@Test
public void testTriplePlaneMultiColorCompositeMerging() {
ProgramSymbolManager psm = new ProgramSymbolManager();
// Load 9x16 Format 1 glyph for PSC (LCID 0x42) using 3 planes:
// Plane 1: Red at pixel (0,0) and (1,0)
byte[] p1 = new byte[4 + 6 + 18];
p1[0] = (byte) 0x81; p1[1] = 0x42; p1[2] = 0x41; p1[3] = 0x04; p1[4] = 0x06;
p1[9] = 0x01; // Plane 1 = Red
p1[10] = (byte) 0x80; // Row 0 Col 0
p1[12] = (byte) 0x80; // Row 0 Col 1
psm.loadps(p1);
// Plane 2: Green at pixel (1,0) and (2,0)
byte[] p2 = new byte[4 + 6 + 18];
p2[0] = (byte) 0x81; p2[1] = 0x42; p2[2] = 0x41; p2[3] = 0x04; p2[4] = 0x06;
p2[9] = 0x02; // Plane 2 = Green
p2[12] = (byte) 0xC0; // Row 0 Col 1, 2
psm.loadps(p2);
// Plane 4: Blue at pixel (0,0), (1,0), (3,0)
byte[] p3 = new byte[4 + 6 + 18];
p3[0] = (byte) 0x81; p3[1] = 0x42; p3[2] = 0x41; p3[3] = 0x04; p3[4] = 0x06;
p3[9] = 0x04; // Plane 4 = Blue
p3[10] = (byte) 0x80; // Row 0 Col 0
p3[12] = (byte) 0xA0; // Row 0 Col 1, 3
psm.loadps(p3);
ProgramSymbolSet.SymbolSlot slot = psm.getSymbol(0x42, 0x41);
assertNotNull(slot);
byte[] pix = slot.getPixelData();
// Pixel (0,0): Red (1) | Blue (4) = 5 (Pink)
assertEquals(5, pix[0] & 0xFF);
// Pixel (1,0): Red (1) | Green (2) | Blue (4) = 7 (White)
assertEquals(7, pix[1] & 0xFF);
// Pixel (2,0): Green (2) alone
assertEquals(2, pix[2] & 0xFF);
// Pixel (3,0): Blue (4) alone
assertEquals(4, pix[3] & 0xFF);
// Verify composite ARGB colors:
int[] rgb = slot.getRgbPixels(0, 0);
assertEquals(GocaConstants.GOCA_COLORS[3], rgb[0], "Color at (0,0) should be Pink (GOCA index 3)");
assertEquals(GocaConstants.GOCA_COLORS[7], rgb[1], "Color at (1,0) should be White (GOCA index 7)");
assertEquals(GocaConstants.GOCA_COLORS[4], rgb[2], "Color at (2,0) should be Green (GOCA index 4)");
assertEquals(GocaConstants.GOCA_COLORS[1], rgb[3], "Color at (3,0) should be Blue (GOCA index 1)");
}
@Test
public void testGocaQueryRepliesBinaryLayout() {
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
// 1. QR_SEGMENT (0xB0)
byte[] seg = qrBuilder.buildSegment();
assertEquals(7, seg.length);
assertEquals((byte) 0x80, seg[0]);
assertEquals(0x02, seg[1]);
assertEquals(0x00, seg[2]);
assertEquals(0x00, seg[3]);
assertEquals(0x00, seg[4]);
assertEquals((byte) 0xFC, seg[5]);
assertEquals(0x00, seg[6]);
// 2. QR_PROCEDURE (0xB1)
byte[] proc = qrBuilder.buildProcedure();
assertEquals(17, proc.length);
assertEquals(0x00, proc[0]);
assertEquals(0x01, proc[1]);
assertEquals(0x00, proc[2]);
assertEquals(0x00, proc[3]);
assertEquals(0x00, proc[4]);
assertEquals((byte) 0xFC, proc[5]);
assertEquals(0x00, proc[6]);
// 3. QR_LINETYPE (0xB2)
byte[] lt = qrBuilder.buildLineType();
assertEquals(20, lt.length);
assertEquals(0x00, lt[0]);
assertEquals(0x09, lt[1]);
assertEquals(0x00, lt[2]);
assertEquals(0x07, lt[3]);
for (int i = 1; i <= 8; i++) {
assertEquals(i, lt[4 + (i - 1) * 2] & 0xFF);
assertEquals(i, lt[4 + (i - 1) * 2 + 1] & 0xFF);
}
// 4. QR_PORT (0xB3)
byte[] port = qrBuilder.buildPort();
assertEquals(64, port.length);
// 5. QR_GRCOLOR (0xB4)
byte[] grColor = qrBuilder.buildGrColor();
assertEquals(105, grColor.length); // 9 header + 16 * 6 = 105 bytes payload
// 6. QR_GRSYMBOLSET (0xB6)
byte[] grSym = qrBuilder.buildGrSymbolSet();
assertEquals(29, grSym.length);
// Trailing 7 bytes must be zero matching HoD QR_GRSYMBOLSET_S_STRING
for (int i = 22; i < 29; i++) {
assertEquals(0x00, grSym[i], "Trailing byte at index " + i + " must be 0");
}
}
@Test
public void testDataStreamProcessorStructuredField0x06Dispatch() {
ScreenBuffer s = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
DataStreamProcessor dsp = new DataStreamProcessor(s, translator);
AtomicBoolean updated = new AtomicBoolean(false);
dsp.addScreenUpdateListener(() -> updated.set(true));
// Construct WSF with SF 0x06 (LoadPS direct)
// Length = 2 bytes (3 + 4 + 18 = 25 = 0x0019), SFID = 0x06, followed by 4+18 byte LoadPS payload
byte[] wsf = new byte[1 + 25];
wsf[0] = (byte) CMD_WSF; // WSF command (0x11)
wsf[1] = 0x00;
wsf[2] = 0x19; // length = 25
wsf[3] = 0x06; // SF_LOADPS_DIRECT (0x06)
// Payload:
wsf[4] = 0x01; // Format 1
wsf[5] = 0x40; // LCID 0x40 (PSA)
wsf[6] = 0x41; // Codepoint 0x41
wsf[7] = 0x02; // RWS 2
wsf[8] = (byte) 0x80; // Row 0 Col 0 bit set
dsp.processRecord(wsf, 0, wsf.length, true);
assertTrue(updated.get(), "Screen update listener must be triggered after SF 0x06");
ProgramSymbolSet.SymbolSlot slot = dsp.getProgramSymbolManager().getSymbol(0x40, 0x41);
assertNotNull(slot, "Symbol 0x41 must be loaded in PSA");
assertEquals(1, slot.getPixelData()[0] & 0xFF);
}
@Test
public void testGraphicsPlanePatternFillWithTriplePlanePss() {
GraphicsPlane plane = new GraphicsPlane(80, 24);
ProgramSymbolManager psm = new ProgramSymbolManager();
plane.setProgramSymbolManager(psm);
// Load 9x16 Format 1 glyph for PSC (LCID 0x42) with Red (1) and Blue (4) = Pink (5)
byte[] p = new byte[4 + 6 + 18];
p[0] = (byte) 0x81; p[1] = 0x42; p[2] = 0x41; p[3] = 0x04; p[4] = 0x06;
p[9] = 0x01; // Plane 1 = Red
p[10] = (byte) 0xC0; // Row 0 and Row 1, Col 0
psm.loadps(p);
byte[] pBlue = new byte[4 + 6 + 18];
pBlue[0] = (byte) 0x81; pBlue[1] = 0x42; pBlue[2] = 0x41; pBlue[3] = 0x04; pBlue[4] = 0x06;
pBlue[9] = 0x04; // Plane 4 = Blue
pBlue[10] = (byte) 0xC0; // Row 0 and Row 1, Col 0
psm.loadps(pBlue);
// Fill a small 9x16 rectangle using patternSet = 0x42, pattern = 0x41
int[] px = new int[]{0, 9, 9, 0};
int[] py = new int[]{0, 0, 16, 16};
plane.fillArea(px, py, 4, new int[]{4}, 1, 0xFFFFFFFF, 0x42, 0x41, false, 0, 0, 1, 0, 0xFF000000, GocaConstants.FILL_RULE_EVEN_ODD);
// Pixel (0,1) must have been rendered with Pink composite color (GOCA_COLORS[3]), not flat foreground 0xFFFFFFFF!
int renderedPixel = plane.getPixel(0, 1);
assertEquals(GocaConstants.GOCA_COLORS[3], renderedPixel, "Triple plane pattern fill must render with composite color (Pink)");
}
}