- M0: libghostty SSH 终端(渲染/输入/连接)+ 白屏修复(OutputGate) + 会话状态机/自动重连 - M1: tsnet 用户态组网 + SSH-over-tsnet(fd 桥),shell 级真机验证;R5(Go+gvisor+C+++Swift 同进程) retire - M1.5: tmux -CC 原生 tab(MVP) - 结构: packages/(TXCore·TXTransport), apps/TerminalX, vendor/(libghostty-spm/libssh2/mbedtls/tsnet-bridge), artifacts/ - 文档: CLAUDE.md + docs/HANDOFF.md(新会话入口) - 环境: 认证代理→依赖 vendor 本地化;Go 在 ~/.local/go;仅模拟器/未签名 - 待续: M2 mosh, tmux 多 pane, M4 安全(host key/SE) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
159 lines
5.0 KiB
Swift
159 lines
5.0 KiB
Swift
@testable import GhosttyTerminal
|
|
import Darwin
|
|
import Foundation
|
|
import GhosttyKit
|
|
import Testing
|
|
|
|
struct InMemoryTerminalSessionOutputQueueTests {
|
|
@Test
|
|
func `receive returns before surface write completes`() {
|
|
let writeStarted = DispatchSemaphore(value: 0)
|
|
let allowWriteToFinish = DispatchSemaphore(value: 0)
|
|
let session = makeSession { _, _ in
|
|
writeStarted.signal()
|
|
allowWriteToFinish.wait()
|
|
}
|
|
session.setSurface(testSurface(1))
|
|
|
|
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
|
|
allowWriteToFinish.signal()
|
|
}
|
|
|
|
let start = ProcessInfo.processInfo.systemUptime
|
|
session.receive(Data("hello".utf8))
|
|
let elapsed = ProcessInfo.processInfo.systemUptime - start
|
|
|
|
#expect(elapsed < 0.2)
|
|
#expect(writeStarted.wait(timeout: .now() + 1) == .success)
|
|
allowWriteToFinish.signal()
|
|
session.waitForPendingOutput()
|
|
}
|
|
|
|
@Test
|
|
func `writes and process exit preserve enqueue order`() {
|
|
let events = LockedValues<String>()
|
|
let session = InMemoryTerminalSession(
|
|
write: { _ in },
|
|
resize: { _ in },
|
|
surfaceWrite: { _, data in
|
|
events.append(String(decoding: data, as: UTF8.self))
|
|
},
|
|
processExit: { _, exitCode, runtimeMilliseconds in
|
|
events.append("exit:\(exitCode):\(runtimeMilliseconds)")
|
|
}
|
|
)
|
|
session.setSurface(testSurface(2))
|
|
|
|
session.receive("first")
|
|
session.receive("second")
|
|
session.finish(exitCode: 7, runtimeMilliseconds: 42)
|
|
session.waitForPendingOutput()
|
|
|
|
#expect(events.values == ["first", "second", "exit:7:42"])
|
|
}
|
|
|
|
@Test
|
|
func `surface teardown waits for active write and drops queued stale writes`() {
|
|
let firstWriteStarted = DispatchSemaphore(value: 0)
|
|
let allowFirstWriteToFinish = DispatchSemaphore(value: 0)
|
|
let clearFinished = DispatchSemaphore(value: 0)
|
|
let writes = LockedValues<String>()
|
|
let surface = SendableSurface(testSurface(3))
|
|
let session = makeSession { _, data in
|
|
let value = String(decoding: data, as: UTF8.self)
|
|
writes.append(value)
|
|
if value == "first" {
|
|
firstWriteStarted.signal()
|
|
allowFirstWriteToFinish.wait()
|
|
}
|
|
}
|
|
session.setSurface(surface.rawValue)
|
|
session.receive("first")
|
|
session.receive("stale")
|
|
#expect(firstWriteStarted.wait(timeout: .now() + 1) == .success)
|
|
|
|
DispatchQueue.global().async {
|
|
session.clearSurface(ifMatches: surface.rawValue)
|
|
clearFinished.signal()
|
|
}
|
|
|
|
let clearDeadline = ProcessInfo.processInfo.systemUptime + 1
|
|
while session.currentSurface != nil,
|
|
ProcessInfo.processInfo.systemUptime < clearDeadline
|
|
{
|
|
sched_yield()
|
|
}
|
|
#expect(session.currentSurface == nil)
|
|
|
|
allowFirstWriteToFinish.signal()
|
|
#expect(clearFinished.wait(timeout: .now() + 1) == .success)
|
|
session.waitForPendingOutput()
|
|
|
|
#expect(writes.values == ["first"])
|
|
}
|
|
|
|
@Test
|
|
func `blocked session does not block another session`() {
|
|
let firstWriteStarted = DispatchSemaphore(value: 0)
|
|
let allowFirstWriteToFinish = DispatchSemaphore(value: 0)
|
|
let secondWriteFinished = DispatchSemaphore(value: 0)
|
|
let firstSession = makeSession { _, _ in
|
|
firstWriteStarted.signal()
|
|
allowFirstWriteToFinish.wait()
|
|
}
|
|
let secondSession = makeSession { _, _ in
|
|
secondWriteFinished.signal()
|
|
}
|
|
firstSession.setSurface(testSurface(4))
|
|
secondSession.setSurface(testSurface(5))
|
|
|
|
firstSession.receive("blocked")
|
|
#expect(firstWriteStarted.wait(timeout: .now() + 1) == .success)
|
|
secondSession.receive("independent")
|
|
|
|
#expect(secondWriteFinished.wait(timeout: .now() + 1) == .success)
|
|
allowFirstWriteToFinish.signal()
|
|
firstSession.waitForPendingOutput()
|
|
secondSession.waitForPendingOutput()
|
|
}
|
|
}
|
|
|
|
private func makeSession(
|
|
surfaceWrite: @escaping InMemoryTerminalSurfaceAccess.Write
|
|
) -> InMemoryTerminalSession {
|
|
InMemoryTerminalSession(
|
|
write: { _ in },
|
|
resize: { _ in },
|
|
surfaceWrite: surfaceWrite
|
|
)
|
|
}
|
|
|
|
private func testSurface(_ address: Int) -> ghostty_surface_t {
|
|
UnsafeMutableRawPointer(bitPattern: address)!
|
|
}
|
|
|
|
private struct SendableSurface: @unchecked Sendable {
|
|
let rawValue: ghostty_surface_t
|
|
|
|
init(_ rawValue: ghostty_surface_t) {
|
|
self.rawValue = rawValue
|
|
}
|
|
}
|
|
|
|
private final class LockedValues<Value: Sendable>: @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var storage: [Value] = []
|
|
|
|
var values: [Value] {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
return storage
|
|
}
|
|
|
|
func append(_ value: Value) {
|
|
lock.lock()
|
|
storage.append(value)
|
|
lock.unlock()
|
|
}
|
|
}
|