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
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 🖥 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"))
}
}