Hotspot detection and screen buffer
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 1m20s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m28s
Release j3270 / Build & Publish Release (push) Successful in 1m39s
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 1m20s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m28s
Release j3270 / Build & Publish Release (push) Successful in 1m39s
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
package haus.nightmare.lib3270j.input;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class HodKeyMapTest {
|
||||
|
||||
@Test
|
||||
public void testModifierPrefixes() {
|
||||
assertEquals('B', HodKeyMap.getModifierPrefix(0));
|
||||
assertEquals('S', HodKeyMap.getModifierPrefix(HodKeyMap.MOD_SHIFT));
|
||||
assertEquals('C', HodKeyMap.getModifierPrefix(HodKeyMap.MOD_CTRL));
|
||||
assertEquals('D', HodKeyMap.getModifierPrefix(HodKeyMap.MOD_SHIFT | HodKeyMap.MOD_CTRL));
|
||||
assertEquals('A', HodKeyMap.getModifierPrefix(HodKeyMap.MOD_ALT));
|
||||
assertEquals('H', HodKeyMap.getModifierPrefix(HodKeyMap.MOD_SHIFT | HodKeyMap.MOD_ALT));
|
||||
assertEquals('I', HodKeyMap.getModifierPrefix(HodKeyMap.MOD_CTRL | HodKeyMap.MOD_ALT));
|
||||
assertEquals('J', HodKeyMap.getModifierPrefix(HodKeyMap.MOD_SHIFT | HodKeyMap.MOD_CTRL | HodKeyMap.MOD_ALT));
|
||||
|
||||
assertEquals(0, HodKeyMap.getModifiersFromPrefix('B'));
|
||||
assertEquals(HodKeyMap.MOD_SHIFT, HodKeyMap.getModifiersFromPrefix('S'));
|
||||
assertEquals(HodKeyMap.MOD_CTRL, HodKeyMap.getModifiersFromPrefix('C'));
|
||||
assertEquals(HodKeyMap.MOD_SHIFT | HodKeyMap.MOD_CTRL, HodKeyMap.getModifiersFromPrefix('D'));
|
||||
assertEquals(HodKeyMap.MOD_ALT, HodKeyMap.getModifiersFromPrefix('A'));
|
||||
assertEquals(HodKeyMap.MOD_SHIFT | HodKeyMap.MOD_ALT, HodKeyMap.getModifiersFromPrefix('H'));
|
||||
assertEquals(HodKeyMap.MOD_CTRL | HodKeyMap.MOD_ALT, HodKeyMap.getModifiersFromPrefix('I'));
|
||||
assertEquals(HodKeyMap.MOD_SHIFT | HodKeyMap.MOD_CTRL | HodKeyMap.MOD_ALT, HodKeyMap.getModifiersFromPrefix('J'));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHashKeyParsingStandardKeys() {
|
||||
// A67 = Alt + C
|
||||
HodKeyMap.KeyEntry ke1 = HodKeyMap.parseHashKey("A67", "[clear]");
|
||||
assertNotNull(ke1);
|
||||
assertEquals(67, ke1.getKeyCode());
|
||||
assertEquals(HodKeyMap.MOD_ALT, ke1.getModifiers());
|
||||
assertEquals(HodKeyMap.LOCATION_STANDARD, ke1.getKeyLocation());
|
||||
assertEquals("alt C", ke1.toKeyStrokeString());
|
||||
assertEquals("[clear]", ke1.getValue());
|
||||
|
||||
// B35 = End
|
||||
HodKeyMap.KeyEntry ke2 = HodKeyMap.parseHashKey("B35", "[eraseeof]");
|
||||
assertNotNull(ke2);
|
||||
assertEquals(35, ke2.getKeyCode());
|
||||
assertEquals(0, ke2.getModifiers());
|
||||
assertEquals("END", ke2.toKeyStrokeString());
|
||||
|
||||
// A82 = Alt + R
|
||||
HodKeyMap.KeyEntry ke3 = HodKeyMap.parseHashKey("A82", "[reset]");
|
||||
assertNotNull(ke3);
|
||||
assertEquals(82, ke3.getKeyCode());
|
||||
assertEquals(HodKeyMap.MOD_ALT, ke3.getModifiers());
|
||||
assertEquals("alt R", ke3.toKeyStrokeString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDuplicateAndNumpadKeys() {
|
||||
// Numpad Down: 225
|
||||
HodKeyMap.KeyEntry keDown = HodKeyMap.parseHashKey("B225", "[down]");
|
||||
assertNotNull(keDown);
|
||||
assertEquals(HodKeyMap.VK_DOWN, keDown.getKeyCode());
|
||||
assertEquals(HodKeyMap.LOCATION_NUMPAD, keDown.getKeyLocation());
|
||||
assertEquals("DOWN", keDown.toKeyStrokeString());
|
||||
|
||||
// Numpad Enter: 0x2000000 + 10 = 33554442
|
||||
HodKeyMap.KeyEntry keEnter = HodKeyMap.parseHashKey("B33554442", "[enter]");
|
||||
assertNotNull(keEnter);
|
||||
assertEquals(HodKeyMap.VK_ENTER, keEnter.getKeyCode());
|
||||
assertEquals(HodKeyMap.LOCATION_NUMPAD, keEnter.getKeyLocation());
|
||||
assertEquals("ENTER", keEnter.toKeyStrokeString());
|
||||
|
||||
// Generation of hash keys
|
||||
assertEquals("B224", HodKeyMap.hashKey(HodKeyMap.VK_UP, 0, HodKeyMap.LOCATION_NUMPAD));
|
||||
assertEquals("B225", HodKeyMap.hashKey(HodKeyMap.VK_DOWN, 0, HodKeyMap.LOCATION_NUMPAD));
|
||||
assertEquals("B226", HodKeyMap.hashKey(HodKeyMap.VK_LEFT, 0, HodKeyMap.LOCATION_NUMPAD));
|
||||
assertEquals("B227", HodKeyMap.hashKey(HodKeyMap.VK_RIGHT, 0, HodKeyMap.LOCATION_NUMPAD));
|
||||
assertEquals("B33554442", HodKeyMap.hashKey(HodKeyMap.VK_ENTER, 0, HodKeyMap.LOCATION_NUMPAD));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseHodKmpString() throws Exception {
|
||||
String kmpContent =
|
||||
"[KeyRemap]\n" +
|
||||
"XF:[textlogicaldisp]=[textlogicaldisp]\n" +
|
||||
"A67=[clear]\n" +
|
||||
"autoApply=false\n" +
|
||||
"keyVersion=2\n" +
|
||||
"B35=[eraseeof]\n" +
|
||||
"sessionType=1\n" +
|
||||
"disableKeyBuffer=false\n" +
|
||||
"A82=[reset]\n" +
|
||||
"codePage=037\n";
|
||||
|
||||
Map<String, String> map = HodKeyMap.parseHodKmp(new StringReader(kmpContent));
|
||||
assertEquals("alt C", map.get("CLEAR"));
|
||||
assertEquals("END", map.get("END"));
|
||||
assertEquals("alt R", map.get("ESCAPE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseStandardKmpFileIfPresent() throws Exception {
|
||||
File f = new File("/home/rudi/Documents/IBM HoD/Standard.kmp");
|
||||
if (f.exists()) {
|
||||
try (FileReader fr = new FileReader(f)) {
|
||||
Map<String, String> map = HodKeyMap.parseHodKmp(fr);
|
||||
assertEquals("alt C", map.get("CLEAR"));
|
||||
assertEquals("END", map.get("END"));
|
||||
assertEquals("alt R", map.get("ESCAPE"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExportHodKmp() throws Exception {
|
||||
Map<String, String> bindings = new LinkedHashMap<>();
|
||||
bindings.put("CLEAR", "alt C");
|
||||
bindings.put("END", "END");
|
||||
bindings.put("ESCAPE", "alt R");
|
||||
|
||||
StringWriter sw = new StringWriter();
|
||||
HodKeyMap.exportHodKmp(sw, bindings);
|
||||
String out = sw.toString();
|
||||
|
||||
assertTrue(out.contains("[KeyRemap]"));
|
||||
assertTrue(out.contains("A67=[clear]"));
|
||||
assertTrue(out.contains("B35=[eraseeof]"));
|
||||
assertTrue(out.contains("A82=[reset]"));
|
||||
|
||||
// Verify roundtrip
|
||||
Map<String, String> parsed = HodKeyMap.parseHodKmp(new StringReader(out));
|
||||
assertEquals("alt C", parsed.get("CLEAR"));
|
||||
assertEquals("END", parsed.get("END"));
|
||||
assertEquals("alt R", parsed.get("ESCAPE"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package haus.nightmare.lib3270j.input;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class PasteWordBreakTest {
|
||||
|
||||
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);
|
||||
|
||||
screen.erase(false);
|
||||
// Field 1: pos 0 (FA), pos 1..5 unprotected (length 5)
|
||||
// pos 6: FA (unprotected), pos 7..16 unprotected (length 10)
|
||||
// pos 17: FA (protected)
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(6, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(17, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasteWordBreakPreventsSplittingAcrossFields() {
|
||||
// Cursor at pos 1 (start of Field 1)
|
||||
screen.setCursorAddress(1);
|
||||
input.typeCharacter('A');
|
||||
input.typeCharacter('B');
|
||||
input.typeCharacter('C');
|
||||
// Now cursor is at pos 4, remaining space in Field 1 is 2 (pos 4 and 5)
|
||||
|
||||
// Paste "WORLD": length 5, does not fit in 2 positions
|
||||
// With pasteWordBreak=true, it should skip to Field 2 (pos 7) and paste WORLD
|
||||
input.setPasteWordBreak(true);
|
||||
input.pasteText("WORLD", false, false, false, true);
|
||||
|
||||
// Field 1: pos 1='A', 2='B', 3='C' with trailing blanks untouched
|
||||
assertEquals("ABC ", ps.getString(1, 5));
|
||||
|
||||
// Field 2: pos 7..11 should have "WORLD"
|
||||
assertEquals("WORLD", ps.getString(7, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasteWithoutWordBreakSplitsWord() {
|
||||
// Cursor at pos 1 (start of Field 1)
|
||||
screen.setCursorAddress(1);
|
||||
input.typeCharacter('A');
|
||||
input.typeCharacter('B');
|
||||
input.typeCharacter('C');
|
||||
// Now cursor is at pos 4, remaining space in Field 1 is 2 (pos 4 and 5)
|
||||
|
||||
// With pasteWordBreak=false, "WO" goes to pos 4..5, and "RLD" spills into Field 2
|
||||
input.setPasteWordBreak(false);
|
||||
input.pasteText("WORLD", false, false, false, false);
|
||||
|
||||
assertEquals("ABCWO", ps.getString(1, 5));
|
||||
|
||||
// Field 2 receives "RLD"
|
||||
assertEquals("RLD", ps.getString(7, 3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasteWordBreakHandlesSpaceDelimiterWithoutLeadingSpaceInNextField() {
|
||||
screen.setCursorAddress(1);
|
||||
// Field 1 has length 5. We paste "HELLO WORLD".
|
||||
// "HELLO" takes pos 1..5. The space is a delimiter.
|
||||
// "WORLD" should start at pos 7 with no leading space!
|
||||
input.setPasteWordBreak(true);
|
||||
input.pasteText("HELLO WORLD", false, false, false, true);
|
||||
|
||||
assertEquals("HELLO", ps.getString(1, 5));
|
||||
assertEquals("WORLD", ps.getString(7, 5));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package haus.nightmare.lib3270j.screen;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for ScreenHistoryBuffer (Phase 25.1).
|
||||
* Tests rolling 64-frame ring buffer, deduplication, chronological ordering,
|
||||
* text/table extraction, and listener callbacks.
|
||||
*/
|
||||
public class ScreenHistoryBufferTest {
|
||||
|
||||
private ScreenBuffer screenBuffer;
|
||||
private ScreenHistoryBuffer historyBuffer;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
screenBuffer = new ScreenBuffer();
|
||||
historyBuffer = new ScreenHistoryBuffer(64);
|
||||
}
|
||||
|
||||
private void writeString(int row, int col, String text) {
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
screenBuffer.setChar(row, col + i, text.charAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Initial buffer state is empty")
|
||||
public void testInitialState() {
|
||||
assertEquals(0, historyBuffer.size());
|
||||
assertTrue(historyBuffer.isEmpty());
|
||||
assertEquals(64, historyBuffer.getCapacity());
|
||||
assertNull(historyBuffer.getLatest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Captures snapshot with correct dimensions, text, and cursor")
|
||||
public void testSnapshotCapture() {
|
||||
writeString(1, 0, "HELLO");
|
||||
screenBuffer.setCursorAddress(85);
|
||||
|
||||
ScreenHistoryBuffer.ScreenSnapshot snap = historyBuffer.snapshot(screenBuffer);
|
||||
assertNotNull(snap);
|
||||
assertEquals(1, historyBuffer.size());
|
||||
assertFalse(historyBuffer.isEmpty());
|
||||
assertEquals(24, snap.getRows());
|
||||
assertEquals(80, snap.getCols());
|
||||
assertEquals(85, snap.getCursorAddress());
|
||||
assertEquals(1, snap.getCursorRow());
|
||||
assertEquals(5, snap.getCursorCol());
|
||||
|
||||
assertEquals('H', snap.getChar(1, 0));
|
||||
assertEquals('E', snap.getChar(1, 1));
|
||||
assertEquals('L', snap.getChar(1, 2));
|
||||
assertEquals('L', snap.getChar(1, 3));
|
||||
assertEquals('O', snap.getChar(1, 4));
|
||||
assertEquals("HELLO", snap.getLine(1).trim());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deduplicates identical consecutive snapshots")
|
||||
public void testDeduplication() {
|
||||
screenBuffer.setChar(0, 0, 'A');
|
||||
ScreenHistoryBuffer.ScreenSnapshot s1 = historyBuffer.snapshot(screenBuffer);
|
||||
assertNotNull(s1);
|
||||
assertEquals(1, historyBuffer.size());
|
||||
|
||||
// Snapshot identical buffer state without changes
|
||||
ScreenHistoryBuffer.ScreenSnapshot s2 = historyBuffer.snapshot(screenBuffer);
|
||||
assertNull(s2, "Consecutive identical snapshot should be suppressed");
|
||||
assertEquals(1, historyBuffer.size());
|
||||
|
||||
// Now modify screen buffer
|
||||
screenBuffer.setChar(0, 1, 'B');
|
||||
ScreenHistoryBuffer.ScreenSnapshot s3 = historyBuffer.snapshot(screenBuffer);
|
||||
assertNotNull(s3, "Snapshot should be captured after buffer modification");
|
||||
assertEquals(2, historyBuffer.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Rolls over correctly when capacity of 64 frames is exceeded")
|
||||
public void testRollingRingBufferOverflow() {
|
||||
// Capture 70 distinct screens
|
||||
for (int i = 0; i < 70; i++) {
|
||||
screenBuffer.clear();
|
||||
screenBuffer.setCursorAddress(0);
|
||||
String str = "SCREEN_" + String.format("%03d", i);
|
||||
writeString(0, 0, str);
|
||||
ScreenHistoryBuffer.ScreenSnapshot snap = historyBuffer.snapshot(screenBuffer);
|
||||
assertNotNull(snap);
|
||||
}
|
||||
|
||||
// Buffer size must be capped at 64
|
||||
assertEquals(64, historyBuffer.size());
|
||||
|
||||
// The oldest snapshot (index 0) should be screen 6 (70 - 64 = 6)
|
||||
ScreenHistoryBuffer.ScreenSnapshot oldest = historyBuffer.get(0);
|
||||
assertTrue(oldest.getText().startsWith("SCREEN_006"),
|
||||
"Oldest frame should be SCREEN_006, was: " + oldest.getText().substring(0, 10));
|
||||
|
||||
// The newest snapshot (index 63) should be screen 69
|
||||
ScreenHistoryBuffer.ScreenSnapshot newest = historyBuffer.get(63);
|
||||
assertTrue(newest.getText().startsWith("SCREEN_069"),
|
||||
"Newest frame should be SCREEN_069, was: " + newest.getText().substring(0, 10));
|
||||
|
||||
// getLatest() must match newest
|
||||
assertEquals(newest, historyBuffer.getLatest());
|
||||
|
||||
// Check full sequential list
|
||||
List<ScreenHistoryBuffer.ScreenSnapshot> all = historyBuffer.getSnapshots();
|
||||
assertEquals(64, all.size());
|
||||
for (int i = 0; i < 64; i++) {
|
||||
String expected = "SCREEN_" + String.format("%03d", i + 6);
|
||||
assertTrue(all.get(i).getText().startsWith(expected));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Formatted table trims trailing whitespace per line")
|
||||
public void testFormattedTable() {
|
||||
screenBuffer.setCursorAddress(0);
|
||||
writeString(0, 0, "ABC");
|
||||
|
||||
ScreenHistoryBuffer.ScreenSnapshot snap = historyBuffer.snapshot(screenBuffer);
|
||||
assertNotNull(snap);
|
||||
String table = snap.getFormattedTable();
|
||||
String[] lines = table.split("\n", -1);
|
||||
assertEquals(24, lines.length);
|
||||
assertEquals("ABC", lines[0]);
|
||||
assertEquals("", lines[1]);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Bounded text range returns exact sub-box")
|
||||
public void testTextRange() {
|
||||
screenBuffer.setCursorAddress(0);
|
||||
writeString(0, 0, "ROW0");
|
||||
writeString(1, 0, "ROW1");
|
||||
|
||||
ScreenHistoryBuffer.ScreenSnapshot snap = historyBuffer.snapshot(screenBuffer);
|
||||
assertNotNull(snap);
|
||||
|
||||
String sub = snap.getTextRange(0, 0, 1, 3);
|
||||
assertEquals("ROW0\nROW1", sub);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ScreenHistoryListener receives callbacks on new snapshots")
|
||||
public void testListenerNotification() {
|
||||
AtomicInteger callCount = new AtomicInteger(0);
|
||||
ScreenHistoryBuffer.ScreenHistoryListener listener = (buf, snap) -> callCount.incrementAndGet();
|
||||
|
||||
historyBuffer.addListener(listener);
|
||||
|
||||
screenBuffer.setChar(0, 0, 'X');
|
||||
historyBuffer.snapshot(screenBuffer);
|
||||
assertEquals(1, callCount.get());
|
||||
|
||||
// Duplicate snapshot doesn't fire listener
|
||||
historyBuffer.snapshot(screenBuffer);
|
||||
assertEquals(1, callCount.get());
|
||||
|
||||
// New change fires listener
|
||||
screenBuffer.setChar(0, 1, 'Y');
|
||||
historyBuffer.snapshot(screenBuffer);
|
||||
assertEquals(2, callCount.get());
|
||||
|
||||
historyBuffer.removeListener(listener);
|
||||
screenBuffer.setChar(0, 2, 'Z');
|
||||
historyBuffer.snapshot(screenBuffer);
|
||||
assertEquals(2, callCount.get(), "Removed listener should not receive callbacks");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ScreenBuffer integration automatically records history on notifyScreenUpdate")
|
||||
public void testScreenBufferAutomaticSnapshot() {
|
||||
ScreenHistoryBuffer autoHist = screenBuffer.getHistoryBuffer();
|
||||
assertNotNull(autoHist);
|
||||
int initialSize = autoHist.size();
|
||||
|
||||
screenBuffer.setChar(0, 0, 'T');
|
||||
screenBuffer.notifyScreenUpdate();
|
||||
|
||||
assertEquals(initialSize + 1, autoHist.size());
|
||||
assertTrue(autoHist.getLatest().getText().startsWith("T"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user