mirror of
https://github.com/phranck/TUIkit.git
synced 2026-05-21 09:50:35 +00:00
4493126758
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.
152 lines
4.0 KiB
Swift
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"))
|
||
}
|
||
}
|