Skip to content

test: implement more of the mock screen#917

Merged
gdamore merged 1 commit into
mainfrom
tscreen-test
Dec 21, 2025
Merged

test: implement more of the mock screen#917
gdamore merged 1 commit into
mainfrom
tscreen-test

Conversation

@gdamore

@gdamore gdamore commented Dec 21, 2025

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Improved Unicode/UTF‑8 handling, richer input-state processing, and more robust cursor/erase behaviors.
    • Public API updates: attribute field renamed and new public fields for bell counting, secondary attributes, and private modes.
  • Bug Fixes

    • Better synchronization for reads/drain and more reliable start/stop behavior to avoid race conditions.
  • Tests

    • Added platform build constraints and a short startup delay to stabilize tests.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Dec 21, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

MockTty gains an input state machine (ESC/CSI/OSC), UTF‑8 decoding/buffering, cursor and erase handlers, and goroutine lifecycle coordination. Public changes: attr renamed to Attr; added Bells, SecondaryAttributes, and PrivateModes. Test adds build constraints and a short sleep.

Changes

Cohort / File(s) Summary
MockTty implementation
mock/mock.go
Added state machine constants and state fields (state, escBuf, utfBuf, utfLen, waitG, ...). Implemented UTF‑8 decoding, CSI/OSC parsing helpers (intParams, handleCsi, handleOSC), cursor/erase helpers (moveUp, moveDown, moveForward, moveBackward, eraseCell), and a run input loop. Updated Read/Drain/Start/Stop to coordinate with the goroutine and single‑byte/delayed reads. Reset initializes primary/secondary/extended attributes and buffers. Renamed public field attrAttr and added public fields Bells (int), SecondaryAttributes (string), and PrivateModes (map[int]int).
Tests: build constraint & timing
mock/mock_test.go
Added build constraints to exclude js/wasm, imported time, and added a 100ms sleep in TestMockStart after window size check.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Inspect state machine transitions (ESC/CSI/OSC) and edge cases.
  • Verify UTF‑8 decoding, multibyte boundary handling, and buffer usage.
  • Review goroutine lifecycle, WaitGroup coordination, and Read/Drain synchronization for races or deadlocks.
  • Confirm public field rename/additions and update dependent callers/tests.

Possibly related PRs

Poem

🐇 I hop through bytes and funny codes,

escapes and CSIs on winding roads.
UTF‑8 whiskers twitch and sing,
bells that ring and cursors spring,
a mocktty jig — I dance and decode! 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: expanding MockTty's implementation with state machine logic, input handling, and new public fields to better simulate terminal behavior.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch tscreen-test

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1cea08c and 8c22982.

📒 Files selected for processing (2)
  • mock/mock.go (5 hunks)
  • mock/mock_test.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • mock/mock_test.go
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: gdamore
Repo: gdamore/tcell PR: 908
File: stdin_unix.go:128-155
Timestamp: 2025-12-18T02:50:37.669Z
Learning: In the tcell Tty interface implementations (stdin_unix.go, tty_unix.go, tty_win.go, tty_plan9.go), the caller is responsible for providing synchronization/mutual exclusion guarantees for method calls like NotifyResize. The Tty implementations themselves do not need internal locking for these methods, as documented in tty.go. The tscreen.go layer handles the synchronization.
📚 Learning: 2025-12-18T02:50:37.669Z
Learnt from: gdamore
Repo: gdamore/tcell PR: 908
File: stdin_unix.go:128-155
Timestamp: 2025-12-18T02:50:37.669Z
Learning: In the tcell Tty interface implementations (stdin_unix.go, tty_unix.go, tty_win.go, tty_plan9.go), the caller is responsible for providing synchronization/mutual exclusion guarantees for method calls like NotifyResize. The Tty implementations themselves do not need internal locking for these methods, as documented in tty.go. The tscreen.go layer handles the synchronization.

Applied to files:

  • mock/mock.go
🧬 Code graph analysis (1)
mock/mock.go (1)
attr.go (2)
  • AttrMask (22-22)
  • AttrNone (35-35)
🔇 Additional comments (18)
mock/mock.go (18)

20-31: LGTM!

The new imports appropriately support the state machine, UTF-8 decoding, synchronization, and wide character width calculation.


