Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add file sync daemon tests #129

Open
wants to merge 5 commits into
base: ethan/remote-file-picker
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,6 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: 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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,10 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
try await fileSync.deleteSessions(ids: [existingSession.id])
}
try await fileSync.createSession(
localPath: localPath,
agentHost: workspace.primaryHost!,
remotePath: remotePath
arg: .init(
alpha: .init(path: localPath, protocolKind: .local),
beta: .init(path: remotePath, protocolKind: .ssh(host: workspace.primaryHost!))
)
)
} catch {
createError = error
Expand Down
167 changes: 167 additions & 0 deletions Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
@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

// Before each test
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)
}

// After each test
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)
}
}
17 changes: 16 additions & 1 deletion Coder-Desktop/Coder-DesktopTests/Util.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}

Expand Down Expand Up @@ -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
}
}
}
23 changes: 7 additions & 16 deletions Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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()
}
}

Expand Down
Loading
Loading