Add bugfixes from discord
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m9s
Release j3270 / Build & Publish Release (push) Successful in 32s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 42s

This commit is contained in:
2026-09-10 09:24:39 -04:00
parent 59925c2787
commit 46a022d86b
23 changed files with 2075 additions and 202 deletions
@@ -0,0 +1,154 @@
package haus.nightmare.lib3270j.datastream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for high-throughput zero-allocation buffer primitives.
*/
public class FastByteBufferTest {
@BeforeEach
public void setUp() {
ReusableByteBufferPool.clear();
}
@Test
@DisplayName("FastByteBuffer operations: write, grow, reset, slice, and direct array access")
public void testFastByteBufferBasicOperations() {
FastByteBuffer buf = new FastByteBuffer(16);
assertEquals(0, buf.size());
assertTrue(buf.buffer().length >= 16);
buf.write(0x11);
buf.write(0x22);
assertEquals(2, buf.size());
assertEquals((byte) 0x11, buf.buffer()[0]);
assertEquals((byte) 0x22, buf.buffer()[1]);
byte[] payload = new byte[]{0x01, 0x02, 0x03, 0x04, 0x05};
buf.write(payload, 0, payload.length);
assertEquals(7, buf.size());
buf.write(payload, 1, 3); // write 0x02, 0x03, 0x04
assertEquals(10, buf.size());
// Test auto-growth
byte[] largeData = new byte[100];
for (int i = 0; i < largeData.length; i++) {
largeData[i] = (byte) (i & 0xFF);
}
buf.write(largeData, 0, largeData.length);
assertEquals(110, buf.size());
assertTrue(buf.buffer().length >= 110);
// Verify direct array access
byte[] raw = buf.buffer();
assertNotNull(raw);
assertEquals((byte) 0x11, raw[0]);
assertEquals((byte) 0x22, raw[1]);
// Verify ByteBuffer view
ByteBuffer readOnly = buf.asByteBuffer();
assertEquals(110, readOnly.remaining());
assertEquals((byte) 0x11, readOnly.get());
// Verify ByteBuffer slice
ByteBuffer slice = buf.slice(2, 5);
assertEquals(5, slice.remaining());
assertEquals((byte) 0x01, slice.get(0));
assertEquals((byte) 0x05, slice.get(4));
// Verify copied array
byte[] copied = buf.toByteArray();
assertEquals(110, copied.length);
assertEquals((byte) 0x11, copied[0]);
// Test reset
buf.reset();
assertEquals(0, buf.size());
// Buffer retained for zero-allocation reuse
assertTrue(buf.buffer().length >= 110);
}
@Test
@DisplayName("ReusableByteBufferPool acquires, releases, and recycles tiered buffers")
public void testByteBufferPoolRecycling() {
byte[] small1 = ReusableByteBufferPool.acquire(256);
assertEquals(ReusableByteBufferPool.SIZE_SMALL, small1.length);
byte[] medium1 = ReusableByteBufferPool.acquire(ReusableByteBufferPool.SIZE_MEDIUM);
assertEquals(ReusableByteBufferPool.SIZE_MEDIUM, medium1.length);
byte[] large1 = ReusableByteBufferPool.acquire(ReusableByteBufferPool.SIZE_LARGE);
assertEquals(ReusableByteBufferPool.SIZE_LARGE, large1.length);
// Non-standard oversized buffer
byte[] huge = ReusableByteBufferPool.acquire(65536);
assertEquals(65536, huge.length);
// Release back to pool
ReusableByteBufferPool.release(small1);
ReusableByteBufferPool.release(medium1);
ReusableByteBufferPool.release(large1);
ReusableByteBufferPool.release(huge);
// Next acquire should reuse the released instances
byte[] small2 = ReusableByteBufferPool.acquire(128);
assertSame(small1, small2, "Small buffer should be recycled from pool");
byte[] medium2 = ReusableByteBufferPool.acquire(2048);
assertSame(medium1, medium2, "Medium buffer should be recycled from pool");
byte[] large2 = ReusableByteBufferPool.acquire(30000);
assertSame(large1, large2, "Large buffer should be recycled from pool");
ByteBuffer bb = ReusableByteBufferPool.acquireByteBuffer(ReusableByteBufferPool.SIZE_SMALL);
assertNotNull(bb);
assertEquals(ReusableByteBufferPool.SIZE_SMALL, bb.capacity());
}
@Test
@DisplayName("ReusableByteBufferPool is safe under concurrent acquisition and release")
public void testConcurrentPoolAccess() throws InterruptedException {
int threads = 8;
int iterations = 1000;
ExecutorService executor = Executors.newFixedThreadPool(threads);
CountDownLatch latch = new CountDownLatch(threads);
AtomicInteger failures = new AtomicInteger(0);
for (int t = 0; t < threads; t++) {
executor.submit(() -> {
try {
for (int i = 0; i < iterations; i++) {
byte[] buf = ReusableByteBufferPool.acquire(ReusableByteBufferPool.SIZE_SMALL);
if (buf == null || buf.length != ReusableByteBufferPool.SIZE_SMALL) {
failures.incrementAndGet();
}
buf[0] = (byte) 0xAA;
buf[1] = (byte) 0xBB;
ReusableByteBufferPool.release(buf);
}
} catch (Exception e) {
failures.incrementAndGet();
} finally {
latch.countDown();
}
});
}
assertTrue(latch.await(5, TimeUnit.SECONDS));
executor.shutdown();
assertEquals(0, failures.get(), "No failures occurred during concurrent pool operations");
}
}
@@ -0,0 +1,141 @@
package haus.nightmare.lib3270j.epi;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class EpiDataStream3270Test {
private Screen screen;
private DataStream3270 dataStream;
@BeforeEach
public void setUp() {
screen = new Screen(80, 24);
dataStream = new DataStream3270(screen);
}
@Test
public void testEncodeAndDecodeAddress() {
int[] testAddresses = {0, 1, 79, 80, 1919, 2000, 4095};
byte[] target = new byte[2];
for (int addr : testAddresses) {
dataStream.encodeAddress(target, 0, addr);
int decoded = dataStream.decodeAddress(target[0], target[1]);
assertEquals(addr, decoded, "Address round-trip mismatch for " + addr);
}
// Test invalid decode
assertEquals(-1, dataStream.decodeAddress((byte) 0x00, (byte) 0x00));
assertEquals(-1, dataStream.decodeAddress((byte) 0xFF, (byte) 0xFF));
}
@Test
public void testCharacterTranslations() {
char ebcdicChar = dataStream.toEbcdic('A'); // ASCII 65 -> EBCDIC 0xC1
assertEquals('\u00c1', ebcdicChar);
byte asciiByte = dataStream.toAscii(ebcdicChar); // EBCDIC 0xC1 -> ASCII 65
assertEquals('A', (char) asciiByte);
// Boundary cases
char low = dataStream.toEbcdic(0x10);
assertEquals((char) 0x10, low);
}
@Test
public void testAnalyzeWriteAndFormat() throws Exception {
// Build 3270 Write buffer:
// CMD_W (49 / 0x31), WCC (0xC3), SBA (17), Addr(0, 0), SF (29), Attr (0xC1 = MDT set), Text "TEST"
byte[] stream = new byte[11];
stream[0] = 49; // Write
stream[1] = (byte) 0xC3; // WCC
stream[2] = 17; // SBA
dataStream.encodeAddress(stream, 3, 0);
stream[5] = 29; // SF
stream[6] = (byte) 0xC1; // Attribute (MDT=1)
stream[7] = (byte) 'T';
stream[8] = (byte) 'E';
stream[9] = (byte) 'S';
stream[10] = (byte) 'T';
dataStream.analyze(stream, stream.length);
assertEquals(1, screen.fieldCount());
Field f = screen.field(1);
assertNotNull(f);
assertTrue(f.hasAttribute());
assertEquals(1, f.dataTag());
// Now format outbound buffer
screen.setAID(AID.enter);
screen.setCursor(1, 1);
byte[] outBuf = new byte[100];
int outLen = dataStream.format(outBuf);
assertTrue(outLen > 0);
assertEquals(AID.enter.translate(), outBuf[0]);
// Cursor addr at [1, 2]
// SBA (17) at [3]
assertEquals(17, outBuf[3]);
// Data text starting at [6]
assertEquals((byte) 'T', outBuf[6]);
assertEquals((byte) 'E', outBuf[7]);
assertEquals((byte) 'S', outBuf[8]);
assertEquals((byte) 'T', outBuf[9]);
}
@Test
public void testReadBufferSerialization() throws Exception {
// Erase/Write (53), WCC (0), SBA (17), Addr(0), SF (29), Attr(0xC0), Text "OK"
byte[] stream = new byte[9];
stream[0] = 53; // Erase / Write
stream[1] = 0; // WCC
stream[2] = 17; // SBA
dataStream.encodeAddress(stream, 3, 0);
stream[5] = 29; // SF
stream[6] = (byte) 0xC0; // Unmodified attribute
stream[7] = (byte) 'O';
stream[8] = (byte) 'K';
dataStream.analyze(stream, stream.length);
assertEquals(1, screen.fieldCount());
byte[] outBuf = new byte[100];
int len = dataStream.readBuffer(outBuf);
assertTrue(len >= 8);
assertEquals(screen.getAID().translate(), outBuf[0]);
assertEquals(17, outBuf[3]); // SBA
assertEquals(29, outBuf[6]); // SF
}
@Test
public void testEpiScreenBufferBridge() {
screen.setWidth(80);
screen.setDepth(24);
screen.setCursor(2, 5);
Field f = new Field(screen, 80);
f.setAttribute(true);
f.setBaseAttribute((char) 0xC8);
f.setBytes(0, new byte[]{(byte) 0xC1, (byte) 0xC2}, 2); // 'A', 'B'
screen.insertField(f);
ScreenBuffer sb = new ScreenBuffer();
EpiScreenBufferBridge.copyToScreenBuffer(screen, sb);
assertEquals(80, sb.getCols());
assertEquals(24, sb.getRows());
assertEquals(84, sb.getCursorAddress()); // row 2 (index 1) * 80 + col 5 (index 4) = 84
// Reverse copy
Screen backScreen = new Screen();
EpiScreenBufferBridge.copyFromScreenBuffer(sb, backScreen);
assertEquals(80, backScreen.getWidth());
assertEquals(24, backScreen.getDepth());
assertEquals(2, backScreen.getCursorRow());
assertEquals(5, backScreen.getCursorColumn());
}
}