From 30ccbf0bfc2df9d4a951b60a746954281582dc50 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Fri, 4 Apr 2025 18:07:58 +1100 Subject: [PATCH 1/8] chore: add file sync daemon tests --- .../Coder-Desktop/Coder_DesktopApp.swift | 6 +- .../Preview Content/PreviewFileSync.swift | 2 +- .../Views/FileSync/FileSyncConfig.swift | 4 - .../Views/FileSync/FileSyncSessionModal.swift | 7 +- .../FileSyncDaemonTests.swift | 165 ++++++++++++++++++ Coder-Desktop/Coder-DesktopTests/Util.swift | 17 +- .../VPNLib/FileSync/FileSyncDaemon.swift | 23 +-- .../VPNLib/FileSync/FileSyncManagement.swift | 86 ++++++--- Coder-Desktop/project.yml | 4 +- 9 files changed, 262 insertions(+), 52 deletions(-) create mode 100644 Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift index a110432..30ea7e7 100644 --- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift +++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift @@ -51,9 +51,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { #elseif arch(x86_64) let mutagenBinary = "mutagen-darwin-amd64" #endif - fileSyncDaemon = MutagenDaemon( + let fileSyncDaemon = MutagenDaemon( mutagenPath: Bundle.main.url(forResource: mutagenBinary, withExtension: nil) ) + Task { + await fileSyncDaemon.tryStart() + } + self.fileSyncDaemon = fileSyncDaemon } func applicationDidFinishLaunching(_: Notification) { diff --git a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift index 4559716..1253e42 100644 --- a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift +++ b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift @@ -20,7 +20,7 @@ final class PreviewFileSync: FileSyncDaemon { state = .stopped } - func createSession(localPath _: String, agentHost _: String, remotePath _: String) async throws(DaemonError) {} + func createSession(arg _: CreateSyncSessionRequest) async throws(DaemonError) {} func deleteSessions(ids _: [String]) async throws(VPNLib.DaemonError) {} diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift index dc946c8..7400635 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift @@ -166,10 +166,6 @@ struct FileSyncConfig: View { defer { loading = false } do throws(DaemonError) { try await fileSync.deleteSessions(ids: [selection!]) - if fileSync.sessionState.isEmpty { - // Last session was deleted, stop the daemon - await fileSync.stop() - } } catch { actionError = error } diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift index 7b902f2..e3af4d0 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift @@ -100,9 +100,10 @@ struct FileSyncSessionModal: View { try await fileSync.deleteSessions(ids: [existingSession.id]) } try await fileSync.createSession( - localPath: localPath, - agentHost: remoteHostname, - remotePath: remotePath + arg: .init( + alpha: .init(path: localPath, protocolKind: .local), + beta: .init(path: remotePath, protocolKind: .ssh(host: workspace.primaryHost!)) + ) ) } catch { createError = error diff --git a/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift b/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift new file mode 100644 index 0000000..9d007eb --- /dev/null +++ b/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift @@ -0,0 +1,165 @@ +@testable import Coder_Desktop +import Foundation +import GRPC +import NIO +import Subprocess +import Testing +import VPNLib +import XCTest + +@MainActor +@Suite(.timeLimit(.minutes(1))) +class FileSyncDaemonTests { + let tempDir: URL + let mutagenBinary: URL + let mutagenDataDirectory: URL + let mutagenAlphaDirectory: URL + let mutagenBetaDirectory: URL + + init() throws { + tempDir = FileManager.default.makeTempDir()! + #if arch(arm64) + let binaryName = "mutagen-darwin-arm64" + #elseif arch(x86_64) + let binaryName = "mutagen-darwin-amd64" + #endif + mutagenBinary = Bundle.main.url(forResource: binaryName, withExtension: nil)! + mutagenDataDirectory = tempDir.appending(path: "mutagen") + mutagenAlphaDirectory = tempDir.appending(path: "alpha") + try FileManager.default.createDirectory(at: mutagenAlphaDirectory, withIntermediateDirectories: true) + mutagenBetaDirectory = tempDir.appending(path: "beta") + try FileManager.default.createDirectory(at: mutagenBetaDirectory, withIntermediateDirectories: true) + } + + deinit { + try? FileManager.default.removeItem(at: tempDir) + } + + private func statesEqual(_ first: DaemonState, _ second: DaemonState) -> Bool { + switch (first, second) { + case (.stopped, .stopped): + true + case (.running, .running): + true + case (.unavailable, .unavailable): + true + default: + false + } + } + + @Test + func fullSync() async throws { + let daemon = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory) + #expect(statesEqual(daemon.state, .stopped)) + #expect(daemon.sessionState.count == 0) + + // The daemon won't start until we create a session + await daemon.tryStart() + #expect(statesEqual(daemon.state, .stopped)) + #expect(daemon.sessionState.count == 0) + + try await daemon.createSession( + arg: .init( + alpha: .init( + path: mutagenAlphaDirectory.path(), + protocolKind: .local + ), + beta: .init( + path: mutagenBetaDirectory.path(), + protocolKind: .local + ) + ) + ) + + // Daemon should have started itself + #expect(statesEqual(daemon.state, .running)) + #expect(daemon.sessionState.count == 1) + + // Write a file to Alpha + let alphaFile = mutagenAlphaDirectory.appendingPathComponent("test.txt") + try "Hello, World!".write(to: alphaFile, atomically: true, encoding: .utf8) + try #expect( + await eventually(timeout: .seconds(5), interval: .milliseconds(100)) { @MainActor in + return try FileManager.default.fileExists( + atPath: self.mutagenBetaDirectory.appending(path: "test.txt").path() + ) + }) + + try await daemon.deleteSessions(ids: daemon.sessionState.map(\.id)) + #expect(daemon.sessionState.count == 0) + // Daemon should have stopped itself once all sessions are deleted + #expect(statesEqual(daemon.state, .stopped)) + } + + @Test + func autoStopStart() async throws { + let daemon = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory) + #expect(statesEqual(daemon.state, .stopped)) + #expect(daemon.sessionState.count == 0) + + try await daemon.createSession( + arg: .init( + alpha: .init( + path: mutagenAlphaDirectory.path(), + protocolKind: .local + ), + beta: .init( + path: mutagenBetaDirectory.path(), + protocolKind: .local + ) + ) + ) + + try await daemon.createSession( + arg: .init( + alpha: .init( + path: mutagenAlphaDirectory.path(), + protocolKind: .local + ), + beta: .init( + path: mutagenBetaDirectory.path(), + protocolKind: .local + ) + ) + ) + + #expect(statesEqual(daemon.state, .running)) + #expect(daemon.sessionState.count == 2) + + try await daemon.deleteSessions(ids: [daemon.sessionState[0].id]) + #expect(daemon.sessionState.count == 1) + #expect(statesEqual(daemon.state, .running)) + + try await daemon.deleteSessions(ids: [daemon.sessionState[0].id]) + #expect(daemon.sessionState.count == 0) + #expect(statesEqual(daemon.state, .stopped)) + } + + @Test + func orphaned() async throws { + let daemon1 = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory) + await daemon1.refreshSessions() + try await daemon1.createSession(arg: + .init( + alpha: .init( + path: mutagenAlphaDirectory.path(), + protocolKind: .local + ), + beta: .init( + path: mutagenBetaDirectory.path(), + protocolKind: .local + ) + ) + ) + #expect(statesEqual(daemon1.state, .running)) + #expect(daemon1.sessionState.count == 1) + + let daemon2 = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory) + await daemon2.tryStart() + #expect(statesEqual(daemon2.state, .running)) + + // Daemon 2 should have killed daemon 1, causing it to fail + #expect(daemon1.state.isFailed) + } +} diff --git a/Coder-Desktop/Coder-DesktopTests/Util.swift b/Coder-Desktop/Coder-DesktopTests/Util.swift index 249aa10..52e6de1 100644 --- a/Coder-Desktop/Coder-DesktopTests/Util.swift +++ b/Coder-Desktop/Coder-DesktopTests/Util.swift @@ -47,7 +47,7 @@ class MockFileSyncDaemon: FileSyncDaemon { [] } - func createSession(localPath _: String, agentHost _: String, remotePath _: String) async throws(DaemonError) {} + func createSession(arg _: CreateSyncSessionRequest) async throws(DaemonError) {} func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {} @@ -82,3 +82,18 @@ public func eventually( } return false } + +extension FileManager { + func makeTempDir() -> URL? { + let tempDirectory = FileManager.default.temporaryDirectory + let directoryName = String(Int.random(in: 0 ..< 1_000_000)) + let directoryURL = tempDirectory.appendingPathComponent(directoryName) + + do { + try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true) + return directoryURL + } catch { + return nil + } + } +} diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift index 9e10f2a..7f300fb 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift @@ -14,7 +14,7 @@ public protocol FileSyncDaemon: ObservableObject { func tryStart() async func stop() async func refreshSessions() async - func createSession(localPath: String, agentHost: String, remotePath: String) async throws(DaemonError) + func createSession(arg: CreateSyncSessionRequest) async throws(DaemonError) func deleteSessions(ids: [String]) async throws(DaemonError) func pauseSessions(ids: [String]) async throws(DaemonError) func resumeSessions(ids: [String]) async throws(DaemonError) @@ -76,21 +76,6 @@ public class MutagenDaemon: FileSyncDaemon { state = .unavailable return } - - // If there are sync sessions, the daemon should be running - Task { - do throws(DaemonError) { - try await start() - } catch { - state = .failed(error) - return - } - await refreshSessions() - if sessionState.isEmpty { - logger.info("No sync sessions found on startup, stopping daemon") - await stop() - } - } } public func tryStart() async { @@ -99,6 +84,12 @@ public class MutagenDaemon: FileSyncDaemon { try await start() } catch { state = .failed(error) + return + } + await refreshSessions() + if sessionState.isEmpty { + logger.info("No sync sessions found on startup, stopping daemon") + await stop() } } diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift index d1d3f6c..47feffa 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift @@ -17,11 +17,7 @@ public extension MutagenDaemon { sessionState = sessions.sessionStates.map { FileSyncSession(state: $0) } } - func createSession( - localPath: String, - agentHost: String, - remotePath: String - ) async throws(DaemonError) { + func createSession(arg: CreateSyncSessionRequest) async throws(DaemonError) { if case .stopped = state { do throws(DaemonError) { try await start() @@ -35,15 +31,8 @@ public extension MutagenDaemon { let req = Synchronization_CreateRequest.with { req in req.prompter = promptID req.specification = .with { spec in - spec.alpha = .with { alpha in - alpha.protocol = .local - alpha.path = localPath - } - spec.beta = .with { beta in - beta.protocol = .ssh - beta.host = agentHost - beta.path = remotePath - } + spec.alpha = arg.alpha.mutagenURL + spec.beta = arg.beta.mutagenURL // TODO: Ingest a config from somewhere spec.configuration = Synchronization_Configuration() spec.configurationAlpha = Synchronization_Configuration() @@ -64,20 +53,26 @@ public extension MutagenDaemon { func deleteSessions(ids: [String]) async throws(DaemonError) { // Terminating sessions does not require prompting, according to the // Mutagen CLI - let (stream, promptID) = try await host(allowPrompts: false) - defer { stream.cancel() } - guard case .running = state else { return } do { - _ = try await client!.sync.terminate(Synchronization_TerminateRequest.with { req in - req.prompter = promptID - req.selection = .with { selection in - selection.specifications = ids - } - }, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout))) - } catch { - throw .grpcFailure(error) + let (stream, promptID) = try await host(allowPrompts: false) + defer { stream.cancel() } + guard case .running = state else { return } + do { + _ = try await client!.sync.terminate(Synchronization_TerminateRequest.with { req in + req.prompter = promptID + req.selection = .with { selection in + selection.specifications = ids + } + }, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout))) + } catch { + throw .grpcFailure(error) + } } await refreshSessions() + if sessionState.isEmpty { + // Last session was deleted, stop the daemon + await stop() + } } func pauseSessions(ids: [String]) async throws(DaemonError) { @@ -135,3 +130,44 @@ public extension MutagenDaemon { await refreshSessions() } } + +public struct CreateSyncSessionRequest { + public let alpha: Endpoint + public let beta: Endpoint + + public init(alpha: Endpoint, beta: Endpoint) { + self.alpha = alpha + self.beta = beta + } +} + +public struct Endpoint { + public let path: String + public let protocolKind: ProtocolKind + + public init(path: String, protocolKind: ProtocolKind) { + self.path = path + self.protocolKind = protocolKind + } + + public enum ProtocolKind { + case local + case ssh(host: String) + } + + var mutagenURL: Url_URL { + switch protocolKind { + case .local: + .with { url in + url.path = path + url.protocol = .local + } + case let .ssh(host): + .with { url in + url.path = path + url.protocol = .ssh + url.host = host + } + } + } +} diff --git a/Coder-Desktop/project.yml b/Coder-Desktop/project.yml index fb38d35..d256767 100644 --- a/Coder-Desktop/project.yml +++ b/Coder-Desktop/project.yml @@ -164,7 +164,7 @@ targets: SKIP_INSTALL: NO LD_RUNPATH_SEARCH_PATHS: # Load frameworks from the SE bundle. - - "@executable_path/../../Contents/Library/SystemExtensions/com.coder.Coder-Desktop.VPN.systemextension/Contents/Frameworks" + - "@executable_path/../../Contents/Library/SystemExtensions/com.coder.Coder-Desktop.VPN.systemextension/Contents/Frameworks" - "@executable_path/../Frameworks" - "@loader_path/Frameworks" dependencies: @@ -192,6 +192,8 @@ targets: platform: macOS sources: - path: Coder-DesktopTests + - path: Resources + buildPhase: resources settings: base: BUNDLE_LOADER: "$(TEST_HOST)" From bf28b11ea7b0f90ecff8b0df0807b2b65c3b5f92 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 7 Apr 2025 11:49:14 +1000 Subject: [PATCH 2/8] inject daemon in tests --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ebb8e38..558077b 100644 --- a/Makefile +++ b/Makefile @@ -116,7 +116,7 @@ fmt: ## Run Swift file formatter $(FMTFLAGS) . .PHONY: test -test: $(XCPROJECT) ## Run all tests +test: $(XCPROJECT) $(addprefix $(PROJECT)/Resources/,$(MUTAGEN_RESOURCES)) ## Run all tests set -o pipefail && xcodebuild test \ -project $(XCPROJECT) \ -scheme $(SCHEME) \ From c17bc15911b2762b8b2193d039fbbb0fb135ae08 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 7 Apr 2025 12:02:10 +1000 Subject: [PATCH 3/8] fixup --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 558077b..f65e362 100644 --- a/Makefile +++ b/Makefile @@ -116,14 +116,14 @@ fmt: ## Run Swift file formatter $(FMTFLAGS) . .PHONY: test -test: $(XCPROJECT) $(addprefix $(PROJECT)/Resources/,$(MUTAGEN_RESOURCES)) ## Run all tests +test: $(addprefix $(PROJECT)/Resources/,$(MUTAGEN_RESOURCES)) $(XCPROJECT) ## Run all tests set -o pipefail && xcodebuild test \ -project $(XCPROJECT) \ -scheme $(SCHEME) \ -testPlan $(TEST_PLAN) \ -skipPackagePluginValidation \ CODE_SIGNING_REQUIRED=NO \ - CODE_SIGNING_ALLOWED=NO | xcbeautify + CODE_SIGNING_ALLOWED=NO .PHONY: lint lint: lint/swift lint/actions ## Lint all files in the repo From c5c972f9d5775aa3f792d266a4a814042887fb56 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 7 Apr 2025 12:08:57 +1000 Subject: [PATCH 4/8] fixup --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f65e362..115f6e8 100644 --- a/Makefile +++ b/Makefile @@ -123,7 +123,7 @@ test: $(addprefix $(PROJECT)/Resources/,$(MUTAGEN_RESOURCES)) $(XCPROJECT) ## Ru -testPlan $(TEST_PLAN) \ -skipPackagePluginValidation \ CODE_SIGNING_REQUIRED=NO \ - CODE_SIGNING_ALLOWED=NO + CODE_SIGNING_ALLOWED=NO | xcbeautify .PHONY: lint lint: lint/swift lint/actions ## Lint all files in the repo From a07bd8ec6122d9380735e7b75c86ab007e5c1a78 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 7 Apr 2025 12:46:14 +1000 Subject: [PATCH 5/8] review --- Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift b/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift index 9d007eb..32bb182 100644 --- a/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift @@ -16,6 +16,7 @@ class FileSyncDaemonTests { let mutagenAlphaDirectory: URL let mutagenBetaDirectory: URL + // Before each test init() throws { tempDir = FileManager.default.makeTempDir()! #if arch(arm64) @@ -31,6 +32,7 @@ class FileSyncDaemonTests { try FileManager.default.createDirectory(at: mutagenBetaDirectory, withIntermediateDirectories: true) } + // After each test deinit { try? FileManager.default.removeItem(at: tempDir) } From 54701c298bc07aafbb36bfe47f915eff3abf4ca2 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 8 Apr 2025 13:24:17 +1000 Subject: [PATCH 6/8] rebase --- .../Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift index e3af4d0..bd612cb 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift @@ -102,7 +102,7 @@ struct FileSyncSessionModal: View { try await fileSync.createSession( arg: .init( alpha: .init(path: localPath, protocolKind: .local), - beta: .init(path: remotePath, protocolKind: .ssh(host: workspace.primaryHost!)) + beta: .init(path: remotePath, protocolKind: .ssh(host: remotePath)) ) ) } catch { From 2a8ab2016e8cd981e30b00946ed01d104c945cec Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 8 Apr 2025 17:45:18 +1000 Subject: [PATCH 7/8] ignore vcs directories --- .../Views/FileSync/FileSyncSessionModal.swift | 2 +- Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift index bd612cb..66b20ba 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift @@ -102,7 +102,7 @@ struct FileSyncSessionModal: View { try await fileSync.createSession( arg: .init( alpha: .init(path: localPath, protocolKind: .local), - beta: .init(path: remotePath, protocolKind: .ssh(host: remotePath)) + beta: .init(path: remotePath, protocolKind: .ssh(host: remoteHostname)) ) ) } catch { diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift index 47feffa..aaf86b1 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift @@ -33,8 +33,12 @@ public extension MutagenDaemon { req.specification = .with { spec in spec.alpha = arg.alpha.mutagenURL spec.beta = arg.beta.mutagenURL - // TODO: Ingest a config from somewhere - spec.configuration = Synchronization_Configuration() + // TODO: Ingest configs from somewhere + spec.configuration = .with { + // ALWAYS ignore VCS directories for now + // https://mutagen.io/documentation/synchronization/version-control-systems/ + $0.ignoreVcsmode = .ignore + } spec.configurationAlpha = Synchronization_Configuration() spec.configurationBeta = Synchronization_Configuration() } From e2a0f1b453f8e63e094a28c018cd3f947549908f Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 8 Apr 2025 18:11:12 +1000 Subject: [PATCH 8/8] account for swift 6 sendable's house of cards --- .../Coder-DesktopTests/FilePickerTests.swift | 4 ++-- .../Coder-DesktopTests/FileSyncDaemonTests.swift | 4 ++-- Coder-Desktop/Coder-DesktopTests/Util.swift | 13 +++---------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift index 61bf219..d361581 100644 --- a/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift @@ -103,8 +103,8 @@ struct FilePickerTests { try disclosureGroup.expand() // Disclosure group should expand out to 3 more directories - try #expect(await eventually { @MainActor in - return try view.findAll(ViewType.DisclosureGroup.self).count == 6 + #expect(await eventually { @MainActor in + return view.findAll(ViewType.DisclosureGroup.self).count == 6 }) } } diff --git a/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift b/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift index 32bb182..916faf6 100644 --- a/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift @@ -81,9 +81,9 @@ class FileSyncDaemonTests { // Write a file to Alpha let alphaFile = mutagenAlphaDirectory.appendingPathComponent("test.txt") try "Hello, World!".write(to: alphaFile, atomically: true, encoding: .utf8) - try #expect( + #expect( await eventually(timeout: .seconds(5), interval: .milliseconds(100)) { @MainActor in - return try FileManager.default.fileExists( + return FileManager.default.fileExists( atPath: self.mutagenBetaDirectory.appending(path: "test.txt").path() ) }) diff --git a/Coder-Desktop/Coder-DesktopTests/Util.swift b/Coder-Desktop/Coder-DesktopTests/Util.swift index 52e6de1..c5239a9 100644 --- a/Coder-Desktop/Coder-DesktopTests/Util.swift +++ b/Coder-Desktop/Coder-DesktopTests/Util.swift @@ -61,26 +61,19 @@ extension Inspection: @unchecked Sendable, @retroactive InspectionEmissary {} public func eventually( timeout: Duration = .milliseconds(500), interval: Duration = .milliseconds(10), - condition: @escaping () async throws -> Bool -) async throws -> Bool { + condition: @Sendable () async throws -> Bool +) async rethrows -> Bool { let endTime = ContinuousClock.now.advanced(by: timeout) - var lastError: Error? - while ContinuousClock.now < endTime { do { if try await condition() { return true } - lastError = nil } catch { - lastError = error try await Task.sleep(for: interval) } } - if let lastError { - throw lastError - } - return false + return try await condition() } extension FileManager {