Initial release

This commit is contained in:
aculix committed 2026-09-28 18:28:32 +05:30
commit 4b67728cce
108 files changed
+13310

No files matched your search

+336
View File
@@ -0,0 +1,336 @@
import Foundation
import Synchronization
import Testing
@testable import KnotchyCore
/// A forgotten `release` fails the test after this instead of hanging the run.
private let heldCallLimit: Duration = .seconds(10)
final class FakeVMBackend: VMBackend {
enum Call: Equatable, Sendable {
case sweep(keeping: Set<String>)
case preflight
case start(String)
case destroy(String)
case forceDestroy(String)
case fix(SetupIssue)
case destroyBlocking([String])
case stopService
var kind: Kind {
switch self {
case .sweep: .sweep
case .preflight: .preflight
case .start: .start
case .destroy: .destroy
case .forceDestroy: .forceDestroy
case .fix: .fix
case .destroyBlocking: .destroyBlocking
case .stopService: .stopService
}
}
}
enum Kind: Hashable, Sendable {
case sweep, preflight, start, destroy, forceDestroy, fix, destroyBlocking, stopService
}
struct StartArguments: Equatable, Sendable {
var id: String
var url: URL
var renderSize: PixelSize
var blockCIDRs: [String]
}
private struct State {
var calls: [Call] = []
var startArguments: [StartArguments] = []
var active = 0
var maxActive = 0
var held: Set<Kind> = []
var nextParkID = 0
var parked: [Kind: [(id: Int, continuation: CheckedContinuation<Void, Never>)]] = [:]
var preflightResults: [PreflightResult] = []
var startError: (any Error)?
var destroyError: (any Error)?
var forceDestroyError: (any Error)?
var fixOutput: [String] = []
var fixError: (any Error)?
var runtimeStatus: RuntimeStatus = .current("1.4.1")
var stopError: (any Error)?
}
private let state = Mutex(State())
var calls: [Call] { state.withLock { $0.calls } }
var startArguments: [StartArguments] { state.withLock { $0.startArguments } }
var maxConcurrentCalls: Int { state.withLock { $0.maxActive } }
/// Used in order; after them, preflight is ready.
func queuePreflight(_ results: PreflightResult...) {
state.withLock { $0.preflightResults += results }
}
func failStarts(with error: (any Error)?) { state.withLock { $0.startError = error } }
func failDestroys(with error: (any Error)?) { state.withLock { $0.destroyError = error } }
func failForceDestroys(with error: (any Error)?) { state.withLock { $0.forceDestroyError = error } }
func scriptFix(output: [String], error: (any Error)? = nil) {
state.withLock {
$0.fixOutput = output
$0.fixError = error
}
}
func hold(_ kind: Kind) {
precondition(kind != .destroyBlocking, "destroyBlocking is synchronous and can't be held")
_ = state.withLock { $0.held.insert(kind) }
}
func release(_ kind: Kind) {
let parked = state.withLock { s in
s.held.remove(kind)
return s.parked.removeValue(forKey: kind) ?? []
}
for call in parked { call.continuation.resume() }
}
func parkedCount(_ kind: Kind) -> Int {
state.withLock { $0.parked[kind]?.count ?? 0 }
}
func sweepOrphans(keeping: Set<String>) async {
await begin(.sweep(keeping: keeping))
end()
}
func preflight() async -> PreflightResult {
await begin(.preflight)
defer { end() }
return state.withLock { s in
s.preflightResults.isEmpty ? .ready(appRoot: "/fake/appRoot", staleImage: nil) : s.preflightResults.removeFirst()
}
}
func start(id: String, url: URL, renderSize: PixelSize, blockCIDRs: [String]) async throws -> VMSession {
state.withLock {
$0.startArguments.append(StartArguments(id: id, url: url, renderSize: renderSize, blockCIDRs: blockCIDRs))
}
await begin(.start(id))
defer { end() }
if let error = state.withLock({ $0.startError }) { throw error }
return VMSession(id: id, socketPath: "/fake/\(id).sock")
}
func destroy(id: String) async throws {
await begin(.destroy(id))
defer { end() }
if let error = state.withLock({ $0.destroyError }) { throw error }
}
func forceDestroy(id: String) async throws {
await begin(.forceDestroy(id))
defer { end() }
if let error = state.withLock({ $0.forceDestroyError }) { throw error }
}
func fix(_ issue: SetupIssue, output: @escaping @Sendable (String) -> Void) async throws {
record(.fix(issue))
state.withLock { $0.fixOutput }.forEach(output)
await parkIfHeld(.fix)
defer { end() }
if let error = state.withLock({ $0.fixError }) { throw error }
}
func destroyBlocking(ids: [String], deadline: Duration) {
state.withLock { $0.calls.append(.destroyBlocking(ids)) }
}
func setRuntimeStatus(_ status: RuntimeStatus) { state.withLock { $0.runtimeStatus = status } }
func failStopService(with error: (any Error)?) { state.withLock { $0.stopError = error } }
func runtimeStatus() async -> RuntimeStatus {
state.withLock { $0.runtimeStatus }
}
func stopService() async throws {
await begin(.stopService)
defer { end() }
if let error = state.withLock({ $0.stopError }) { throw error }
}
private func begin(_ call: Call) async {
record(call)
await parkIfHeld(call.kind)
}
private func record(_ call: Call) {
state.withLock { s in
s.calls.append(call)
s.active += 1
s.maxActive = max(s.maxActive, s.active)
}
}
private func end() {
state.withLock { $0.active -= 1 }
}
private func parkIfHeld(_ kind: Kind) async {
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
let id: Int? = state.withLock { s in
guard s.held.contains(kind) else { return nil }
s.nextParkID += 1
s.parked[kind, default: []].append((s.nextParkID, continuation))
return s.nextParkID
}
guard let id else { return continuation.resume() }
Task {
try? await Task.sleep(for: heldCallLimit)
let forgotten = self.state.withLock { s -> CheckedContinuation<Void, Never>? in
guard let index = s.parked[kind]?.firstIndex(where: { $0.id == id }) else { return nil }
return s.parked[kind]?.remove(at: index).continuation
}
if let forgotten {
Issue.record("A held \(kind) call was never released")
forgotten.resume()
}
}
}
}
}
/// Queued errors go to connects in call order; `connections` is in completion order.
final class FakeStreamConnector: StreamConnector {
private struct State {
var socketPaths: [String] = []
var connections: [FakeStreamConnection] = []
var errors: [StreamError] = []
var held = false
var nextParkID = 0
var parked: [(id: Int, continuation: CheckedContinuation<Void, Never>)] = []
}
private let state = Mutex(State())
var socketPaths: [String] { state.withLock { $0.socketPaths } }
var connections: [FakeStreamConnection] { state.withLock { $0.connections } }
var parkedCount: Int { state.withLock { $0.parked.count } }
func failNextConnect(with error: StreamError) {
state.withLock { $0.errors.append(error) }
}
func hold() {
state.withLock { $0.held = true }
}
func release() {
let parked = state.withLock { s in
s.held = false
defer { s.parked = [] }
return s.parked
}
for call in parked { call.continuation.resume() }
}
func connect(socketPath: String) async throws -> any StreamConnection {
let scriptedError = state.withLock { s -> StreamError? in
s.socketPaths.append(socketPath)
return s.errors.isEmpty ? nil : s.errors.removeFirst()
}
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
let id: Int? = state.withLock { s in
guard s.held else { return nil }
s.nextParkID += 1
s.parked.append((s.nextParkID, continuation))
return s.nextParkID
}
guard let id else { return continuation.resume() }
Task {
try? await Task.sleep(for: heldCallLimit)
let forgotten = self.state.withLock { s -> CheckedContinuation<Void, Never>? in
guard let index = s.parked.firstIndex(where: { $0.id == id }) else { return nil }
return s.parked.remove(at: index).continuation
}
if let forgotten {
Issue.record("A held connect was never released")
forgotten.resume()
}
}
}
if let scriptedError { throw scriptedError }
let connection = FakeStreamConnection(socketPath: socketPath)
state.withLock { $0.connections.append(connection) }
return connection
}
}
final class FakeStreamConnection: StreamConnection {
let socketPath: String
let events: AsyncStream<StreamEvent>
private let continuation: AsyncStream<StreamEvent>.Continuation
private struct State {
var decodeEnabled = true
var decodeChanges: [Bool] = []
var sent: [InputEvent] = []
var closed = false
}
private let state = Mutex(State())
init(socketPath: String) {
self.socketPath = socketPath
let (stream, continuation) = AsyncStream.makeStream(of: StreamEvent.self)
events = stream
self.continuation = continuation
}
var decodeEnabled: Bool {
get { state.withLock { $0.decodeEnabled } }
set {
state.withLock {
$0.decodeEnabled = newValue
$0.decodeChanges.append(newValue)
}
}
}
var decodeChanges: [Bool] { state.withLock { $0.decodeChanges } }
var sent: [InputEvent] { state.withLock { $0.sent } }
var isClosed: Bool { state.withLock { $0.closed } }
func setFrameHandler(_ handler: @escaping @Sendable (DecodedFrame) -> Void) {}
func send(_ input: InputEvent) {
state.withLock { $0.sent.append(input) }
}
func close() {
state.withLock { $0.closed = true }
continuation.finish()
}
func emit(_ event: StreamEvent) {
continuation.yield(event)
}
/// Without an `.ended` event, as a buggy client might.
func finishEvents() {
continuation.finish()
}
}
final class FakeDiskSpace: DiskSpaceChecking {
private let free = Mutex<Int64?>(100_000_000_000)
var available: Int64? {
get { free.withLock { $0 } }
set { free.withLock { $0 = newValue } }
}
func bytesFree(atPath path: String) -> Int64? {
free.withLock { $0 }
}
}
@@ -0,0 +1,87 @@
import Foundation
import Testing
@testable import KnotchyCore
@MainActor
final class Recorder {
var quitRequests = 0
var setupRequests = 0
}
@MainActor
final class Harness {
static let url = URL(string: "https://example.com/")!
static let otherURL = URL(string: "https://example.org/")!
let backend = FakeVMBackend()
let connector = FakeStreamConnector()
let disk = FakeDiskSpace()
let clock = TestClock()
let recorder: Recorder
let controller: SessionController
init(hostNetworks: [String] = ["192.168.1.0/24"]) {
let recorder = Recorder()
self.recorder = recorder
var sessions = 0
controller = SessionController(
backend: backend,
connector: connector,
diskSpace: disk,
clock: clock,
hostNetworks: { hostNetworks },
requestQuit: { recorder.quitRequests += 1 },
makeSessionID: {
sessions += 1
return String(format: "knotchy-%08x", sessions)
}
)
controller.showSetup = { recorder.setupRequests += 1 }
}
/// Lets go of held calls, so the fakes' 10 s check can't fire later against another test.
deinit {
for kind in [FakeVMBackend.Kind.sweep, .preflight, .start, .destroy, .forceDestroy, .fix, .stopService] {
backend.release(kind)
}
connector.release()
for stream in connector.connections { stream.close() }
}
var state: SessionState { controller.state }
func advance(by duration: Duration) {
clock.advance(by: duration)
controller.fireDueTimers()
}
@discardableResult
func openAndConnect(_ url: URL = Harness.url) async -> FakeStreamConnection {
let before = connector.connections.count
controller.open(url)
await waitUntil("the stream connects") {
self.connector.connections.count > before && self.state == .starting(.loading, folded: false)
}
return connector.connections.last!
}
func enter(_ mode: LiveMode) {
switch mode {
case .peek:
break
case .expanded:
controller.pointerEntered()
controller.expand()
case .collapsed:
controller.clickedOutside()
}
}
@discardableResult
func goLive(_ url: URL = Harness.url) async -> FakeStreamConnection {
let stream = await openAndConnect(url)
stream.emit(.contentReady)
await waitUntil("the session is live") { self.state == .live(.peek) }
return stream
}
}
@@ -0,0 +1,117 @@
import Foundation
import Synchronization
@testable import KnotchyCore
final class FakeHost: HostChecking {
private let value = Mutex(HostFacts(unsupported: nil, osVersion: "macOS 27.0", bytesFree: 212_000_000_000))
var current: HostFacts {
get { value.withLock { $0 } }
set { value.withLock { $0 = newValue } }
}
func facts() async -> HostFacts { current }
}
@MainActor
final class FakeInstallerSession: InstallerSession {
var hasQuit: Bool
nonisolated init(hasQuit: Bool = true) {
self.hasQuit = hasQuit
}
}
final class FakeInstaller: RuntimeInstalling {
enum Opening: Sendable {
case opened
case changed
case failed(String)
}
private struct State {
var downloadError: DownloadError?
var opening: Opening = .opened
var calls: [String] = []
}
private let state = Mutex(State())
/// What `.opened` hands out; it has already quit unless a test says otherwise.
let session = FakeInstallerSession()
var calls: [String] { state.withLock { $0.calls } }
func failDownload(with error: DownloadError) { state.withLock { $0.downloadError = error } }
func open(with opening: Opening) { state.withLock { $0.opening = opening } }
func download(_ release: ContainerRuntime.Release,
progress: @escaping @Sendable (Int64) -> Void) async throws -> DownloadedPackage {
let error = state.withLock { s -> DownloadError? in
s.calls.append("download")
return s.downloadError
}
if let error { throw error }
return DownloadedPackage(directory: URL(fileURLWithPath: "/fake/knotchy-runtime"), name: release.url.lastPathComponent)
}
@MainActor
func openInInstaller(_ package: DownloadedPackage, release: ContainerRuntime.Release) async -> InstallerOpening {
let opening = state.withLock { s -> Opening in
s.calls.append("open")
return s.opening
}
switch opening {
case .opened: return .opened(session)
case .changed: return .changed
case .failed(let message): return .failed(message)
}
}
func discard(_ package: DownloadedPackage) {
state.withLock { $0.calls.append("discard") }
}
}
@MainActor
final class FakeSetupDriver: SetupDriving {
var state: SessionState = .idle
var setupLog: String?
var setupOutput: [String] = []
/// Used in order; the last one repeats.
var runtimeStatuses: [RuntimeStatus] = [.current("1.4.1")]
/// Used in order; then `.ready`.
var checkResults: [SetupRunOutcome?] = []
var runResults: [SetupRunOutcome?] = []
var holdResult: Result<Void, RuntimeHoldRefusal> = .success(())
var finishResult: SetupRunOutcome = .ready
private(set) var calls: [String] = []
func runtimeStatus() async -> RuntimeStatus {
calls.append("runtimeStatus")
return runtimeStatuses.count > 1 ? runtimeStatuses.removeFirst() : runtimeStatuses[0]
}
func checkSetup() async -> SetupRunOutcome? {
calls.append("checkSetup")
return checkResults.isEmpty ? .ready : checkResults.removeFirst()
}
func runSetup(_ issue: SetupIssue, continuing: Bool) async -> SetupRunOutcome? {
calls.append("runSetup(\(issue), continuing: \(continuing))")
return runResults.isEmpty ? .ready : runResults.removeFirst()
}
func beginRuntimeInstall(replacing found: String?) async -> Result<Void, RuntimeHoldRefusal> {
calls.append("begin(\(found ?? "nil"))")
return holdResult
}
func finishRuntimeInstall(_ result: RuntimeInstallResult) async -> SetupRunOutcome {
calls.append("finish(\(result))")
return finishResult
}
func waitWhileSettingUp() async {
calls.append("wait")
state = .idle
}
}
@@ -0,0 +1,66 @@
import Synchronization
@testable import KnotchyCore
final class TestClock: KnotchyClock {
private struct Sleeper {
let id: Int
let deadline: ContinuousClock.Instant
let continuation: CheckedContinuation<Void, any Error>
}
private struct State {
var now: ContinuousClock.Instant
var nextID = 0
var sleepers: [Sleeper] = []
}
private let state: Mutex<State>
init(now: ContinuousClock.Instant = .now) {
state = Mutex(State(now: now))
}
var now: ContinuousClock.Instant {
state.withLock { $0.now }
}
var sleeperCount: Int {
state.withLock { $0.sleepers.count }
}
func sleep(until deadline: ContinuousClock.Instant) async throws {
let id = state.withLock { s in
s.nextID += 1
return s.nextID
}
try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
let outcome: Result<Void, any Error>? = state.withLock { s in
if Task.isCancelled { return .failure(CancellationError()) }
if deadline <= s.now { return .success(()) }
s.sleepers.append(Sleeper(id: id, deadline: deadline, continuation: continuation))
return nil
}
if let outcome { continuation.resume(with: outcome) }
}
} onCancel: {
let sleeper = state.withLock { s -> Sleeper? in
guard let index = s.sleepers.firstIndex(where: { $0.id == id }) else { return nil }
return s.sleepers.remove(at: index)
}
sleeper?.continuation.resume(throwing: CancellationError())
}
}
/// The woken tasks run concurrently, in no particular order.
func advance(by duration: Duration) {
let due = state.withLock { s -> [Sleeper] in
s.now += duration
let now = s.now
let due = s.sleepers.filter { $0.deadline <= now }.sorted { $0.deadline < $1.deadline }
s.sleepers.removeAll { $0.deadline <= now }
return due
}
for sleeper in due { sleeper.continuation.resume() }
}
}
@@ -0,0 +1,19 @@
import Testing
/// For work on other executors; controller timers run on the `TestClock` and never need it.
@MainActor
func waitUntil(
_ description: String,
timeout: Duration = .seconds(5),
sourceLocation: SourceLocation = #_sourceLocation,
_ condition: @MainActor () -> Bool
) async {
let deadline = ContinuousClock.now + timeout
while !condition() {
if ContinuousClock.now >= deadline {
Issue.record("Timed out waiting until \(description)", sourceLocation: sourceLocation)
return
}
do { try await Task.sleep(for: .milliseconds(1)) } catch { return }
}
}