47-71: LGTM!

The field additions support the new state machine and goroutine lifecycle. The public API changes (Attr, Bells, SecondaryAttributes, PrivateModes) align with the PR objectives. Consistent with learnings, no internal locking is added since the caller (tscreen layer) provides synchronization.


73-84: LGTM!

State constants are well-defined and documented. The comment clarifies these states parse application output.


86-103: LGTM!

The CSI parameter parsing logic correctly handles variable-length parameter lists and provides sensible defaults for malformed or missing parameters.


105-135: LGTM!

The cursor movement helpers correctly clamp to terminal bounds in all four directions.


137-143: LGTM!

The cell index calculation correctly uses (y * mt.Cols) + x for row-major order, and bounds checking is comprehensive.


173-203: LGTM!

Cursor positioning commands (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP) correctly validate bounds and update cursor position.


231-245: LGTM!

EL (Erase in Line) commands correctly include the cursor position in all three cases (0: cursor to end, 1: start to cursor, 2: entire line).


246-283: LGTM!

Device attribute queries, status reports, and private mode handling are correctly implemented with appropriate replies sent back via the ReadQ channel.


285-288: LGTM!

OSC sequence handling is currently a no-op placeholder, which is acceptable for a mock implementation.


290-298: LGTM!

The reply helper safely sends data to the application's stdin while respecting the stop signal.


399-453: LGTM!

ESC state handling correctly processes single-character escapes (cursor movement, bell) and transitions to other states for multi-character sequences. Row bounds checks for cursor movement are correct.


455-476: LGTM!

CSI and OSC state handling correctly accumulates parameter bytes and processes final characters, supporting both 7-bit and 8-bit sequence terminators.


480-497: LGTM!

Goroutine lifecycle is correctly managed. Start() initializes the stop channel and launches the goroutine, while Stop() safely signals termination and waits for cleanup.


499-526: LGTM!

Read implementation correctly follows VMIN 1, VTIME 0 semantics: blocks until at least one byte is available, then opportunistically reads more without blocking, respecting the stop signal throughout.


528-539: LGTM!

Write correctly sends bytes to the processing queue with stop signal handling, and Close is appropriately a no-op for this mock.


541-551: LGTM!

Drain correctly waits for pending reads to complete and safely closes the stop channel with proper guard logic to prevent double-close panics.


553-584: LGTM!

Terminal control methods are correctly implemented. Reset properly initializes all fields including the new public attributes (PrimaryAttributes, SecondaryAttributes, ExtendedAttributes) and PrivateModes map with appropriate defaults.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Dec 21, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 37.24832% with 187 lines in your changes missing coverage. Please review.
✅ Project coverage is 29.68%. Comparing base (b0458c0) to head (8c22982).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
mock/mock.go 37.24% 183 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #917      +/-   ##
==========================================
+ Coverage   27.95%   29.68%   +1.73%     
==========================================
  Files          40       40              
  Lines        3456     3743     +287     
==========================================
+ Hits          966     1111     +145     
- Misses       2399     2534     +135     
- Partials       91       98       +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (2)
mock/mock_test.go (1)

43-43: Fixed sleep may be fragile in tests.

Fixed-duration sleeps can cause test flakiness on slow CI runners. Consider whether a synchronization mechanism (e.g., waiting for a specific state or using a channel) would be more robust. However, for a mock test helper, this 100ms delay may be acceptable if the goal is simply to allow initialization sequences to complete.

mock/mock.go (1)

85-100: Minor: redundant strings.Split call.

The string is split twice (lines 86 and 94). Consider reusing the words slice:

