Files
TUIkit/Tests/TUIkitTests/TerminalOutputBufferTests.swift
T
phranck 4493126758 Refactor: Swift 6 concurrency improvements and ContainerView architecture
Phase 1: Thread-Safe Primitives
- AppState now uses OSAllocatedUnfairLock for thread-safe state management
- SignalManager flags remain nonisolated(unsafe) with documentation explaining
  why (signal handlers cannot use locks safely)

Phase 2: @MainActor Isolation (Core Types)
- Terminal, FrameDiffWriter, RenderLoop, AppRunner, ViewRenderer: @MainActor
- App.main() uses MainActor.assumeIsolated for synchronous entry point
- renderOnce() function: @MainActor
- Tests updated with @MainActor annotations

ContainerView Refactoring
- Changed from body: Never + Renderable to body: some View
- Extracted rendering logic to private _ContainerViewCore
- Enables modifiers to work correctly on ContainerView

All 591 tests pass, SwiftLint clean.
2026-02-07 02:50:41 +01:00

152 lines
4.0 KiB
Swift

// 🖥️ TUIKit — Terminal UI Kit for Swift
// TerminalOutputBufferTests.swift
//
// Created by LAYERED.work
// License: MIT
import Testing
#if canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#elseif canImport(Darwin)
import Darwin
#endif
import Foundation
@testable import TUIkit
// MARK: - Output Capture Helper
/// Captures all bytes written to `STDOUT_FILENO` during a closure.
///
/// Redirects stdout to a pipe, runs the closure, restores stdout,
/// and returns the captured bytes as a UTF-8 string.
@MainActor
private func captureStdout(_ body: () -> Void) -> String {
var pipeFDs: [Int32] = [0, 0]
pipe(&pipeFDs)
let savedStdout = dup(STDOUT_FILENO)
dup2(pipeFDs[1], STDOUT_FILENO)
body()
// Flush and close write end so read doesn't block
close(pipeFDs[1])
dup2(savedStdout, STDOUT_FILENO)
close(savedStdout)
// Read captured output
var data = Data()
var readBuffer = [UInt8](repeating: 0, count: 4096)
while true {
let bytesRead = read(pipeFDs[0], &readBuffer, readBuffer.count)
if bytesRead <= 0 { break }
data.append(contentsOf: readBuffer.prefix(bytesRead))
}
close(pipeFDs[0])
return String(data: data, encoding: .utf8) ?? ""
}
// MARK: - Frame Buffer Tests
@Suite("Terminal Output Buffer Tests", .serialized)
@MainActor
struct TerminalOutputBufferTests {
@Test("Buffered writes produce same output as unbuffered writes")
func bufferedMatchesUnbuffered() {
let unbuffered = captureStdout {
let terminal = Terminal()
terminal.write("Hello")
terminal.write(" World")
}
let buffered = captureStdout {
let terminal = Terminal()
terminal.beginFrame()
terminal.write("Hello")
terminal.write(" World")
terminal.endFrame()
}
#expect(unbuffered == "Hello World")
#expect(buffered == "Hello World")
}
@Test("endFrame without beginFrame is a no-op")
func endFrameWithoutBeginIsNoop() {
let output = captureStdout {
let terminal = Terminal()
terminal.endFrame()
terminal.write("After")
}
#expect(output == "After")
}
@Test("Double beginFrame is a no-op — buffer is not reset")
func doubleBeginFrameIsNoop() {
let output = captureStdout {
let terminal = Terminal()
terminal.beginFrame()
terminal.write("First")
terminal.beginFrame() // should not reset buffer
terminal.write("Second")
terminal.endFrame()
}
#expect(output == "FirstSecond")
}
@Test("Sequential frames on same terminal flush independently")
func sequentialFramesFlushIndependently() {
let output = captureStdout {
let terminal = Terminal()
terminal.beginFrame()
terminal.write("Frame1")
terminal.endFrame()
terminal.beginFrame()
terminal.write("Frame2")
terminal.endFrame()
}
#expect(output == "Frame1Frame2")
}
@Test("Empty frame produces no output")
func emptyFrameProducesNoOutput() {
let output = captureStdout {
let terminal = Terminal()
terminal.beginFrame()
terminal.endFrame()
}
#expect(output.isEmpty)
}
@Test("Buffered frame handles ANSI escape sequences correctly")
func bufferedFrameHandlesANSI() {
let cursorMove = ANSIRenderer.moveCursor(toRow: 5, column: 1)
var style = TextStyle()
style.isBold = true
let styled = ANSIRenderer.render("Bold", with: style)
let output = captureStdout {
let terminal = Terminal()
terminal.beginFrame()
terminal.write(cursorMove)
terminal.write(styled)
terminal.endFrame()
}
#expect(output.contains("\u{1B}[5;1H"))
#expect(output.contains("\u{1B}[1m"))
#expect(output.contains("Bold"))
}
}