mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
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"))
|
|
}
|
|
}
|