Add tests
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
import haus.nightmare.j3270.ui.TerminalPanel;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.nvt.NvtProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.file.Files;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Verification test suite for Phase 27:
|
||||
* Build Hygiene, Release Versioning & Zero-Risk Pruning (v1.2.4)
|
||||
*/
|
||||
public class Phase27BuildAndRenderTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 27.1 & 27.2: Release version 1.2.4 synchronized across build files and android.useAndroidX removed")
|
||||
public void testVersionSynchronizationAndGradleProperties() throws Exception {
|
||||
File projectRoot = new File(".").getCanonicalFile();
|
||||
if (!new File(projectRoot, "settings.gradle").exists() && projectRoot.getParentFile() != null) {
|
||||
projectRoot = projectRoot.getParentFile();
|
||||
}
|
||||
|
||||
// 1. Verify build.gradle version = '1.2.4'
|
||||
File buildGradle = new File(projectRoot, "build.gradle");
|
||||
assertTrue(buildGradle.exists(), "build.gradle must exist");
|
||||
String buildGradleContent = Files.readString(buildGradle.toPath());
|
||||
assertTrue(buildGradleContent.contains("version = '1.2.4'"), "build.gradle must define version = '1.2.4'");
|
||||
assertFalse(buildGradleContent.contains("0.1.0"), "build.gradle must not contain 0.1.0");
|
||||
|
||||
// 2. Verify build_all.sh Implementation-Version: 1.2.4
|
||||
File buildAll = new File(projectRoot, "build_all.sh");
|
||||
assertTrue(buildAll.exists(), "build_all.sh must exist");
|
||||
String buildAllContent = Files.readString(buildAll.toPath());
|
||||
assertTrue(buildAllContent.contains("Implementation-Version: 1.2.4"), "build_all.sh must specify Implementation-Version: 1.2.4");
|
||||
assertFalse(buildAllContent.contains("Implementation-Version: 0.1.0"), "build_all.sh must not contain 0.1.0");
|
||||
|
||||
// 3. Verify setup.ps1 Implementation-Version: 1.2.4
|
||||
File setupPs1 = new File(projectRoot, "setup.ps1");
|
||||
assertTrue(setupPs1.exists(), "setup.ps1 must exist");
|
||||
String setupPs1Content = Files.readString(setupPs1.toPath());
|
||||
assertTrue(setupPs1Content.contains("Implementation-Version: 1.2.4"), "setup.ps1 must specify Implementation-Version: 1.2.4");
|
||||
assertFalse(setupPs1Content.contains("Implementation-Version: 0.1.0"), "setup.ps1 must not contain 0.1.0");
|
||||
|
||||
// 4. Verify setup.sh Implementation-Version: 1.2.4
|
||||
File setupSh = new File(projectRoot, "setup.sh");
|
||||
assertTrue(setupSh.exists(), "setup.sh must exist");
|
||||
String setupShContent = Files.readString(setupSh.toPath());
|
||||
assertTrue(setupShContent.contains("Implementation-Version: 1.2.4"), "setup.sh must specify Implementation-Version: 1.2.4");
|
||||
assertFalse(setupShContent.contains("Implementation-Version: 0.1.0"), "setup.sh must not contain 0.1.0");
|
||||
|
||||
// 5. Verify gradle.properties android.useAndroidX removed
|
||||
File gradleProps = new File(projectRoot, "gradle.properties");
|
||||
assertTrue(gradleProps.exists(), "gradle.properties must exist");
|
||||
String gradlePropsContent = Files.readString(gradleProps.toPath());
|
||||
assertFalse(gradlePropsContent.contains("android.useAndroidX"), "gradle.properties must not contain android.useAndroidX");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 27.3: T1-B Static Color & BasicStroke constants promoted in TerminalPanel")
|
||||
public void testHotspotHoverStaticConstants() throws Exception {
|
||||
Field colorField = TerminalPanel.class.getDeclaredField("HOTSPOT_HOVER_COLOR");
|
||||
colorField.setAccessible(true);
|
||||
assertTrue(Modifier.isStatic(colorField.getModifiers()), "HOTSPOT_HOVER_COLOR must be static");
|
||||
assertTrue(Modifier.isFinal(colorField.getModifiers()), "HOTSPOT_HOVER_COLOR must be final");
|
||||
Color hoverColor = (Color) colorField.get(null);
|
||||
assertEquals(new Color(0, 220, 255), hoverColor, "HOTSPOT_HOVER_COLOR must match bright cyan (0, 220, 255)");
|
||||
|
||||
Field strokeField = TerminalPanel.class.getDeclaredField("HOTSPOT_HOVER_STROKE");
|
||||
strokeField.setAccessible(true);
|
||||
assertTrue(Modifier.isStatic(strokeField.getModifiers()), "HOTSPOT_HOVER_STROKE must be static");
|
||||
assertTrue(Modifier.isFinal(strokeField.getModifiers()), "HOTSPOT_HOVER_STROKE must be final");
|
||||
Stroke hoverStroke = (Stroke) strokeField.get(null);
|
||||
assertNotNull(hoverStroke, "HOTSPOT_HOVER_STROKE must not be null");
|
||||
assertTrue(hoverStroke instanceof BasicStroke, "HOTSPOT_HOVER_STROKE must be BasicStroke");
|
||||
assertEquals(2.0f, ((BasicStroke) hoverStroke).getLineWidth(), 0.001f, "HOTSPOT_HOVER_STROKE line width must be 2.0f");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 27.4: T1-A charScratch Buffer exists and non-ASCII render renders without exceptions")
|
||||
public void testCharScratchAndNonAsciiRendering() throws Exception {
|
||||
Field scratchField = TerminalPanel.class.getDeclaredField("charScratch");
|
||||
scratchField.setAccessible(true);
|
||||
assertFalse(Modifier.isStatic(scratchField.getModifiers()), "charScratch should be an instance field confined to EDT");
|
||||
assertTrue(Modifier.isFinal(scratchField.getModifiers()), "charScratch should be final");
|
||||
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
char[] scratch = (char[]) scratchField.get(panel);
|
||||
assertNotNull(scratch, "charScratch must not be null");
|
||||
assertEquals(1, scratch.length, "charScratch must be length 1");
|
||||
|
||||
// Set up client with non-ASCII characters (e.g. U+2500 box drawing, U+00E9 acute e, U+2550 double box)
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Put non-ASCII characters on screen
|
||||
sb.getCell(0).ucs4 = 0x2500; // Box drawing horizontal line
|
||||
sb.getCell(1).ucs4 = 0x00E9; // e with acute
|
||||
sb.getCell(2).ucs4 = 0x2550; // Box drawing double horizontal
|
||||
sb.getCell(3).ucs4 = 0x2308; // APL left ceiling
|
||||
sb.updateDisplaySnapshot();
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
BufferedImage img = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2 = img.createGraphics();
|
||||
assertDoesNotThrow(() -> panel.paint(g2), "paint must render non-ASCII cells via drawChars without error");
|
||||
g2.dispose();
|
||||
|
||||
// Check that charScratch was used
|
||||
assertTrue(scratch[0] > 0, "charScratch should have held the last drawn non-ASCII character");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 27.5: T1-C Reusable NVT Single-Byte Buffer dispatches without allocations")
|
||||
public void testReusableNvtSingleByteBuffer() throws Exception {
|
||||
Field scratchField = TelnetFSM.class.getDeclaredField("nvtSingleByteScratch");
|
||||
scratchField.setAccessible(true);
|
||||
assertFalse(Modifier.isStatic(scratchField.getModifiers()), "nvtSingleByteScratch must be an instance field");
|
||||
assertTrue(Modifier.isFinal(scratchField.getModifiers()), "nvtSingleByteScratch must be final");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
TelnetFSM fsm = client.getTelnetFSM();
|
||||
byte[] expectedScratch = (byte[]) scratchField.get(fsm);
|
||||
assertNotNull(expectedScratch, "nvtSingleByteScratch must not be null");
|
||||
assertEquals(1, expectedScratch.length, "nvtSingleByteScratch must have length 1");
|
||||
|
||||
fsm.setConnectionState(ConnectionState.CONNECTED_NVT);
|
||||
|
||||
AtomicBoolean dispatched = new AtomicBoolean(false);
|
||||
AtomicReference<byte[]> receivedBuffer = new AtomicReference<>();
|
||||
|
||||
NvtProcessor customNvtProcessor = new NvtProcessor(client.getScreenBuffer(), new EbcdicTranslator()) {
|
||||
@Override
|
||||
public void processNVTData(byte[] buffer, int offset, int length) {
|
||||
dispatched.set(true);
|
||||
receivedBuffer.set(buffer);
|
||||
assertEquals(0, offset);
|
||||
assertEquals(1, length);
|
||||
assertEquals((byte) 'Z', buffer[offset]);
|
||||
super.processNVTData(buffer, offset, length);
|
||||
}
|
||||
};
|
||||
|
||||
Field nvtProcField = TelnetFSM.class.getDeclaredField("nvtProcessor");
|
||||
nvtProcField.setAccessible(true);
|
||||
nvtProcField.set(fsm, customNvtProcessor);
|
||||
|
||||
// Feed single byte 'Z' into FSM
|
||||
fsm.feedByte('Z');
|
||||
|
||||
assertTrue(dispatched.get(), "NvtProcessor.processNVTData should have been called");
|
||||
assertSame(expectedScratch, receivedBuffer.get(), "Buffer passed to processNVTData must be the reusable nvtSingleByteScratch array instance");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 27.6: J3270App has Settings import and no in-line FQCN occurrences")
|
||||
public void testJ3270AppSettingsImport() throws Exception {
|
||||
File projectRoot = new File(".").getCanonicalFile();
|
||||
if (!new File(projectRoot, "j3270").exists() && projectRoot.getParentFile() != null) {
|
||||
projectRoot = projectRoot.getParentFile();
|
||||
}
|
||||
File appFile = new File(projectRoot, "j3270/src/main/java/haus/nightmare/j3270/J3270App.java");
|
||||
assertTrue(appFile.exists(), "J3270App.java must exist");
|
||||
String content = Files.readString(appFile.toPath());
|
||||
|
||||
assertTrue(content.contains("import haus.nightmare.j3270.config.Settings;"),
|
||||
"J3270App.java must import haus.nightmare.j3270.config.Settings");
|
||||
|
||||
// The only occurrence of haus.nightmare.j3270.config.Settings should be in the import
|
||||
int firstIndex = content.indexOf("haus.nightmare.j3270.config.Settings");
|
||||
assertTrue(firstIndex >= 0, "Must contain import");
|
||||
int secondIndex = content.indexOf("haus.nightmare.j3270.config.Settings", firstIndex + 1);
|
||||
assertEquals(-1, secondIndex, "There should be no inline occurrences of haus.nightmare.j3270.config.Settings in J3270App");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
import haus.nightmare.j3270.ui.FieldInspectorDialog;
|
||||
import haus.nightmare.j3270.ui.FindDialog;
|
||||
import haus.nightmare.j3270.ui.HotspotEngine;
|
||||
import haus.nightmare.j3270.ui.ScriptDialog;
|
||||
import haus.nightmare.j3270.ui.TerminalPanel;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseMotionListener;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Verification test suite for Phase 28:
|
||||
* UI Responsiveness, Dialog Lifecycle & Event Optimization
|
||||
*/
|
||||
public class Phase28UiOptimizationTest {
|
||||
|
||||
private void setField(Object obj, String fieldName, Object val) throws Exception {
|
||||
Field f = obj.getClass().getDeclaredField(fieldName);
|
||||
f.setAccessible(true);
|
||||
f.set(obj, val);
|
||||
}
|
||||
|
||||
private void writeString(ScreenBuffer sb, int row, int col, String text) {
|
||||
int cols = sb.getDisplayCols();
|
||||
int base = row * cols + col;
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
sb.getCell(base + i).ucs4 = text.charAt(i);
|
||||
}
|
||||
sb.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 28.1: T2-A Hotspot Dirty Flagging & Lazy Text Scraping eliminates redundant scans on mouseMoved")
|
||||
public void testRapidMouseMoveEventsScanOnlyOnce() throws Exception {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Put hotspots on screen: PF keys and URL
|
||||
writeString(sb, 0, 0, "APPLICATION SELECTION MENU");
|
||||
writeString(sb, 2, 0, "PF1=Help PF3=Exit PF12=Cancel");
|
||||
writeString(sb, 4, 0, "Visit http://example.com/3270 for assistance");
|
||||
sb.updateDisplaySnapshot();
|
||||
|
||||
panel.setClient(client);
|
||||
HotspotEngine engine = panel.getHotspotEngine();
|
||||
engine.setUrlEnabled(true);
|
||||
engine.setPfKeyEnabled(true);
|
||||
engine.setMenuChoiceEnabled(true);
|
||||
|
||||
assertTrue(panel.isHotspotsDirty(), "Hotspots should initially be dirty on panel setup");
|
||||
long initialScanCount = engine.getScanCount();
|
||||
|
||||
MouseMotionListener[] motionListeners = panel.getMouseMotionListeners();
|
||||
assertTrue(motionListeners.length > 0, "TerminalPanel must have at least one MouseMotionListener");
|
||||
MouseMotionListener mml = motionListeners[0];
|
||||
|
||||
// Simulate 1,000 rapid mouse move events across the terminal grid
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
int x = 20 + (i % 200);
|
||||
int y = 30 + (i % 150);
|
||||
MouseEvent me = new MouseEvent(panel, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, x, y, 0, false);
|
||||
mml.mouseMoved(me);
|
||||
}
|
||||
|
||||
// After 1,000 moves, hotspotEngine.scan() should only have executed ONCE
|
||||
assertEquals(initialScanCount + 1, engine.getScanCount(),
|
||||
"hotspotEngine.scan() should execute exactly once across 1,000 rapid mouse move events on a clean screen");
|
||||
assertFalse(panel.isHotspotsDirty(), "Hotspots must be marked clean after scanning");
|
||||
|
||||
// Verify hotspots were actually discovered
|
||||
List<HotspotEngine.Hotspot> hotspots = engine.getHotspots();
|
||||
assertFalse(hotspots.isEmpty(), "Hotspots on screen must be found");
|
||||
assertNotNull(engine.findHotspot(2, 0), "Hotspot at row 2 col 0 (PF1) must be found");
|
||||
|
||||
// Direct calls to HotspotEngine.scan(sb) without snapshot version changes must also bypass StringBuilder
|
||||
long beforeDirectCalls = engine.getScanCount();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
List<HotspotEngine.Hotspot> res = engine.scan(sb);
|
||||
assertFalse(res.isEmpty());
|
||||
}
|
||||
assertEquals(beforeDirectCalls, engine.getScanCount(),
|
||||
"Direct calls to HotspotEngine.scan(sb) without snapshot version change must return cachedHotspots");
|
||||
|
||||
// Now simulate screen update
|
||||
writeString(sb, 6, 0, "NEW UPDATE: PF7=Up PF8=Down");
|
||||
panel.onScreenUpdated();
|
||||
assertTrue(panel.isHotspotsDirty(), "onScreenUpdated() must flag hotspotsDirty = true");
|
||||
|
||||
// Simulate another burst of 1,000 rapid mouse moves
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
int x = 30 + (i % 200);
|
||||
int y = 40 + (i % 150);
|
||||
MouseEvent me = new MouseEvent(panel, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, x, y, 0, false);
|
||||
mml.mouseMoved(me);
|
||||
}
|
||||
|
||||
assertEquals(initialScanCount + 2, engine.getScanCount(),
|
||||
"After screen update, exactly one more scan should run across 1,000 mouse moves");
|
||||
assertFalse(panel.isHotspotsDirty(), "Hotspots must be marked clean again after rescan");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 28.2: T2-B Precomputed Selection Scratch Lookup matches isCellSelected exactly across all modes")
|
||||
public void testSelectionBoundsMatchingPrecomputedScratchVsLegacy() throws Exception {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
panel.setClient(client);
|
||||
|
||||
int rows = 24;
|
||||
int cols = 80;
|
||||
|
||||
// Configuration 1: No selection
|
||||
panel.clearSelection();
|
||||
panel.precomputeSelectionScratch(rows, cols);
|
||||
boolean[] scratch = panel.getSelectedCellsScratch();
|
||||
assertEquals(rows * cols, scratch.length);
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int baddr = r * cols + c;
|
||||
assertFalse(scratch[baddr], "No cell should be selected");
|
||||
assertEquals(panel.isCellSelected(r, c), scratch[baddr]);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration 2: Linear / Stream mode forward selection (spanning multiple rows)
|
||||
setField(panel, "blockSelectMode", false);
|
||||
setField(panel, "trimRectSizingHandles", false);
|
||||
setField(panel, "selectionStartRow", 2);
|
||||
setField(panel, "selectionStartCol", 15);
|
||||
setField(panel, "selectionEndRow", 5);
|
||||
setField(panel, "selectionEndCol", 42);
|
||||
|
||||
panel.precomputeSelectionScratch(rows, cols);
|
||||
scratch = panel.getSelectedCellsScratch();
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int baddr = r * cols + c;
|
||||
boolean expected = panel.isCellSelected(r, c);
|
||||
assertEquals(expected, scratch[baddr], "Mismatch in linear forward mode at row " + r + ", col " + c);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration 3: Linear / Stream mode backward selection (drag upwards)
|
||||
setField(panel, "selectionStartRow", 8);
|
||||
setField(panel, "selectionStartCol", 30);
|
||||
setField(panel, "selectionEndRow", 4);
|
||||
setField(panel, "selectionEndCol", 10);
|
||||
|
||||
panel.precomputeSelectionScratch(rows, cols);
|
||||
scratch = panel.getSelectedCellsScratch();
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int baddr = r * cols + c;
|
||||
boolean expected = panel.isCellSelected(r, c);
|
||||
assertEquals(expected, scratch[baddr], "Mismatch in linear backward mode at row " + r + ", col " + c);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration 4: Block select mode
|
||||
setField(panel, "blockSelectMode", true);
|
||||
setField(panel, "trimRectSizingHandles", false);
|
||||
setField(panel, "selectionStartRow", 3);
|
||||
setField(panel, "selectionStartCol", 20);
|
||||
setField(panel, "selectionEndRow", 7);
|
||||
setField(panel, "selectionEndCol", 60);
|
||||
|
||||
panel.precomputeSelectionScratch(rows, cols);
|
||||
scratch = panel.getSelectedCellsScratch();
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int baddr = r * cols + c;
|
||||
boolean expected = panel.isCellSelected(r, c);
|
||||
assertEquals(expected, scratch[baddr], "Mismatch in block select mode at row " + r + ", col " + c);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration 5: Trim Rect Sizing Handles mode (block geometry)
|
||||
setField(panel, "blockSelectMode", false);
|
||||
setField(panel, "trimRectSizingHandles", true);
|
||||
setField(panel, "selectionStartRow", 10);
|
||||
setField(panel, "selectionStartCol", 5);
|
||||
setField(panel, "selectionEndRow", 15);
|
||||
setField(panel, "selectionEndCol", 25);
|
||||
|
||||
panel.precomputeSelectionScratch(rows, cols);
|
||||
scratch = panel.getSelectedCellsScratch();
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int baddr = r * cols + c;
|
||||
boolean expected = panel.isCellSelected(r, c);
|
||||
assertEquals(expected, scratch[baddr], "Mismatch in trim handles mode at row " + r + ", col " + c);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration 6: Model 5 Dynamic Geometry (27 rows x 132 cols)
|
||||
rows = 27;
|
||||
cols = 132;
|
||||
client.getScreenBuffer().setDimensions(rows, cols);
|
||||
client.getScreenBuffer().updateDisplaySnapshot();
|
||||
setField(panel, "selectionStartRow", 1);
|
||||
setField(panel, "selectionStartCol", 1);
|
||||
setField(panel, "selectionEndRow", 25);
|
||||
setField(panel, "selectionEndCol", 130);
|
||||
|
||||
panel.precomputeSelectionScratch(rows, cols);
|
||||
scratch = panel.getSelectedCellsScratch();
|
||||
assertEquals(rows * cols, scratch.length);
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int baddr = r * cols + c;
|
||||
boolean expected = panel.isCellSelected(r, c);
|
||||
assertEquals(expected, scratch[baddr], "Mismatch in Model 5 27x132 at row " + r + ", col " + c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 28.3: Dialog Lifecycle & Reconnection Hardening updates cached dialogs with active client")
|
||||
public void testReconnectCycleFindDialogInteractsWithNewClient() throws Exception {
|
||||
// 1. Verify API contract and reflection for dialog lifecycle methods
|
||||
Method findSetClient = FindDialog.class.getMethod("setClient", Telnet3270Client.class);
|
||||
assertNotNull(findSetClient);
|
||||
Method findGetClient = FindDialog.class.getMethod("getClient");
|
||||
assertEquals(Telnet3270Client.class, findGetClient.getReturnType());
|
||||
|
||||
Method scriptSetClient = ScriptDialog.class.getMethod("setClient", Telnet3270Client.class);
|
||||
assertNotNull(scriptSetClient);
|
||||
Method scriptGetClient = ScriptDialog.class.getMethod("getClient");
|
||||
assertEquals(Telnet3270Client.class, scriptGetClient.getReturnType());
|
||||
|
||||
Method fieldSetClient = FieldInspectorDialog.class.getMethod("setClient", Telnet3270Client.class);
|
||||
assertNotNull(fieldSetClient);
|
||||
Method fieldGetClient = FieldInspectorDialog.class.getMethod("getClient");
|
||||
assertEquals(Telnet3270Client.class, fieldGetClient.getReturnType());
|
||||
|
||||
Method appGetFind = J3270App.class.getMethod("getFindDialog");
|
||||
assertEquals(FindDialog.class, appGetFind.getReturnType());
|
||||
Method appGetScript = J3270App.class.getMethod("getScriptDialog");
|
||||
assertEquals(ScriptDialog.class, appGetScript.getReturnType());
|
||||
Method appGetField = J3270App.class.getMethod("getFieldInspectorDialog");
|
||||
assertEquals(FieldInspectorDialog.class, appGetField.getReturnType());
|
||||
|
||||
// 2. If non-headless environment, perform end-to-end interactive GUI verification
|
||||
try {
|
||||
J3270App app = new J3270App();
|
||||
TerminalPanel panel = app.getTerminalPanel();
|
||||
|
||||
// Session 1: Client 1
|
||||
ConnectionConfig config1 = new ConnectionConfig("host1.example.com", 23);
|
||||
Telnet3270Client client1 = new Telnet3270Client(config1);
|
||||
client1.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
writeString(client1.getScreenBuffer(), 1, 0, "SYSTEM_SESSION_ALPHA_123");
|
||||
|
||||
panel.setClient(client1);
|
||||
setField(app, "client", client1);
|
||||
|
||||
// Open FindDialog on session 1
|
||||
FindDialog findDialog = new FindDialog(app, client1, panel);
|
||||
assertSame(client1, findDialog.getClient());
|
||||
|
||||
// Search for string on Client 1
|
||||
findDialog.setSearchText("SYSTEM_SESSION_ALPHA_123");
|
||||
findDialog.findNext();
|
||||
int expectedPos1 = 1 * client1.getScreenBuffer().getDisplayCols();
|
||||
assertEquals(expectedPos1, client1.getScreenBuffer().getCursorAddress(),
|
||||
"FindDialog must move cursor to match on client1");
|
||||
|
||||
// Simulate Session Disconnect
|
||||
setField(app, "findDialog", findDialog);
|
||||
app.disconnect(false);
|
||||
assertNull(findDialog.getClient(), "Disconnect must nullify client in cached dialog");
|
||||
|
||||
// Subsequent find attempt while disconnected should report disconnected without errors
|
||||
findDialog.findNext();
|
||||
assertEquals(expectedPos1, client1.getScreenBuffer().getCursorAddress());
|
||||
|
||||
// Session 2: Reconnection to Client 2
|
||||
ConnectionConfig config2 = new ConnectionConfig("host2.example.com", 23);
|
||||
Telnet3270Client client2 = new Telnet3270Client(config2);
|
||||
client2.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
writeString(client2.getScreenBuffer(), 3, 10, "MAINFRAME_SESSION_BETA_999");
|
||||
|
||||
// Simulate reconnect hook in J3270App
|
||||
findDialog.setClient(client2);
|
||||
assertSame(client2, findDialog.getClient(), "FindDialog must now hold client2");
|
||||
|
||||
// Search for string on Client 2
|
||||
findDialog.setSearchText("MAINFRAME_SESSION_BETA_999");
|
||||
findDialog.findNext();
|
||||
int expectedPos2 = 3 * client2.getScreenBuffer().getDisplayCols() + 10;
|
||||
assertEquals(expectedPos2, client2.getScreenBuffer().getCursorAddress(),
|
||||
"FindDialog must move cursor to match on active client2 after reconnect");
|
||||
|
||||
// Verify search for old client1 string fails on client2
|
||||
int oldCursorPos = client2.getScreenBuffer().getCursorAddress();
|
||||
findDialog.setSearchText("SYSTEM_SESSION_ALPHA_123");
|
||||
findDialog.findNext();
|
||||
assertEquals(oldCursorPos, client2.getScreenBuffer().getCursorAddress(),
|
||||
"Search for stale text should not match on client2");
|
||||
|
||||
// Verify ScriptDialog and FieldInspectorDialog client lifecycle
|
||||
ScriptDialog scriptDialog = new ScriptDialog(app, client1, panel);
|
||||
assertSame(client1, scriptDialog.getClient());
|
||||
scriptDialog.setClient(client2);
|
||||
assertSame(client2, scriptDialog.getClient());
|
||||
|
||||
FieldInspectorDialog fieldInspector = new FieldInspectorDialog(app, client1, panel);
|
||||
assertSame(client1, fieldInspector.getClient());
|
||||
fieldInspector.setClient(client2);
|
||||
assertSame(client2, fieldInspector.getClient());
|
||||
|
||||
// Cleanup
|
||||
fieldInspector.dispose();
|
||||
findDialog.dispose();
|
||||
scriptDialog.dispose();
|
||||
app.closeWindow();
|
||||
} catch (java.awt.HeadlessException ignored) {
|
||||
// Running in headless mode: GUI instantiation safely bypassed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Verification test suite for Phase 30:
|
||||
* Library Distribution, Maven Artifacts & Release Tagging (v1.2.4)
|
||||
*/
|
||||
public class Phase30LibraryDistributionTest {
|
||||
|
||||
private File locateProjectRoot() {
|
||||
File current = new File(".").getAbsoluteFile();
|
||||
while (current != null) {
|
||||
if (new File(current, "settings.gradle").exists() || new File(current, "build.gradle").exists()) {
|
||||
if (new File(current, "lib3270j").exists() && new File(current, "j3270").exists()) {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
current = current.getParentFile();
|
||||
}
|
||||
return new File(".").getAbsoluteFile();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 30.1: lib3270j/build.gradle configures maven-publish, POM metadata, and artifacts")
|
||||
public void testMavenPublishConfigurationInBuildGradle() throws Exception {
|
||||
File projectRoot = locateProjectRoot();
|
||||
File libGradleFile = new File(projectRoot, "lib3270j/build.gradle");
|
||||
assertTrue(libGradleFile.exists(), "lib3270j/build.gradle must exist");
|
||||
|
||||
String content = Files.readString(libGradleFile.toPath());
|
||||
|
||||
// Plugin validation
|
||||
assertTrue(content.contains("id 'maven-publish'"), "maven-publish plugin must be applied");
|
||||
assertTrue(content.contains("withSourcesJar()"), "withSourcesJar() must be enabled");
|
||||
assertTrue(content.contains("withJavadocJar()"), "withJavadocJar() must be enabled");
|
||||
|
||||
// Publication coordinates
|
||||
assertTrue(content.contains("groupId = 'haus.nightmare.j3270'"), "groupId must be haus.nightmare.j3270");
|
||||
assertTrue(content.contains("artifactId = 'lib3270j'"), "artifactId must be lib3270j");
|
||||
|
||||
// POM metadata
|
||||
assertTrue(content.contains("name = 'lib3270j'"), "POM name must be lib3270j");
|
||||
assertTrue(content.contains("description = 'Pure Java IBM 3270 terminal emulation and protocol library with Host On-Demand (HOD) parity'"),
|
||||
"POM description must describe HOD parity");
|
||||
assertTrue(content.contains("The Unlicense"), "POM must declare The Unlicense");
|
||||
assertFalse(content.contains("Apache License"), "POM must not declare Apache License");
|
||||
assertTrue(content.contains("scm:git:git://github.com/rudi/j3270.git"), "POM must declare SCM connection");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 30.2: Standalone build scripts package lib3270j.jar with correct manifest metadata")
|
||||
public void testBuildScriptsStandaloneLibraryPackaging() throws Exception {
|
||||
File projectRoot = locateProjectRoot();
|
||||
|
||||
// 1. build_all.sh
|
||||
File buildAll = new File(projectRoot, "build_all.sh");
|
||||
assertTrue(buildAll.exists(), "build_all.sh must exist");
|
||||
String buildAllContent = Files.readString(buildAll.toPath());
|
||||
assertTrue(buildAllContent.contains("lib3270j.jar"), "build_all.sh must package lib3270j.jar");
|
||||
assertTrue(buildAllContent.contains("Implementation-Title: lib3270j"), "build_all.sh must set Implementation-Title: lib3270j");
|
||||
assertTrue(buildAllContent.contains("Implementation-Version: 1.2.4"), "build_all.sh must set Implementation-Version: 1.2.4");
|
||||
|
||||
// 2. setup.sh
|
||||
File setupSh = new File(projectRoot, "setup.sh");
|
||||
assertTrue(setupSh.exists(), "setup.sh must exist");
|
||||
String setupShContent = Files.readString(setupSh.toPath());
|
||||
assertTrue(setupShContent.contains("lib3270j.jar"), "setup.sh must package lib3270j.jar");
|
||||
assertTrue(setupShContent.contains("Implementation-Title: lib3270j"), "setup.sh must set Implementation-Title: lib3270j");
|
||||
assertTrue(setupShContent.contains("Implementation-Version: 1.2.4"), "setup.sh must set Implementation-Version: 1.2.4");
|
||||
|
||||
// 3. setup.ps1
|
||||
File setupPs1 = new File(projectRoot, "setup.ps1");
|
||||
assertTrue(setupPs1.exists(), "setup.ps1 must exist");
|
||||
String setupPs1Content = Files.readString(setupPs1.toPath());
|
||||
assertTrue(setupPs1Content.contains("lib3270j.jar"), "setup.ps1 must package lib3270j.jar");
|
||||
assertTrue(setupPs1Content.contains("Implementation-Title: lib3270j"), "setup.ps1 must set Implementation-Title: lib3270j");
|
||||
assertTrue(setupPs1Content.contains("Implementation-Version: 1.2.4"), "setup.ps1 must set Implementation-Version: 1.2.4");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 30.2: Core subsystem packages and classes are present and loadable")
|
||||
public void testCoreSubsystemsPresent() {
|
||||
// Telnet & Protocol
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.telnet.TelnetFSM"));
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.Telnet3270Client"));
|
||||
|
||||
// Screen Buffer & Concurrency
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.screen.ScreenBuffer"));
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.screen.ScreenBuffer$DisplaySnapshot"));
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.screen.ExtendedAttribute"));
|
||||
|
||||
// Charset & EBCDIC
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.charset.EbcdicTranslator"));
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.charset.CodePage"));
|
||||
|
||||
// GOCA & Graphics
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.graphics.GocaDecoder"));
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.graphics.GraphicsMode"));
|
||||
|
||||
// ECL (standard and HOD compatibility namespaces)
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.ecl.ECLSession"));
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.eNetwork.ECL.ECLSession"));
|
||||
assertDoesNotThrow(() -> Class.forName("com.ibm.eNetwork.ECL.ECLSession"));
|
||||
assertDoesNotThrow(() -> Class.forName("com.ibm.eNetwork.ECL.ECLPS"));
|
||||
assertDoesNotThrow(() -> Class.forName("com.ibm.eNetwork.beans.HOD.Macro"));
|
||||
|
||||
// FastByteBuffer de-synchronized accumulation
|
||||
assertDoesNotThrow(() -> Class.forName("haus.nightmare.lib3270j.datastream.FastByteBuffer"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 30.2: Maven local published artifacts exist and validate if maven-publish was executed")
|
||||
public void testMavenLocalPublishedArtifacts() throws Exception {
|
||||
String userHome = System.getProperty("user.home");
|
||||
File m2ArtifactDir = new File(userHome, ".m2/repository/haus/nightmare/j3270/lib3270j/1.2.4");
|
||||
if (m2ArtifactDir.exists()) {
|
||||
File pomFile = new File(m2ArtifactDir, "lib3270j-1.2.4.pom");
|
||||
File jarFile = new File(m2ArtifactDir, "lib3270j-1.2.4.jar");
|
||||
File sourcesFile = new File(m2ArtifactDir, "lib3270j-1.2.4-sources.jar");
|
||||
File javadocFile = new File(m2ArtifactDir, "lib3270j-1.2.4-javadoc.jar");
|
||||
|
||||
assertTrue(pomFile.exists() && pomFile.length() > 0, "POM file must exist and be non-empty");
|
||||
assertTrue(jarFile.exists() && jarFile.length() > 0, "JAR file must exist and be non-empty");
|
||||
assertTrue(sourcesFile.exists() && sourcesFile.length() > 0, "Sources JAR must exist and be non-empty");
|
||||
assertTrue(javadocFile.exists() && javadocFile.length() > 0, "Javadoc JAR must exist and be non-empty");
|
||||
|
||||
String pomXml = Files.readString(pomFile.toPath());
|
||||
assertTrue(pomXml.contains("<groupId>haus.nightmare.j3270</groupId>"), "POM must contain groupId");
|
||||
assertTrue(pomXml.contains("<artifactId>lib3270j</artifactId>"), "POM must contain artifactId");
|
||||
assertTrue(pomXml.contains("<version>1.2.4</version>"), "POM must contain version 1.2.4");
|
||||
assertTrue(pomXml.contains("The Unlicense"), "POM must contain Unlicense");
|
||||
assertFalse(pomXml.contains("Apache"), "lib3270j POM must not contain Apache license");
|
||||
assertFalse(pomXml.contains("<dependencies>"), "lib3270j POM must have zero external dependencies");
|
||||
}
|
||||
}
|
||||
}
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
package haus.nightmare.lib3270j.screen;
|
||||
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.datastream.FastByteBuffer;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Verification test suite for Phase 29:
|
||||
* Lock-Free Concurrency Architecture & Buffer De-Synchronization
|
||||
*/
|
||||
public class Phase29ConcurrencyStressTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 29.1: FastByteBuffer methods have synchronized modifier removed for thread-confined stream accumulation")
|
||||
public void testFastByteBufferMethodsAreUnsynchronized() throws Exception {
|
||||
Class<?> clazz = FastByteBuffer.class;
|
||||
String[] methodNames = {
|
||||
"write", "buffer", "size", "reset", "asByteBuffer", "slice", "toByteArray"
|
||||
};
|
||||
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
for (String name : methodNames) {
|
||||
if (method.getName().equals(name)) {
|
||||
int mod = method.getModifiers();
|
||||
assertFalse(Modifier.isSynchronized(mod),
|
||||
"FastByteBuffer method '" + method.getName() + "' must NOT have synchronized modifier");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Functional stream test
|
||||
FastByteBuffer fbb = new FastByteBuffer(128);
|
||||
for (int i = 0; i < 5000; i++) {
|
||||
fbb.write(i & 0xFF);
|
||||
}
|
||||
assertEquals(5000, fbb.size());
|
||||
assertEquals((byte) 0, fbb.buffer()[0]);
|
||||
assertEquals((byte) (4999 & 0xFF), fbb.buffer()[4999]);
|
||||
|
||||
ByteBuffer bb = fbb.asByteBuffer();
|
||||
assertEquals(5000, bb.remaining());
|
||||
ByteBuffer sl = fbb.slice(10, 20);
|
||||
assertEquals(20, sl.remaining());
|
||||
|
||||
fbb.reset();
|
||||
assertEquals(0, fbb.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 29.2: 10 worker threads reading getDisplayCell continuously while network thread writes 5,000 updates")
|
||||
public void testConcurrentDisplayCellReadsDuringHighFrequencyUpdates() throws Exception {
|
||||
ScreenBuffer sb = new ScreenBuffer(TerminalModel.IBM_3279_2, new EbcdicTranslator());
|
||||
int size = sb.getRows() * sb.getCols();
|
||||
|
||||
int workerThreadCount = 10;
|
||||
int totalUpdates = 5000;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(workerThreadCount + 1);
|
||||
AtomicBoolean running = new AtomicBoolean(true);
|
||||
AtomicInteger tornCellCount = new AtomicInteger(0);
|
||||
AtomicInteger exceptionCount = new AtomicInteger(0);
|
||||
AtomicLong totalReads = new AtomicLong(0);
|
||||
|
||||
CountDownLatch startLatch = new CountDownLatch(1);
|
||||
CountDownLatch writersDoneLatch = new CountDownLatch(1);
|
||||
|
||||
// Spin 10 worker threads reading getDisplayCell() continuously
|
||||
for (int w = 0; w < workerThreadCount; w++) {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
startLatch.await();
|
||||
while (running.get()) {
|
||||
int rows = sb.getDisplayRows();
|
||||
int cols = sb.getDisplayCols();
|
||||
int curAddr = sb.getDisplayCursorAddress();
|
||||
long version = sb.getSnapshotVersion();
|
||||
|
||||
assertTrue(rows > 0, "Display rows must be positive");
|
||||
assertTrue(cols > 0, "Display cols must be positive");
|
||||
assertTrue(curAddr >= 0, "Display cursor must be non-negative");
|
||||
|
||||
int cellCount = rows * cols;
|
||||
for (int addr = 0; addr < cellCount; addr++) {
|
||||
ExtendedAttribute cell = sb.getDisplayCell(addr);
|
||||
if (cell == null) {
|
||||
exceptionCount.incrementAndGet();
|
||||
continue;
|
||||
}
|
||||
totalReads.incrementAndGet();
|
||||
|
||||
// Validate torn cell state:
|
||||
// In our patterned writer, every cell written in iteration N has:
|
||||
// ucs4 = (char) ('A' + (N % 26))
|
||||
// ec = (byte) (0x10 + (N % 26))
|
||||
// fg = (byte) (0x30 + (N % 26))
|
||||
// bg = (byte) (0x50 + (N % 26))
|
||||
// A torn read occurs if ucs4, ec, fg, or bg don't match the same iteration!
|
||||
if (cell.ucs4 >= 'A' && cell.ucs4 <= 'Z') {
|
||||
int ucsOffset = cell.ucs4 - 'A';
|
||||
int ecOffset = (cell.ec & 0xFF) - 0x10;
|
||||
int fgOffset = (cell.fg & 0xFF) - 0x30;
|
||||
int bgOffset = (cell.bg & 0xFF) - 0x50;
|
||||
|
||||
if (ucsOffset != ecOffset || ucsOffset != fgOffset || ucsOffset != bgOffset) {
|
||||
tornCellCount.incrementAndGet();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
exceptionCount.incrementAndGet();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Writer task simulating network thread writing 5,000 screen updates
|
||||
Future<?> writerFuture = executor.submit(() -> {
|
||||
try {
|
||||
startLatch.await();
|
||||
for (int iter = 0; iter < totalUpdates; iter++) {
|
||||
int mod = iter % 26;
|
||||
char ch = (char) ('A' + mod);
|
||||
byte ec = (byte) (0x10 + mod);
|
||||
byte fg = (byte) (0x30 + mod);
|
||||
byte bg = (byte) (0x50 + mod);
|
||||
|
||||
// Mutate screen buffer
|
||||
ExtendedAttribute[] rawBuf = sb.getBuffer();
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute cell = rawBuf[i];
|
||||
cell.ucs4 = ch;
|
||||
cell.ec = ec;
|
||||
cell.fg = fg;
|
||||
cell.bg = bg;
|
||||
}
|
||||
sb.setCursorAddress(iter % size);
|
||||
sb.updateDisplaySnapshot();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
exceptionCount.incrementAndGet();
|
||||
} finally {
|
||||
writersDoneLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger start
|
||||
startLatch.countDown();
|
||||
|
||||
// Await completion of 5,000 updates with timeout to verify 0 deadlocks
|
||||
boolean writerFinished = writersDoneLatch.await(15, TimeUnit.SECONDS);
|
||||
assertTrue(writerFinished, "Writer must complete 5,000 updates without deadlock");
|
||||
|
||||
// Stop workers
|
||||
running.set(false);
|
||||
executor.shutdown();
|
||||
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS), "Worker threads must terminate cleanly");
|
||||
|
||||
// Assert zero errors, zero torn reads, and high-volume lock-free reads verified
|
||||
assertEquals(0, exceptionCount.get(), "Zero exceptions (NPE/AIOOBE) must occur during concurrent execution");
|
||||
assertEquals(0, tornCellCount.get(), "Zero torn cell states must occur during atomic DisplaySnapshot swapping");
|
||||
assertTrue(totalReads.get() > 100_000, "Worker threads must execute high-throughput lock-free reads (actual: " + totalReads.get() + ")");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 29.2: Screen dimension resizing under concurrent reader load produces 0 exceptions")
|
||||
public void testConcurrentReadsDuringScreenResizing() throws Exception {
|
||||
ScreenBuffer sb = new ScreenBuffer(24, 80, 27, 132, new EbcdicTranslator());
|
||||
int workerCount = 8;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(workerCount + 1);
|
||||
AtomicBoolean running = new AtomicBoolean(true);
|
||||
AtomicInteger exceptionCount = new AtomicInteger(0);
|
||||
AtomicLong successfulReads = new AtomicLong(0);
|
||||
|
||||
CountDownLatch startLatch = new CountDownLatch(1);
|
||||
CountDownLatch writerLatch = new CountDownLatch(1);
|
||||
|
||||
for (int w = 0; w < workerCount; w++) {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
startLatch.await();
|
||||
while (running.get()) {
|
||||
ScreenBuffer.DisplaySnapshot snap = sb.getDisplaySnapshot();
|
||||
if (snap == null) {
|
||||
exceptionCount.incrementAndGet();
|
||||
continue;
|
||||
}
|
||||
|
||||
int rows = sb.getDisplayRows();
|
||||
int cols = sb.getDisplayCols();
|
||||
int total = rows * cols;
|
||||
|
||||
for (int i = 0; i < total; i++) {
|
||||
ExtendedAttribute cell = sb.getDisplayCell(i);
|
||||
if (cell != null) {
|
||||
successfulReads.incrementAndGet();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
exceptionCount.incrementAndGet();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Writer alternates dimensions between 24x80 (1920) and 27x132 (3564)
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
startLatch.await();
|
||||
for (int iter = 0; iter < 1000; iter++) {
|
||||
sb.erase((iter % 2) != 0);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
exceptionCount.incrementAndGet();
|
||||
} finally {
|
||||
writerLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
startLatch.countDown();
|
||||
assertTrue(writerLatch.await(10, TimeUnit.SECONDS), "Dimension resizing writer must complete without deadlock");
|
||||
|
||||
running.set(false);
|
||||
executor.shutdown();
|
||||
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
|
||||
|
||||
assertEquals(0, exceptionCount.get(), "Screen resizing under reader load must not throw ArrayIndexOutOfBoundsException or NPE");
|
||||
assertTrue(successfulReads.get() > 50_000, "Concurrent readers must successfully read cells across dimension changes");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 29.2: setCursorAddress propagates cursor changes atomically to DisplaySnapshot")
|
||||
public void testCursorUpdatePropagatesToDisplaySnapshot() {
|
||||
ScreenBuffer sb = new ScreenBuffer(TerminalModel.IBM_3279_2, new EbcdicTranslator());
|
||||
sb.erase(false);
|
||||
|
||||
assertEquals(0, sb.getDisplayCursorAddress());
|
||||
|
||||
sb.setCursorAddress(125);
|
||||
assertEquals(125, sb.getDisplayCursorAddress());
|
||||
assertNotNull(sb.getDisplaySnapshot());
|
||||
assertEquals(125, sb.getDisplaySnapshot().cursorAddress);
|
||||
|
||||
sb.setCursorPosition(5, 10);
|
||||
int expected = 5 * sb.getCols() + 10;
|
||||
assertEquals(expected, sb.getDisplayCursorAddress());
|
||||
assertEquals(expected, sb.getDisplaySnapshot().cursorAddress);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user