🔎 Proposed fix
 func intParams(str string, minNum int, defVal int) []int {
 	words := strings.Split(str, ";")
 	if len(words) > minNum {
 		minNum = len(words)
 	}
 	result := make([]int, minNum)
 	for i := range len(result) {
 		result[i] = defVal
 	}
-	for i, word := range strings.Split(str, ";") {
+	for i, word := range words {
 		if v, e := strconv.Atoi(word); e == nil {
 			result[i] = v
 		}
 	}
 	return result
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b0458c0 and 41d833e.

📒 Files selected for processing (2)
  • mock/mock.go (5 hunks)
  • mock/mock_test.go (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: gdamore
Repo: gdamore/tcell PR: 908
File: stdin_unix.go:128-155
Timestamp: 2025-12-18T02:50:37.669Z
Learning: In the tcell Tty interface implementations (stdin_unix.go, tty_unix.go, tty_win.go, tty_plan9.go), the caller is responsible for providing synchronization/mutual exclusion guarantees for method calls like NotifyResize. The Tty implementations themselves do not need internal locking for these methods, as documented in tty.go. The tscreen.go layer handles the synchronization.
📚 Learning: 2025-12-18T02:50:37.669Z
Learnt from: gdamore
Repo: gdamore/tcell PR: 908
File: stdin_unix.go:128-155
Timestamp: 2025-12-18T02:50:37.669Z
Learning: In the tcell Tty interface implementations (stdin_unix.go, tty_unix.go, tty_win.go, tty_plan9.go), the caller is responsible for providing synchronization/mutual exclusion guarantees for method calls like NotifyResize. The Tty implementations themselves do not need internal locking for these methods, as documented in tty.go. The tscreen.go layer handles the synchronization.

Applied to files:

  • mock/mock.go
🧬 Code graph analysis (1)
mock/mock.go (1)
attr.go (2)
  • AttrMask (22-22)
  • AttrNone (35-35)
🔇 Additional comments (4)
mock/mock_test.go (1)

15-16: Build constraints look correct.

The dual format (//go:build and // +build) ensures compatibility with both Go 1.17+ and earlier versions. Excluding js and wasm targets makes sense since these platform targets don't support the terminal operations being tested.

mock/mock.go (3)

439-456: Good goroutine lifecycle management.

The WaitGroup correctly synchronizes the run() goroutine, and the stopQ close is guarded against double-close. This aligns with the Tty interface pattern where the caller provides synchronization. Based on learnings, this design is consistent with other tcell Tty implementations.


517-533: Initialization looks correct.

The Reset() function properly initializes the state machine, escape buffer, and default terminal attributes on first call.


102-128: Movement helper functions look correct.

The bounds checking in moveUp, moveDown, moveForward, and moveBackward correctly constrains the cursor to valid positions.

Comment thread mock/mock.go
Comment thread mock/mock.go
Comment thread mock/mock.go
Comment thread mock/mock.go
Comment thread mock/mock.go
Comment thread mock/mock.go Outdated
Comment thread mock/mock.go
Comment thread mock/mock.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
mock/mock.go (2)

73-83: States stateDCS, stateSOS, statePM, stateAPC are defined but not handled.

These states are entered (lines 313-321, 382-396) but the main run() switch has no cases for them. The mock will remain stuck in these states, ignoring all subsequent input until stopped.


332-357: UTF-8 validation fixed, but wide character bounds issue remains.

Line 333 now correctly validates UTF-8 continuation bytes (fixed from previous review). However, the wide character handling at lines 351-356 still has a potential out-of-bounds access.

When a wide character is placed at the last column of the last row (mt.X == mt.Cols-1 and mt.Y == mt.Rows-1), line 352 accesses mt.Cells[ix+1], which equals mt.Rows * mt.Cols and is out of bounds.

🔎 Proposed fix
 			if uniseg.StringWidth(string(r)) > 1 && mt.X < mt.Cols {
-				mt.Cells[ix+1] = Cell{C: nil, Fg: mt.Fg, Bg: mt.Bg, Attr: mt.Attr}
-				if mt.X < mt.Cols-1 {
-					mt.X++
+				if ix+1 < len(mt.Cells) {
+					mt.Cells[ix+1] = Cell{C: nil, Fg: mt.Fg, Bg: mt.Bg, Attr: mt.Attr}
+					if mt.X < mt.Cols-1 {
+						mt.X++
+					}
 				}
 			}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 41d833e and 1cea08c.

📒 Files selected for processing (2)
  • mock/mock.go (4 hunks)
  • mock/mock_test.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • mock/mock_test.go
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: gdamore
Repo: gdamore/tcell PR: 908
File: stdin_unix.go:128-155
Timestamp: 2025-12-18T02:50:37.669Z
Learning: In the tcell Tty interface implementations (stdin_unix.go, tty_unix.go, tty_win.go, tty_plan9.go), the caller is responsible for providing synchronization/mutual exclusion guarantees for method calls like NotifyResize. The Tty implementations themselves do not need internal locking for these methods, as documented in tty.go. The tscreen.go layer handles the synchronization.
📚 Learning: 2025-12-18T02:50:37.669Z
Learnt from: gdamore
Repo: gdamore/tcell PR: 908
File: stdin_unix.go:128-155
Timestamp: 2025-12-18T02:50:37.669Z
Learning: In the tcell Tty interface implementations (stdin_unix.go, tty_unix.go, tty_win.go, tty_plan9.go), the caller is responsible for providing synchronization/mutual exclusion guarantees for method calls like NotifyResize. The Tty implementations themselves do not need internal locking for these methods, as documented in tty.go. The tscreen.go layer handles the synchronization.

Applied to files:

  • mock/mock.go
🧬 Code graph analysis (1)
mock/mock.go (1)
attr.go (2)
  • AttrMask (22-22)
  • AttrNone (35-35)
🔇 Additional comments (9)
mock/mock.go (9)

85-100: LGTM!

The intParams helper correctly parses CSI parameters with sensible defaults and handles edge cases.


102-135: LGTM!

The movement helpers correctly clamp cursor positions, and eraseCell now uses the correct index calculation (y * mt.Cols) + x (fixed from previous review).


153-177: Good fixes from previous review.

The CHA (line 175) now correctly validates against mt.Cols instead of mt.Rows. The cursor movement commands are implemented correctly.


224-245: LGTM!

The device attribute and status report handlers correctly respond with the configured attributes.


358-413: Escape sequence handling improved.

Lines 366-374 now correctly compare mt.Y against mt.Rows-1 instead of mt.Cols-1 (fixed from previous review). The ESC sequence parsing logic looks correct.


414-435: LGTM!

The CSI and OSC state handlers correctly accumulate sequence parameters and detect terminators.


453-480: LGTM!

The Read() implementation correctly follows VMIN 1, VTIME 0 semantics: blocking on the first byte, then reading available bytes without blocking, with proper stop signal handling.


495-505: Good fix from previous review.

The Drain() method now uses a select-with-default pattern to safely close stopQ without panicking if it's already closed (fixed from previous review).


515-533: LGTM!

The Reset() method properly initializes all state, including the new fields (Attr, PrimaryAttributes, SecondaryAttributes, escBuf).

Comment thread mock/mock.go
Comment on lines +184 to +205
case "J": // erase in display (ED)
switch intParams(str, 1, 0)[0] {
case 0: // erase below
for y := mt.Y + 1; y < mt.Rows; y++ {
for x := 0; x < mt.Cols; x++ {
mt.eraseCell(x, y)
}
}
case 1: // erase above
for y := 0; y < mt.Y; y++ {
for x := 0; x < mt.Cols; x++ {
mt.eraseCell(x, y)
}
}
case 2: // erase all
for y := range mt.Rows {
for x := range mt.Cols {
mt.eraseCell(x, y)
}
}
// others not supported (3 is erase saved lines)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

ED case 1 (erase above) is incomplete.

Lines 198-204 correctly fix the "erase all" case from the previous review. However, ED case 1 (erase above) at lines 192-197 only erases complete rows above the cursor line. According to the VT specification, it should also erase the current row from column 0 to the cursor position (inclusive).

🔎 Proposed fix
 	case 1: // erase above
 		for y := 0; y < mt.Y; y++ {
 			for x := 0; x < mt.Cols; x++ {
 				mt.eraseCell(x, y)
 			}
 		}
+		for x := 0; x <= mt.X; x++ {
+			mt.eraseCell(x, mt.Y)
+		}
🤖 Prompt for AI Agents
In mock/mock.go around lines 184 to 205, the ED case for parameter 1 (erase
above) currently only erases full rows above the cursor and omits clearing the
portion of the current row from column 0 through the cursor column; update that
branch to first erase columns 0..mt.X on the current row (inclusive of the
cursor position) by calling mt.eraseCell for x from 0 to mt.X, then continue to
erase all full rows y from 0 up to mt.Y-1 as already implemented.

Comment thread mock/mock.go
Comment thread mock/mock.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant