This commit is contained in:
+2
-2
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "haus.nightmare.a3270"
|
applicationId "haus.nightmare.a3270"
|
||||||
minSdk 24
|
minSdk 24
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode 8
|
versionCode 9
|
||||||
versionName "1.2.3"
|
versionName "1.2.4"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:versionCode="8"
|
android:versionCode="9"
|
||||||
android:versionName="1.2.3">
|
android:versionName="1.2.4">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|||||||
@@ -555,7 +555,7 @@ fun MainScreen(
|
|||||||
val fieldStatus = remember(screenBuffer, cursorAddr, screenVersion) {
|
val fieldStatus = remember(screenBuffer, cursorAddr, screenVersion) {
|
||||||
val buf = screenBuffer
|
val buf = screenBuffer
|
||||||
if (buf != null && buf.isFormatted) {
|
if (buf != null && buf.isFormatted) {
|
||||||
val cell = buf.getCell(cursorAddr)
|
val cell = buf.getDisplayCell(cursorAddr)
|
||||||
val isDbcs = (cell != null && (cell.db == haus.nightmare.lib3270j.screen.ExtendedAttribute.DB_LEFT ||
|
val isDbcs = (cell != null && (cell.db == haus.nightmare.lib3270j.screen.ExtendedAttribute.DB_LEFT ||
|
||||||
cell.db == haus.nightmare.lib3270j.screen.ExtendedAttribute.DB_RIGHT ||
|
cell.db == haus.nightmare.lib3270j.screen.ExtendedAttribute.DB_RIGHT ||
|
||||||
cell.cs == haus.nightmare.lib3270j.screen.ExtendedAttribute.CS_DBCS))
|
cell.cs == haus.nightmare.lib3270j.screen.ExtendedAttribute.CS_DBCS))
|
||||||
|
|||||||
@@ -481,9 +481,12 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
val c = client
|
val c = client
|
||||||
if (c != null) {
|
if (c != null) {
|
||||||
_cursorAddress.value = c.screenBuffer.cursorAddress
|
_cursorAddress.value = c.screenBuffer.displayCursorAddress
|
||||||
|
val snapVersion = c.screenBuffer.snapshotVersion
|
||||||
|
_screenVersion.value = if (snapVersion > 0) snapVersion else System.currentTimeMillis()
|
||||||
|
} else {
|
||||||
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
_screenVersion.value = System.currentTimeMillis()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1096,16 +1099,26 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
val buf = newClient.screenBuffer
|
val buf = newClient.screenBuffer
|
||||||
_screenBuffer.value = buf
|
_screenBuffer.value = buf
|
||||||
if (buf != null) {
|
if (buf != null) {
|
||||||
_rows.value = buf.rows
|
val snap = buf.displaySnapshot
|
||||||
_cols.value = buf.cols
|
if (snap != null) {
|
||||||
_cursorAddress.value = buf.cursorAddress
|
_rows.value = snap.rows
|
||||||
|
_cols.value = snap.cols
|
||||||
|
_cursorAddress.value = snap.cursorAddress
|
||||||
|
_screenVersion.value = snap.version
|
||||||
|
} else {
|
||||||
|
_rows.value = buf.displayRows
|
||||||
|
_cols.value = buf.displayCols
|
||||||
|
_cursorAddress.value = buf.displayCursorAddress
|
||||||
|
_screenVersion.value = buf.snapshotVersion
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
_isKeyboardLocked.value = newClient.inputProcessor.isKeyboardLocked
|
_isKeyboardLocked.value = newClient.inputProcessor.isKeyboardLocked
|
||||||
_isInsertMode.value = newClient.isInsertMode
|
_isInsertMode.value = newClient.isInsertMode
|
||||||
_isDocMode.value = newClient.isDocMode
|
_isDocMode.value = newClient.isDocMode
|
||||||
_isWordWrap.value = newClient.isWordWrap
|
_isWordWrap.value = newClient.isWordWrap
|
||||||
_isAplMode.value = newClient.isAplMode
|
_isAplMode.value = newClient.isAplMode
|
||||||
_screenVersion.value = System.currentTimeMillis()
|
|
||||||
|
|
||||||
// Drive CUT mode file transfers if active
|
// Drive CUT mode file transfers if active
|
||||||
fileTransferCoordinator?.onScreenUpdated()
|
fileTransferCoordinator?.onScreenUpdated()
|
||||||
@@ -1478,9 +1491,10 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
|
|
||||||
private fun getLineText(buf: ScreenBuffer, row: Int, cols: Int): String {
|
private fun getLineText(buf: ScreenBuffer, row: Int, cols: Int): String {
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
|
val snap = buf.displaySnapshot
|
||||||
for (c in 0 until cols) {
|
for (c in 0 until cols) {
|
||||||
val addr = row * cols + c
|
val addr = row * cols + c
|
||||||
val cell = buf.getCell(addr)
|
val cell = snap?.getCell(addr) ?: buf.getDisplayCell(addr)
|
||||||
if (cell != null && !cell.isFieldAttribute) {
|
if (cell != null && !cell.isFieldAttribute) {
|
||||||
val ch = cell.ucs4
|
val ch = cell.ucs4
|
||||||
if (ch in '!'..'~' || ch.code > 127) {
|
if (ch in '!'..'~' || ch.code > 127) {
|
||||||
@@ -1498,9 +1512,10 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
private fun computeScreenContentHash(buf: ScreenBuffer?): Int {
|
private fun computeScreenContentHash(buf: ScreenBuffer?): Int {
|
||||||
if (buf == null) return 0
|
if (buf == null) return 0
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
val total = buf.rows * buf.cols
|
val snap = buf.displaySnapshot
|
||||||
|
val total = (snap?.rows ?: buf.displayRows) * (snap?.cols ?: buf.displayCols)
|
||||||
for (i in 0 until total) {
|
for (i in 0 until total) {
|
||||||
val cell = buf.getCell(i)
|
val cell = snap?.getCell(i) ?: buf.getDisplayCell(i)
|
||||||
if (cell != null && !cell.isFieldAttribute && cell.ucs4 > ' ' && cell.ucs4.code != 0xFFFF) {
|
if (cell != null && !cell.isFieldAttribute && cell.ucs4 > ' ' && cell.ucs4.code != 0xFFFF) {
|
||||||
sb.append(cell.ucs4)
|
sb.append(cell.ucs4)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,6 +285,7 @@ fun ScreenExporterDialog(
|
|||||||
|
|
||||||
private fun generatePlainText(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, maskHidden: Boolean): String {
|
private fun generatePlainText(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, maskHidden: Boolean): String {
|
||||||
if (screenBuffer == null) return ""
|
if (screenBuffer == null) return ""
|
||||||
|
screenBuffer.updateDisplaySnapshot()
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
var currentHidden = false
|
var currentHidden = false
|
||||||
|
|
||||||
@@ -292,7 +293,7 @@ private fun generatePlainText(screenBuffer: ScreenBuffer?, rows: Int, cols: Int,
|
|||||||
val rowSb = StringBuilder()
|
val rowSb = StringBuilder()
|
||||||
for (c in 0 until cols) {
|
for (c in 0 until cols) {
|
||||||
val addr = r * cols + c
|
val addr = r * cols + c
|
||||||
val cell = screenBuffer.getCell(addr)
|
val cell = screenBuffer.getDisplayCell(addr) ?: continue
|
||||||
if (cell.fa.toInt() != 0) {
|
if (cell.fa.toInt() != 0) {
|
||||||
val faVal = cell.fa.toInt() and 0xFF
|
val faVal = cell.fa.toInt() and 0xFF
|
||||||
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
||||||
@@ -317,6 +318,7 @@ private fun generatePlainText(screenBuffer: ScreenBuffer?, rows: Int, cols: Int,
|
|||||||
|
|
||||||
private fun generateHtml(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, maskHidden: Boolean): String {
|
private fun generateHtml(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, maskHidden: Boolean): String {
|
||||||
if (screenBuffer == null) return "<html><body><pre></pre></body></html>"
|
if (screenBuffer == null) return "<html><body><pre></pre></body></html>"
|
||||||
|
screenBuffer.updateDisplaySnapshot()
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("<!DOCTYPE html>\n<html>\n<head>\n")
|
sb.append("<!DOCTYPE html>\n<html>\n<head>\n")
|
||||||
sb.append("<meta charset=\"UTF-8\">\n")
|
sb.append("<meta charset=\"UTF-8\">\n")
|
||||||
@@ -343,7 +345,7 @@ private fun generateHtml(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, mask
|
|||||||
for (r in 0 until rows) {
|
for (r in 0 until rows) {
|
||||||
for (c in 0 until cols) {
|
for (c in 0 until cols) {
|
||||||
val addr = r * cols + c
|
val addr = r * cols + c
|
||||||
val cell = screenBuffer.getCell(addr)
|
val cell = screenBuffer.getDisplayCell(addr) ?: continue
|
||||||
if (cell.fa.toInt() != 0) {
|
if (cell.fa.toInt() != 0) {
|
||||||
val faVal = cell.fa.toInt() and 0xFF
|
val faVal = cell.fa.toInt() and 0xFF
|
||||||
currentProt = (faVal and FA_PROTECT) != 0
|
currentProt = (faVal and FA_PROTECT) != 0
|
||||||
@@ -430,6 +432,7 @@ private fun generateBitmap(
|
|||||||
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), bgPaint)
|
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), bgPaint)
|
||||||
|
|
||||||
if (screenBuffer == null) return bitmap
|
if (screenBuffer == null) return bitmap
|
||||||
|
screenBuffer.updateDisplaySnapshot()
|
||||||
|
|
||||||
// Layer 1: Vector graphics plane (under text)
|
// Layer 1: Vector graphics plane (under text)
|
||||||
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
||||||
@@ -457,7 +460,7 @@ private fun generateBitmap(
|
|||||||
for (r in 0 until rows) {
|
for (r in 0 until rows) {
|
||||||
for (c in 0 until cols) {
|
for (c in 0 until cols) {
|
||||||
val addr = r * cols + c
|
val addr = r * cols + c
|
||||||
val cell = screenBuffer.getCell(addr)
|
val cell = screenBuffer.getDisplayCell(addr) ?: continue
|
||||||
val left = (c * charWidth).toFloat()
|
val left = (c * charWidth).toFloat()
|
||||||
val top = (r * charHeight).toFloat()
|
val top = (r * charHeight).toFloat()
|
||||||
|
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ fun TerminalView(
|
|||||||
if (buf != null && buf.isFormatted) {
|
if (buf != null && buf.isFormatted) {
|
||||||
val faPos = buf.findFieldAttribute(clickAddr)
|
val faPos = buf.findFieldAttribute(clickAddr)
|
||||||
if (faPos >= 0) {
|
if (faPos >= 0) {
|
||||||
val fa = buf.getCell(faPos).fa.toInt() and 0xFF
|
val fa = buf.getDisplayCell(faPos)?.fa?.toInt()?.and(0xFF) ?: 0
|
||||||
if (faIsSelectable(fa) && !faIsZero(fa)) {
|
if (faIsSelectable(fa) && !faIsZero(fa)) {
|
||||||
onLightPenSelect?.invoke(clickAddr)
|
onLightPenSelect?.invoke(clickAddr)
|
||||||
isDetectable = true
|
isDetectable = true
|
||||||
@@ -232,7 +232,7 @@ fun TerminalView(
|
|||||||
var addr = clickAddr
|
var addr = clickAddr
|
||||||
if (buf != null && buf.isFormatted) {
|
if (buf != null && buf.isFormatted) {
|
||||||
val faVal = buf.getFieldAttributeAt(addr)
|
val faVal = buf.getFieldAttributeAt(addr)
|
||||||
if (faIsProtected(faVal.toInt() and 0xFF) || buf.getCell(addr).isFieldAttribute) {
|
if (faIsProtected(faVal.toInt() and 0xFF) || buf.getDisplayCell(addr)?.isFieldAttribute == true) {
|
||||||
addr = buf.findNextUnprotected(addr)
|
addr = buf.findNextUnprotected(addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,10 +247,15 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
val version = currentScreenVersion
|
val version = currentScreenVersion
|
||||||
val activeCursorAddr = currentCursorAddr
|
|
||||||
if (version == -1L) return@Canvas
|
if (version == -1L) return@Canvas
|
||||||
|
|
||||||
val metrics = calculateGridMetrics(size.width, size.height, cols, rows)
|
val buf = screenBuffer
|
||||||
|
val snapshot = buf?.displaySnapshot
|
||||||
|
val snapRows = snapshot?.rows ?: (buf?.displayRows ?: rows)
|
||||||
|
val snapCols = snapshot?.cols ?: (buf?.displayCols ?: cols)
|
||||||
|
val activeCursorAddr = snapshot?.cursorAddress ?: currentCursorAddr
|
||||||
|
|
||||||
|
val metrics = calculateGridMetrics(size.width, size.height, snapCols, snapRows)
|
||||||
if (metrics.cellWidth <= 0f || metrics.cellHeight <= 0f) return@Canvas
|
if (metrics.cellWidth <= 0f || metrics.cellHeight <= 0f) return@Canvas
|
||||||
|
|
||||||
val cellWidth = metrics.cellWidth
|
val cellWidth = metrics.cellWidth
|
||||||
@@ -272,8 +277,7 @@ fun TerminalView(
|
|||||||
paint.textSize = baseTextSize * (cellWidth * 0.85f / measuredW)
|
paint.textSize = baseTextSize * (cellWidth * 0.85f / measuredW)
|
||||||
}
|
}
|
||||||
|
|
||||||
val buf = screenBuffer
|
val totalCells = snapRows * snapCols
|
||||||
val totalCells = rows * cols
|
|
||||||
|
|
||||||
var currentFA: Byte = 0
|
var currentFA: Byte = 0
|
||||||
var currentFieldEa: ExtendedAttribute? = null
|
var currentFieldEa: ExtendedAttribute? = null
|
||||||
@@ -287,10 +291,10 @@ fun TerminalView(
|
|||||||
val start = selectionStart
|
val start = selectionStart
|
||||||
val end = selectionEnd
|
val end = selectionEnd
|
||||||
if (start != null && end != null && cellWidth > 0f && cellHeight > 0f) {
|
if (start != null && end != null && cellWidth > 0f && cellHeight > 0f) {
|
||||||
val startCol = ((start.x - offsetX) / cellWidth).toInt().coerceIn(0, cols - 1)
|
val startCol = ((start.x - offsetX) / cellWidth).toInt().coerceIn(0, snapCols - 1)
|
||||||
val startRow = ((start.y - offsetY) / cellHeight).toInt().coerceIn(0, rows - 1)
|
val startRow = ((start.y - offsetY) / cellHeight).toInt().coerceIn(0, snapRows - 1)
|
||||||
val endCol = ((end.x - offsetX) / cellWidth).toInt().coerceIn(0, cols - 1)
|
val endCol = ((end.x - offsetX) / cellWidth).toInt().coerceIn(0, snapCols - 1)
|
||||||
val endRow = ((end.y - offsetY) / cellHeight).toInt().coerceIn(0, rows - 1)
|
val endRow = ((end.y - offsetY) / cellHeight).toInt().coerceIn(0, snapRows - 1)
|
||||||
|
|
||||||
selMinRow = minOf(startRow, endRow)
|
selMinRow = minOf(startRow, endRow)
|
||||||
selMaxRow = maxOf(startRow, endRow)
|
selMaxRow = maxOf(startRow, endRow)
|
||||||
@@ -317,11 +321,11 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Draw Crosshair Ruler if enabled
|
// 2. Draw Crosshair Ruler if enabled
|
||||||
if (crosshairRuler && cols > 0 && rows > 0) {
|
if (crosshairRuler && snapCols > 0 && snapRows > 0) {
|
||||||
val curCol = (activeCursorAddr % cols).coerceIn(0, cols - 1)
|
val curCol = (activeCursorAddr % snapCols).coerceIn(0, snapCols - 1)
|
||||||
val curRow = ((activeCursorAddr / cols) % rows).coerceIn(0, rows - 1)
|
val curRow = ((activeCursorAddr / snapCols) % snapRows).coerceIn(0, snapRows - 1)
|
||||||
val gridW = cols * cellWidth
|
val gridW = snapCols * cellWidth
|
||||||
val gridH = rows * cellHeight
|
val gridH = snapRows * cellHeight
|
||||||
val cx = offsetX + curCol * cellWidth
|
val cx = offsetX + curCol * cellWidth
|
||||||
val cy = offsetY + curRow * cellHeight
|
val cy = offsetY + curRow * cellHeight
|
||||||
|
|
||||||
@@ -337,9 +341,9 @@ fun TerminalView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (r in 0 until rows) {
|
for (r in 0 until snapRows) {
|
||||||
for (c in 0 until cols) {
|
for (c in 0 until snapCols) {
|
||||||
val addr = r * cols + c
|
val addr = r * snapCols + c
|
||||||
if (addr >= totalCells) break
|
if (addr >= totalCells) break
|
||||||
|
|
||||||
val left = offsetX + c * cellWidth
|
val left = offsetX + c * cellWidth
|
||||||
@@ -355,7 +359,7 @@ fun TerminalView(
|
|||||||
var csVal = 0
|
var csVal = 0
|
||||||
var ecVal = 0
|
var ecVal = 0
|
||||||
|
|
||||||
val ea = if (buf != null) buf.getCell(addr) else null
|
val ea = snapshot?.getCell(addr) ?: buf?.getDisplayCell(addr)
|
||||||
|
|
||||||
if (ea != null) {
|
if (ea != null) {
|
||||||
if (ea.isFieldAttribute) {
|
if (ea.isFieldAttribute) {
|
||||||
@@ -521,7 +525,7 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Field Outlining (0xC2 / TY_FOUTLINE)
|
// Field Outlining (0xC2 / TY_FOUTLINE)
|
||||||
val cellEa = if (buf != null) buf.getCell(addr) else null
|
val cellEa = snapshot?.getCell(addr) ?: buf?.getDisplayCell(addr)
|
||||||
var outline = if (cellEa != null) cellEa.ol.toInt() and 0xFF else 0
|
var outline = if (cellEa != null) cellEa.ol.toInt() and 0xFF else 0
|
||||||
val fEa = currentFieldEa
|
val fEa = currentFieldEa
|
||||||
val olUnderline = ExtendedAttribute.OL_UNDERLINE.toInt() and 0xFF
|
val olUnderline = ExtendedAttribute.OL_UNDERLINE.toInt() and 0xFF
|
||||||
@@ -536,11 +540,13 @@ fun TerminalView(
|
|||||||
|
|
||||||
val size = totalCells
|
val size = totalCells
|
||||||
val prevAddr = (addr - 1 + size) % size
|
val prevAddr = (addr - 1 + size) % size
|
||||||
if (buf != null && (buf.getDisplayCell(prevAddr)?.isFieldAttribute == true || c == 0)) {
|
val prevCell = snapshot?.getCell(prevAddr) ?: buf?.getDisplayCell(prevAddr)
|
||||||
|
if (prevCell?.isFieldAttribute == true || c == 0) {
|
||||||
if ((fol and olLeft) != 0) outline = outline or olLeft
|
if ((fol and olLeft) != 0) outline = outline or olLeft
|
||||||
}
|
}
|
||||||
val nextAddr = (addr + 1) % size
|
val nextAddr = (addr + 1) % size
|
||||||
if (buf != null && (buf.getDisplayCell(nextAddr)?.isFieldAttribute == true || c == cols - 1)) {
|
val nextCell = snapshot?.getCell(nextAddr) ?: buf?.getDisplayCell(nextAddr)
|
||||||
|
if (nextCell?.isFieldAttribute == true || c == snapCols - 1) {
|
||||||
if ((fol and olRight) != 0) outline = outline or olRight
|
if ((fol and olRight) != 0) outline = outline or olRight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -889,7 +895,7 @@ private fun copySelection(
|
|||||||
for (c in colStart..colEnd) {
|
for (c in colStart..colEnd) {
|
||||||
val addr = r * cols + c
|
val addr = r * cols + c
|
||||||
if (addr < (rows * cols)) {
|
if (addr < (rows * cols)) {
|
||||||
val cell = screenBuffer.getCell(addr)
|
val cell = screenBuffer.getDisplayCell(addr)
|
||||||
if (cell != null && !cell.isFieldAttribute) {
|
if (cell != null && !cell.isFieldAttribute) {
|
||||||
if (cell.db == ExtendedAttribute.DB_RIGHT) {
|
if (cell.db == ExtendedAttribute.DB_RIGHT) {
|
||||||
val isSubsequent = if (blockSelectMode) c > minCol else (addr > (minRow * cols + minCol))
|
val isSubsequent = if (blockSelectMode) c > minCol else (addr > (minRow * cols + minCol))
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package haus.nightmare.a3270
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
|
||||||
|
class DisplaySnapshotIntegrationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSnapshotCaptureAndLockFreeReads() {
|
||||||
|
val translator = EbcdicTranslator()
|
||||||
|
val sb = ScreenBuffer(TerminalModel.IBM_3279_2, translator)
|
||||||
|
|
||||||
|
sb.erase(false)
|
||||||
|
val initialVersion = sb.snapshotVersion
|
||||||
|
|
||||||
|
sb.getCell(0).ucs4 = 'H'
|
||||||
|
sb.getCell(1).ucs4 = 'E'
|
||||||
|
sb.getCell(2).ucs4 = 'L'
|
||||||
|
sb.getCell(3).ucs4 = 'L'
|
||||||
|
sb.getCell(4).ucs4 = 'O'
|
||||||
|
sb.setCursorAddress(42)
|
||||||
|
sb.updateDisplaySnapshot()
|
||||||
|
|
||||||
|
val snap = sb.displaySnapshot
|
||||||
|
assertNotNull("DisplaySnapshot must not be null after updateDisplaySnapshot()", snap)
|
||||||
|
assertTrue("Snapshot version must increase", sb.snapshotVersion > initialVersion)
|
||||||
|
assertEquals(24, snap.rows)
|
||||||
|
assertEquals(80, snap.cols)
|
||||||
|
assertEquals(42, snap.cursorAddress)
|
||||||
|
assertEquals('H', snap.getCell(0)?.ucs4)
|
||||||
|
assertEquals('E', snap.getCell(1)?.ucs4)
|
||||||
|
assertEquals('L', snap.getCell(2)?.ucs4)
|
||||||
|
assertEquals('L', snap.getCell(3)?.ucs4)
|
||||||
|
assertEquals('O', snap.getCell(4)?.ucs4)
|
||||||
|
|
||||||
|
// getDisplayCell fallback
|
||||||
|
assertEquals('H', sb.getDisplayCell(0)?.ucs4)
|
||||||
|
assertEquals(42, sb.displayCursorAddress)
|
||||||
|
assertEquals(24, sb.displayRows)
|
||||||
|
assertEquals(80, sb.displayCols)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTearFreeRenderingUnderConcurrentBufferMutation() {
|
||||||
|
val translator = EbcdicTranslator()
|
||||||
|
val sb = ScreenBuffer(TerminalModel.IBM_3279_2, translator)
|
||||||
|
sb.erase(false)
|
||||||
|
|
||||||
|
val size = 24 * 80
|
||||||
|
val totalUpdates = 2000
|
||||||
|
val workerCount = 4
|
||||||
|
val executor = Executors.newFixedThreadPool(workerCount + 1)
|
||||||
|
val running = AtomicBoolean(true)
|
||||||
|
val tornCellCount = AtomicInteger(0)
|
||||||
|
val exceptionCount = AtomicInteger(0)
|
||||||
|
val totalReads = AtomicLong(0)
|
||||||
|
|
||||||
|
val startLatch = CountDownLatch(1)
|
||||||
|
val writersDoneLatch = CountDownLatch(1)
|
||||||
|
|
||||||
|
// Reader workers simulating Compose Canvas draw passes
|
||||||
|
for (w in 0 until workerCount) {
|
||||||
|
executor.submit {
|
||||||
|
try {
|
||||||
|
startLatch.await()
|
||||||
|
while (running.get()) {
|
||||||
|
val snap = sb.displaySnapshot ?: continue
|
||||||
|
val snapSize = snap.rows * snap.cols
|
||||||
|
|
||||||
|
for (addr in 0 until snapSize) {
|
||||||
|
val cell = snap.getCell(addr) ?: continue
|
||||||
|
totalReads.incrementAndGet()
|
||||||
|
|
||||||
|
// Patterned validation:
|
||||||
|
// ucs4 = 'A' + mod, ec = 0x10 + mod, fg = 0x30 + mod, bg = 0x50 + mod
|
||||||
|
if (cell.ucs4 in 'A'..'Z') {
|
||||||
|
val ucsOffset = cell.ucs4 - 'A'
|
||||||
|
val ecOffset = (cell.ec.toInt() and 0xFF) - 0x10
|
||||||
|
val fgOffset = (cell.fg.toInt() and 0xFF) - 0x30
|
||||||
|
val bgOffset = (cell.bg.toInt() and 0xFF) - 0x50
|
||||||
|
|
||||||
|
if (ucsOffset != ecOffset || ucsOffset != fgOffset || ucsOffset != bgOffset) {
|
||||||
|
tornCellCount.incrementAndGet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
exceptionCount.incrementAndGet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writer task simulating background Telnet network receiver thread
|
||||||
|
executor.submit {
|
||||||
|
try {
|
||||||
|
startLatch.await()
|
||||||
|
for (iter in 0 until totalUpdates) {
|
||||||
|
val mod = iter % 26
|
||||||
|
val ch = ('A' + mod)
|
||||||
|
val ec = (0x10 + mod).toByte()
|
||||||
|
val fg = (0x30 + mod).toByte()
|
||||||
|
val bg = (0x50 + mod).toByte()
|
||||||
|
|
||||||
|
val rawBuf = sb.buffer
|
||||||
|
for (i in 0 until size) {
|
||||||
|
val cell = rawBuf[i]
|
||||||
|
cell.ucs4 = ch
|
||||||
|
cell.ec = ec
|
||||||
|
cell.fg = fg
|
||||||
|
cell.bg = bg
|
||||||
|
}
|
||||||
|
sb.setCursorAddress(iter % size)
|
||||||
|
sb.updateDisplaySnapshot()
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
exceptionCount.incrementAndGet()
|
||||||
|
} finally {
|
||||||
|
writersDoneLatch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startLatch.countDown()
|
||||||
|
val finished = writersDoneLatch.await(10, TimeUnit.SECONDS)
|
||||||
|
assertTrue("Writer must complete updates without deadlock", finished)
|
||||||
|
|
||||||
|
running.set(false)
|
||||||
|
executor.shutdown()
|
||||||
|
assertTrue("Executor must terminate cleanly", executor.awaitTermination(5, TimeUnit.SECONDS))
|
||||||
|
|
||||||
|
assertEquals("Zero exceptions must occur during concurrent execution", 0, exceptionCount.get())
|
||||||
|
assertEquals("Zero torn cell states must occur during DisplaySnapshot access", 0, tornCellCount.get())
|
||||||
|
assertTrue("Total reads must be significant", totalReads.get() > 50_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testConcurrentReadsDuringScreenResizing() {
|
||||||
|
val translator = EbcdicTranslator()
|
||||||
|
val sb = ScreenBuffer(24, 80, 27, 132, translator)
|
||||||
|
val workerCount = 4
|
||||||
|
val executor = Executors.newFixedThreadPool(workerCount + 1)
|
||||||
|
val running = AtomicBoolean(true)
|
||||||
|
val exceptionCount = AtomicInteger(0)
|
||||||
|
val successfulReads = AtomicLong(0)
|
||||||
|
|
||||||
|
val startLatch = CountDownLatch(1)
|
||||||
|
val writerLatch = CountDownLatch(1)
|
||||||
|
|
||||||
|
for (w in 0 until workerCount) {
|
||||||
|
executor.submit {
|
||||||
|
try {
|
||||||
|
startLatch.await()
|
||||||
|
while (running.get()) {
|
||||||
|
val snap = sb.displaySnapshot ?: continue
|
||||||
|
val total = snap.rows * snap.cols
|
||||||
|
|
||||||
|
for (i in 0 until total) {
|
||||||
|
val cell = snap.getCell(i)
|
||||||
|
if (cell != null) {
|
||||||
|
successfulReads.incrementAndGet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
exceptionCount.incrementAndGet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writer alternates dimensions between 24x80 and 27x132
|
||||||
|
executor.submit {
|
||||||
|
try {
|
||||||
|
startLatch.await()
|
||||||
|
for (iter in 0 until 500) {
|
||||||
|
sb.erase((iter % 2) != 0)
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
exceptionCount.incrementAndGet()
|
||||||
|
} finally {
|
||||||
|
writerLatch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startLatch.countDown()
|
||||||
|
assertTrue("Resize writer must complete without deadlock", writerLatch.await(10, TimeUnit.SECONDS))
|
||||||
|
|
||||||
|
running.set(false)
|
||||||
|
executor.shutdown()
|
||||||
|
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS))
|
||||||
|
|
||||||
|
assertEquals("Zero exceptions must occur during dynamic dimension resizing", 0, exceptionCount.get())
|
||||||
|
assertTrue("Successful reads across dimension resizing", successfulReads.get() > 20_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user