Add tests
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 39s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m43s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m43s

This commit is contained in:
2026-09-28 01:57:27 +00:00
parent 9918c823c2
commit 8d5672c708
4 changed files with 938 additions and 0 deletions
@@ -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");
}
}
}
@@ -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);
}
}