Skip to content

Commit a172337

Browse files
committed
chore: add file sync daemon tests
1 parent 968fa7b commit a172337

File tree

9 files changed

+262
-52
lines changed

9 files changed

+262
-52
lines changed

Diff for: Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
5151
#elseif arch(x86_64)
5252
let mutagenBinary = "mutagen-darwin-amd64"
5353
#endif
54-
fileSyncDaemon = MutagenDaemon(
54+
let fileSyncDaemon = MutagenDaemon(
5555
mutagenPath: Bundle.main.url(forResource: mutagenBinary, withExtension: nil)
5656
)
57+
Task {
58+
await fileSyncDaemon.tryStart()
59+
}
60+
self.fileSyncDaemon = fileSyncDaemon
5761
}
5862

5963
func applicationDidFinishLaunching(_: Notification) {

Diff for: Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ final class PreviewFileSync: FileSyncDaemon {
2020
state = .stopped
2121
}
2222

23-
func createSession(localPath _: String, agentHost _: String, remotePath _: String) async throws(DaemonError) {}
23+
func createSession(arg _: CreateSyncSessionRequest) async throws(DaemonError) {}
2424

2525
func deleteSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
2626

Diff for: Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift

-4
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,6 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
175175
defer { loading = false }
176176
do throws(DaemonError) {
177177
try await fileSync.deleteSessions(ids: [selection!])
178-
if fileSync.sessionState.isEmpty {
179-
// Last session was deleted, stop the daemon
180-
await fileSync.stop()
181-
}
182178
} catch {
183179
actionError = error
184180
}

Diff for: Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift

+4-3
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,10 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
9999
try await fileSync.deleteSessions(ids: [existingSession.id])
100100
}
101101
try await fileSync.createSession(
102-
localPath: localPath,
103-
agentHost: workspace.primaryHost!,
104-
remotePath: remotePath
102+
arg: .init(
103+
alpha: .init(path: localPath, protocolKind: .local),
104+
beta: .init(path: remotePath, protocolKind: .ssh(host: workspace.primaryHost!))
105+
)
105106
)
106107
} catch {
107108
createError = error
+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
@testable import Coder_Desktop
2+
import Foundation
3+
import GRPC
4+
import NIO
5+
import Subprocess
6+
import Testing
7+
import VPNLib
8+
import XCTest
9+
10+
@MainActor
11+
@Suite(.timeLimit(.minutes(1)))
12+
class FileSyncDaemonTests {
13+
let tempDir: URL
14+
let mutagenBinary: URL
15+
let mutagenDataDirectory: URL
16+
let mutagenAlphaDirectory: URL
17+
let mutagenBetaDirectory: URL
18+
19+
init() throws {
20+
tempDir = FileManager.default.makeTempDir()!
21+
#if arch(arm64)
22+
let binaryName = "mutagen-darwin-arm64"
23+
#elseif arch(x86_64)
24+
let binaryName = "mutagen-darwin-amd64"
25+
#endif
26+
mutagenBinary = Bundle.main.url(forResource: binaryName, withExtension: nil)!
27+
mutagenDataDirectory = tempDir.appending(path: "mutagen")
28+
mutagenAlphaDirectory = tempDir.appending(path: "alpha")
29+
try FileManager.default.createDirectory(at: mutagenAlphaDirectory, withIntermediateDirectories: true)
30+
mutagenBetaDirectory = tempDir.appending(path: "beta")
31+
try FileManager.default.createDirectory(at: mutagenBetaDirectory, withIntermediateDirectories: true)
32+
}
33+
34+
deinit {
35+
try? FileManager.default.removeItem(at: tempDir)
36+
}
37+
38+
private func statesEqual(_ first: DaemonState, _ second: DaemonState) -> Bool {
39+
switch (first, second) {
40+
case (.stopped, .stopped):
41+
true
42+
case (.running, .running):
43+
true
44+
case (.unavailable, .unavailable):
45+
true
46+
default:
47+
false
48+
}
49+
}
50+
51+
@Test
52+
func fullSync() async throws {
53+
let daemon = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory)
54+
#expect(statesEqual(daemon.state, .stopped))
55+
#expect(daemon.sessionState.count == 0)
56+
57+
// The daemon won't start until we create a session
58+
await daemon.tryStart()
59+
#expect(statesEqual(daemon.state, .stopped))
60+
#expect(daemon.sessionState.count == 0)
61+
62+
try await daemon.createSession(
63+
arg: .init(
64+
alpha: .init(
65+
path: mutagenAlphaDirectory.path(),
66+
protocolKind: .local
67+
),
68+
beta: .init(
69+
path: mutagenBetaDirectory.path(),
70+
protocolKind: .local
71+
)
72+
)
73+
)
74+
75+
// Daemon should have started itself
76+
#expect(statesEqual(daemon.state, .running))
77+
#expect(daemon.sessionState.count == 1)
78+
79+
// Write a file to Alpha
80+
let alphaFile = mutagenAlphaDirectory.appendingPathComponent("test.txt")
81+
try "Hello, World!".write(to: alphaFile, atomically: true, encoding: .utf8)
82+
try #expect(
83+
await eventually(timeout: .seconds(5), interval: .milliseconds(100)) { @MainActor in
84+
return try FileManager.default.fileExists(
85+
atPath: self.mutagenBetaDirectory.appending(path: "test.txt").path()
86+
)
87+
})
88+
89+
try await daemon.deleteSessions(ids: daemon.sessionState.map(\.id))
90+
#expect(daemon.sessionState.count == 0)
91+
// Daemon should have stopped itself once all sessions are deleted
92+
#expect(statesEqual(daemon.state, .stopped))
93+
}
94+
95+
@Test
96+
func autoStopStart() async throws {
97+
let daemon = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory)
98+
#expect(statesEqual(daemon.state, .stopped))
99+
#expect(daemon.sessionState.count == 0)
100+
101+
try await daemon.createSession(
102+
arg: .init(
103+
alpha: .init(
104+
path: mutagenAlphaDirectory.path(),
105+
protocolKind: .local
106+
),
107+
beta: .init(
108+
path: mutagenBetaDirectory.path(),
109+
protocolKind: .local
110+
)
111+
)
112+
)
113+
114+
try await daemon.createSession(
115+
arg: .init(
116+
alpha: .init(
117+
path: mutagenAlphaDirectory.path(),
118+
protocolKind: .local
119+
),
120+
beta: .init(
121+
path: mutagenBetaDirectory.path(),
122+
protocolKind: .local
123+
)
124+
)
125+
)
126+
127+
#expect(statesEqual(daemon.state, .running))
128+
#expect(daemon.sessionState.count == 2)
129+
130+
try await daemon.deleteSessions(ids: [daemon.sessionState[0].id])
131+
#expect(daemon.sessionState.count == 1)
132+
#expect(statesEqual(daemon.state, .running))
133+
134+
try await daemon.deleteSessions(ids: [daemon.sessionState[0].id])
135+
#expect(daemon.sessionState.count == 0)
136+
#expect(statesEqual(daemon.state, .stopped))
137+
}
138+
139+
@Test
140+
func orphaned() async throws {
141+
let daemon1 = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory)
142+
await daemon1.refreshSessions()
143+
try await daemon1.createSession(arg:
144+
.init(
145+
alpha: .init(
146+
path: mutagenAlphaDirectory.path(),
147+
protocolKind: .local
148+
),
149+
beta: .init(
150+
path: mutagenBetaDirectory.path(),
151+
protocolKind: .local
152+
)
153+
)
154+
)
155+
#expect(statesEqual(daemon1.state, .running))
156+
#expect(daemon1.sessionState.count == 1)
157+
158+
let daemon2 = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory)
159+
await daemon2.tryStart()
160+
#expect(statesEqual(daemon2.state, .running))
161+
162+
// Daemon 2 should have killed daemon 1, causing it to fail
163+
#expect(daemon1.state.isFailed)
164+
}
165+
}

