test(macos): make env/defaults helper Swift 6-safe

This commit is contained in:
Peter Steinberger
2026-01-07 20:30:57 +00:00
parent eb5f0b73a9
commit 2b6adc9e60
2 changed files with 58 additions and 46 deletions

View File

@@ -94,7 +94,7 @@ struct LowCoverageHelperTests {
} }
@Test func gatewayLaunchAgentHelpers() async throws { @Test func gatewayLaunchAgentHelpers() async throws {
try await TestIsolation.withEnvValues( await TestIsolation.withEnvValues(
[ [
"CLAWDBOT_GATEWAY_BIND": "Lan", "CLAWDBOT_GATEWAY_BIND": "Lan",
"CLAWDBOT_GATEWAY_TOKEN": " secret ", "CLAWDBOT_GATEWAY_TOKEN": " secret ",

View File

@@ -6,7 +6,7 @@ actor TestIsolationLock {
private var locked = false private var locked = false
private var waiters: [CheckedContinuation<Void, Never>] = [] private var waiters: [CheckedContinuation<Void, Never>] = []
private func lock() async { func acquire() async {
if !self.locked { if !self.locked {
self.locked = true self.locked = true
return return
@@ -17,7 +17,7 @@ actor TestIsolationLock {
// `unlock()` resumed us; lock is now held for this caller. // `unlock()` resumed us; lock is now held for this caller.
} }
private func unlock() { func release() {
if self.waiters.isEmpty { if self.waiters.isEmpty {
self.locked = false self.locked = false
return return
@@ -25,78 +25,90 @@ actor TestIsolationLock {
let next = self.waiters.removeFirst() let next = self.waiters.removeFirst()
next.resume() next.resume()
} }
func withLock<T: Sendable>(_ body: @Sendable () async throws -> T) async rethrows -> T {
await self.lock()
defer { self.unlock() }
return try await body()
}
} }
@MainActor
enum TestIsolation { enum TestIsolation {
static func withIsolatedState<T: Sendable>( static func withIsolatedState<T>(
env: [String: String?] = [:], env: [String: String?] = [:],
defaults: [String: Any?] = [:], defaults: [String: Any?] = [:],
_ body: @Sendable () async throws -> T) async rethrows -> T _ body: () async throws -> T) async rethrows -> T
{ {
try await TestIsolationLock.shared.withLock { await TestIsolationLock.shared.acquire()
var previousEnv: [String: String?] = [:] var previousEnv: [String: String?] = [:]
for (key, value) in env { for (key, value) in env {
previousEnv[key] = getenv(key).map { String(cString: $0) } previousEnv[key] = getenv(key).map { String(cString: $0) }
if let value { if let value {
setenv(key, value, 1) setenv(key, value, 1)
} else { } else {
unsetenv(key) unsetenv(key)
}
} }
}
let userDefaults = UserDefaults.standard let userDefaults = UserDefaults.standard
var previousDefaults: [String: Any?] = [:] var previousDefaults: [String: Any?] = [:]
for (key, value) in defaults { for (key, value) in defaults {
previousDefaults[key] = userDefaults.object(forKey: key) previousDefaults[key] = userDefaults.object(forKey: key)
if let value {
userDefaults.set(value, forKey: key)
} else {
userDefaults.removeObject(forKey: key)
}
}
do {
let result = try await body()
for (key, value) in previousDefaults {
if let value { if let value {
userDefaults.set(value, forKey: key) userDefaults.set(value, forKey: key)
} else { } else {
userDefaults.removeObject(forKey: key) userDefaults.removeObject(forKey: key)
} }
} }
for (key, value) in previousEnv {
defer { if let value {
for (key, value) in previousDefaults { setenv(key, value, 1)
if let value { } else {
userDefaults.set(value, forKey: key) unsetenv(key)
} else {
userDefaults.removeObject(forKey: key)
}
}
for (key, value) in previousEnv {
if let value {
setenv(key, value, 1)
} else {
unsetenv(key)
}
} }
} }
await TestIsolationLock.shared.release()
return try await body() return result
} catch {
for (key, value) in previousDefaults {
if let value {
userDefaults.set(value, forKey: key)
} else {
userDefaults.removeObject(forKey: key)
}
}
for (key, value) in previousEnv {
if let value {
setenv(key, value, 1)
} else {
unsetenv(key)
}
}
await TestIsolationLock.shared.release()
throw error
} }
} }
static func withEnvValues<T: Sendable>( static func withEnvValues<T>(
_ values: [String: String?], _ values: [String: String?],
_ body: @Sendable () async throws -> T) async rethrows -> T _ body: () async throws -> T) async rethrows -> T
{ {
try await Self.withIsolatedState(env: values, defaults: [:], body) try await Self.withIsolatedState(env: values, defaults: [:], body)
} }
static func withUserDefaultsValues<T: Sendable>( static func withUserDefaultsValues<T>(
_ values: [String: Any?], _ values: [String: Any?],
_ body: @Sendable () async throws -> T) async rethrows -> T _ body: () async throws -> T) async rethrows -> T
{ {
try await Self.withIsolatedState(env: [:], defaults: values, body) try await Self.withIsolatedState(env: [:], defaults: values, body)
} }
static func tempConfigPath() -> String { nonisolated static func tempConfigPath() -> String {
FileManager.default.temporaryDirectory FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-test-config-\(UUID().uuidString).json") .appendingPathComponent("clawdbot-test-config-\(UUID().uuidString).json")
.path .path