Diff for: Coder-Desktop/Coder-DesktopTests/Util.swift

+16-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class MockFileSyncDaemon: FileSyncDaemon {
4747
[]
4848
}
4949

50-
func createSession(localPath _: String, agentHost _: String, remotePath _: String) async throws(DaemonError) {}
50+
func createSession(arg _: CreateSyncSessionRequest) async throws(DaemonError) {}
5151

5252
func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
5353

@@ -82,3 +82,18 @@ public func eventually(
8282
}
8383
return false
8484
}
85+
86+
extension FileManager {
87+
func makeTempDir() -> URL? {
88+
let tempDirectory = FileManager.default.temporaryDirectory
89+
let directoryName = String(Int.random(in: 0 ..< 1_000_000))
90+
let directoryURL = tempDirectory.appendingPathComponent(directoryName)
91+
92+
do {
93+
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
94+
return directoryURL
95+
} catch {
96+
return nil
97+
}
98+
}
99+
}

Diff for: Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift

+7-16
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public protocol FileSyncDaemon: ObservableObject {
1414
func tryStart() async
1515
func stop() async
1616
func refreshSessions() async
17-
func createSession(localPath: String, agentHost: String, remotePath: String) async throws(DaemonError)
17+
func createSession(arg: CreateSyncSessionRequest) async throws(DaemonError)
1818
func deleteSessions(ids: [String]) async throws(DaemonError)
1919
func pauseSessions(ids: [String]) async throws(DaemonError)
2020
func resumeSessions(ids: [String]) async throws(DaemonError)
@@ -76,21 +76,6 @@ public class MutagenDaemon: FileSyncDaemon {
7676
state = .unavailable
7777
return
7878
}
79-
80-
// If there are sync sessions, the daemon should be running
81-
Task {
82-
do throws(DaemonError) {
83-
try await start()
84-
} catch {
85-
state = .failed(error)
86-
return
87-
}
88-
await refreshSessions()
89-
if sessionState.isEmpty {
90-
logger.info("No sync sessions found on startup, stopping daemon")
91-
await stop()
92-
}
93-
}
9479
}
9580

9681
public func tryStart() async {
@@ -99,6 +84,12 @@ public class MutagenDaemon: FileSyncDaemon {
9984
try await start()
10085
} catch {
10186
state = .failed(error)
87+
return
88+
}
89+
await refreshSessions()
90+
if sessionState.isEmpty {
91+
logger.info("No sync sessions found on startup, stopping daemon")
92+
await stop()
10293
}
10394
}
10495

0 commit comments

Comments
 (0)