1import AppKit2import Foundation3import Testing45@testable import Haven67private final class ManagerURLProtocol: URLProtocol, @unchecked Sendable {8 override class func canInit(with request: URLRequest) -> Bool { true }9 override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }10 override func startLoading() {11 let path = request.url!.path12 let statusCode = path.contains("fail-http") ? 500 : 20013 let data: Data14 if path.contains("image.png") {15 data = onePixelPNG16 } else if path.contains("invalid.png") {17 data = Data("not an image".utf8)18 } else {19 data = Data("wallpaper".utf8)20 }21 let response = HTTPURLResponse(22 url: request.url!, statusCode: statusCode, httpVersion: nil, headerFields: nil)!23 client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)24 client?.urlProtocol(self, didLoad: data)25 client?.urlProtocolDidFinishLoading(self)26 }27 override func stopLoading() {}28}2930private func managerSession() -> URLSession {31 let configuration = URLSessionConfiguration.ephemeral32 configuration.protocolClasses = [ManagerURLProtocol.self]33 return URLSession(configuration: configuration)34}3536private let onePixelPNG = Data(37 base64Encoded:38 "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="39)!4041@MainActor42private final class DesktopImageStub: DesktopImageSetting {43 var receivedURLs: [URL] = []4445 func setDesktopImageURL(46 _ url: URL, for screen: NSScreen, options: [NSWorkspace.DesktopImageOptionKey: Any]47 ) throws {48 receivedURLs.append(url)49 }50}5152@Suite("Favorites persistence", .serialized)53struct FavoritesManagerTests {54 private let defaultsKey = "favoriteWallpapers"5556 @Test("Toggling adds and removes a favorite")57 func toggleFavorite() {58 UserDefaults.standard.removeObject(forKey: defaultsKey)59 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }60 let manager = FavoritesManager()61 let wallpaper = makeWallpaper()6263 manager.toggleFavorite(wallpaper)64 #expect(manager.favorites == [wallpaper])65 #expect(manager.isFavorite(wallpaper))6667 manager.toggleFavorite(wallpaper)68 #expect(manager.favorites.isEmpty)69 #expect(!manager.isFavorite(wallpaper))70 }7172 @Test("Favorites survive manager recreation")73 func favoritesPersist() {74 UserDefaults.standard.removeObject(forKey: defaultsKey)75 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }76 let wallpaper = makeWallpaper()77 FavoritesManager().toggleFavorite(wallpaper)7879 let reloaded = FavoritesManager()8081 #expect(reloaded.favorites == [wallpaper])82 }8384 85 86 final class ThrowingEncoder: JSONEncoder, @unchecked Sendable {87 let testError = NSError(88 domain: "test", code: 7,89 userInfo: [NSLocalizedDescriptionKey: "encode boom"])90 override func encode<T>(_ value: T) throws -> Data where T: Encodable {91 throw testError92 }93 }9495 @Test("Encode failure surfaces through persistenceError without losing in-memory state")96 func encodeFailureIsSurfaced() {97 UserDefaults.standard.removeObject(forKey: defaultsKey)98 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }99 let manager = FavoritesManager(encoder: ThrowingEncoder())100101 manager.toggleFavorite(makeWallpaper())102103 #expect(manager.favorites.count == 1)104 #expect(manager.persistenceError?.contains("encode boom") == true)105 }106107 @Test("Corrupt stored data surfaces through persistenceError on load")108 func corruptDataIsSurfaced() {109 UserDefaults.standard.set(Data("not wallpaper json".utf8), forKey: defaultsKey)110 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }111112 let manager = FavoritesManager()113114 #expect(manager.favorites.isEmpty)115 #expect(manager.persistenceError?.contains("Failed to load favorites") == true)116 }117118}119120@Suite("History persistence", .serialized)121struct HistoryManagerTests {122 private let defaultsKey = "wallpaperHistory"123124 @Test("History keeps newest items first and promotes duplicates")125 func orderingAndPromotion() {126 UserDefaults.standard.removeObject(forKey: defaultsKey)127 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }128 let manager = HistoryManager()129 let first = makeWallpaper(id: "first")130 let second = makeWallpaper(id: "second")131132 manager.addToHistory(first)133 manager.addToHistory(second)134 manager.addToHistory(first)135136 #expect(manager.history.map(\.id) == ["first", "second"])137 }138139 @Test("History is capped at fifty entries")140 func historyLimit() {141 UserDefaults.standard.removeObject(forKey: defaultsKey)142 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }143 let manager = HistoryManager()144145 for index in 0..<55 {146 manager.addToHistory(makeWallpaper(id: "item-\(index)"))147 }148149 #expect(manager.history.count == 50)150 #expect(manager.history.first?.id == "item-54")151 #expect(manager.history.last?.id == "item-5")152 }153154 @Test("History survives recreation and clear removes persisted data")155 func persistenceAndClear() {156 UserDefaults.standard.removeObject(forKey: defaultsKey)157 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }158 HistoryManager().addToHistory(makeWallpaper(id: "persisted"))159160 let reloaded = HistoryManager()161 #expect(reloaded.history.map(\.id) == ["persisted"])162163 reloaded.clearHistory()164 #expect(HistoryManager().history.isEmpty)165 }166167 final class ThrowingEncoder: JSONEncoder, @unchecked Sendable {168 let testError = NSError(169 domain: "test", code: 9,170 userInfo: [NSLocalizedDescriptionKey: "history encode boom"])171 override func encode<T>(_ value: T) throws -> Data where T: Encodable {172 throw testError173 }174 }175176 @Test("Encode failure surfaces through persistenceError without losing in-memory state")177 func encodeFailureIsSurfaced() {178 UserDefaults.standard.removeObject(forKey: defaultsKey)179 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }180 let manager = HistoryManager(encoder: ThrowingEncoder())181182 manager.addToHistory(makeWallpaper(id: "one"))183 manager.addToHistory(makeWallpaper(id: "two"))184185 #expect(manager.history.count == 2)186 #expect(manager.persistenceError?.contains("history encode boom") == true)187 }188189 @Test("Corrupt stored data surfaces through persistenceError on load")190 func corruptDataIsSurfaced() {191 UserDefaults.standard.set(Data("not history json".utf8), forKey: defaultsKey)192 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }193194 let manager = HistoryManager()195196 #expect(manager.history.isEmpty)197 #expect(manager.persistenceError?.contains("Failed to load history") == true)198 }199200 @Test("clearHistory surfaces persistenceError when the encoder fails")201 func clearHistoryEncoderFailureIsSurfaced() {202 UserDefaults.standard.removeObject(forKey: defaultsKey)203 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }204 205 206 HistoryManager().addToHistory(makeWallpaper(id: "seeded"))207208 let manager = HistoryManager(encoder: ThrowingEncoder())209 manager.clearHistory()210211 #expect(manager.history.isEmpty)212 #expect(manager.persistenceError?.contains("history encode boom") == true)213 }214}215216@Suite("Image cache")217struct ImageCacheTests {218 @Test("Inserted images are returned for the same URL")219 func insertAndRetrieve() {220 let url = URL(string: "https://example.com/\(UUID().uuidString).png")!221 let image = NSImage(size: NSSize(width: 10, height: 20))222223 ImageCache.shared.insertImage(image, for: url)224225 #expect(ImageCache.shared.image(for: url) === image)226 }227228 @Test("Different URLs do not collide")229 func keysDoNotCollide() {230 let base = UUID().uuidString231 let firstURL = URL(string: "https://example.com/\(base)-first.png")!232 let secondURL = URL(string: "https://example.com/\(base)-second.png")!233234 ImageCache.shared.insertImage(NSImage(size: NSSize(width: 1, height: 1)), for: firstURL)235236 #expect(ImageCache.shared.image(for: secondURL) == nil)237 }238239 @Test("Fetch decodes and caches successful image responses")240 func fetchAndCache() async throws {241 let cache = ImageCache(session: managerSession())242 let url = URL(string: "https://example.com/image.png")!243244 let first = try await cache.fetchImage(url: url)245 let second = try await cache.fetchImage(url: url)246247 #expect(first === second)248 }249250 @Test("Fetch rejects HTTP errors and invalid image bytes")251 func fetchFailures() async {252 let cache = ImageCache(session: managerSession())253 let httpError = URL(string: "https://example.com/fail-http.png")!254 let invalid = URL(string: "https://example.com/invalid.png")!255 await #expect(throws: WallhavenError.self) { try await cache.fetchImage(url: httpError) }256 await #expect(throws: WallhavenError.self) { try await cache.fetchImage(url: invalid) }257 }258}259260@Suite("Wallpaper file management", .serialized)261@MainActor262struct WallpaperManagerTests {263 @Test("Download validates HTTP status and writes response data")264 func download() async throws {265 let manager = WallpaperManager(session: managerSession())266 let url = URL(string: "https://example.com/\(UUID().uuidString).jpg")!267 let downloaded = try await manager.download(from: url)268 defer { try? FileManager.default.removeItem(at: downloaded) }269270 #expect(try Data(contentsOf: downloaded) == Data("wallpaper".utf8))271272 let errorURL = URL(string: "https://example.com/fail-http.jpg")!273 await #expect(throws: WallhavenError.self) { try await manager.download(from: errorURL) }274 }275276 @Test("Desktop setter receives explicit and default screens")277 @MainActor278 func desktopSetter() throws {279 guard let screen = NSScreen.screens.first else { return }280 var received: [NSScreen] = []281 let manager = WallpaperManager(desktopSetter: { _, target in received.append(target) })282 let url = URL(fileURLWithPath: "/tmp/test-wallpaper.jpg")283284 try manager.setDesktopImage(from: url, screen: screen)285 try manager.setDesktopImage(from: url)286287 #expect(received.count == 2)288 #expect(received[0] === screen)289 }290291 @Test("Default desktop adapter forwards the image to its workspace")292 func defaultDesktopAdapter() throws {293 guard let screen = NSScreen.screens.first else { return }294 let workspace = DesktopImageStub()295 let manager = WallpaperManager(workspace: workspace)296 let url = URL(fileURLWithPath: "/tmp/test-wallpaper.jpg")297298 try manager.setDesktopImage(from: url, screen: screen)299300 #expect(workspace.receivedURLs == [url])301 }302303 @Test("Set wallpaper composes download and desktop update")304 @MainActor305 func setWallpaper() async throws {306 guard let screen = NSScreen.screens.first else { return }307 var receivedURL: URL?308 let manager = WallpaperManager(309 session: managerSession(), desktopSetter: { url, _ in receivedURL = url })310 let remote = URL(string: "https://example.com/\(UUID().uuidString).jpg")!311312 try await manager.setWallpaper(from: remote, screen: screen)313 defer { if let receivedURL { try? FileManager.default.removeItem(at: receivedURL) } }314315 316 317 #expect(receivedURL?.pathExtension == remote.pathExtension)318 #expect(receivedURL?.lastPathComponent.hasPrefix("haven-") == true)319 #expect(receivedURL?.lastPathComponent != remote.lastPathComponent)320 #expect(try Data(contentsOf: receivedURL!) == Data("wallpaper".utf8))321 }322323 @Test("Saving creates the destination folder and copies the file")324 func saveCreatesFolderAndCopies() throws {325 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)326 defer { try? FileManager.default.removeItem(at: root) }327 let source = root.appendingPathComponent("source/wallpaper.jpg")328 let destination = root.appendingPathComponent("nested/destination")329 try FileManager.default.createDirectory(330 at: source.deletingLastPathComponent(), withIntermediateDirectories: true)331 try Data("first".utf8).write(to: source)332333 try WallpaperManager.shared.saveToFolder(from: source, to: destination)334335 let copied = try Data(contentsOf: destination.appendingPathComponent("wallpaper.jpg"))336 #expect(copied == Data("first".utf8))337 }338339 @Test("Saving replaces an existing file with fresh data")340 func saveReplacesExistingFile() throws {341 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)342 defer { try? FileManager.default.removeItem(at: root) }343 let source = root.appendingPathComponent("source/wallpaper.jpg")344 let destination = root.appendingPathComponent("destination")345 try FileManager.default.createDirectory(346 at: source.deletingLastPathComponent(), withIntermediateDirectories: true)347 try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)348 try Data("new".utf8).write(to: source)349 try Data("old".utf8).write(to: destination.appendingPathComponent("wallpaper.jpg"))350351 try WallpaperManager.shared.saveToFolder(from: source, to: destination)352353 let copied = try Data(contentsOf: destination.appendingPathComponent("wallpaper.jpg"))354 #expect(copied == Data("new".utf8))355 }356357 @Test("Saving the same logical wallpaper leaves the existing file untouched")358 func saveSameContentIsIdempotent() throws {359 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)360 defer { try? FileManager.default.removeItem(at: root) }361 let source = root.appendingPathComponent("source/wallpaper.jpg")362 let destination = root.appendingPathComponent("destination")363 try FileManager.default.createDirectory(364 at: source.deletingLastPathComponent(), withIntermediateDirectories: true)365 try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)366 try Data("identical".utf8).write(to: source)367 let existingURL = destination.appendingPathComponent("wallpaper.jpg")368 try Data("identical".utf8).write(to: existingURL)369 let originalAttributes = try FileManager.default.attributesOfItem(atPath: existingURL.path)370371 try WallpaperManager.shared.saveToFolder(from: source, to: destination)372373 let updatedAttributes = try FileManager.default.attributesOfItem(atPath: existingURL.path)374 #expect(try Data(contentsOf: existingURL) == Data("identical".utf8))375 #expect(376 (updatedAttributes[.modificationDate] as? Date)377 == (originalAttributes[.modificationDate] as? Date)378 )379 }380381 @Test("Saving into the same path as the source does not destroy data")382 func saveSamePathAsSource() throws {383 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)384 defer { try? FileManager.default.removeItem(at: root) }385 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)386 let source = root.appendingPathComponent("wallpaper.jpg")387 try Data("payload".utf8).write(to: source)388389 try WallpaperManager.shared.saveToFolder(from: source, to: root)390391 #expect(try Data(contentsOf: source) == Data("payload".utf8))392 }393394 @Test("Distinct URLs sharing a lastPathComponent produce distinct temp files")395 func distinctDownloadsDoNotCollide() async throws {396 let manager = WallpaperManager(session: managerSession())397 let first = URL(string: "https://example.com/aaa/wallpaper.jpg")!398 let second = URL(string: "https://example.com/bbb/wallpaper.jpg")!399400 let firstFile = try await manager.download(from: first)401 let secondFile = try await manager.download(from: second)402 defer {403 try? FileManager.default.removeItem(at: firstFile)404 try? FileManager.default.removeItem(at: secondFile)405 }406407 #expect(firstFile.path != secondFile.path)408 #expect(firstFile.lastPathComponent != secondFile.lastPathComponent)409 #expect(try Data(contentsOf: firstFile) == Data("wallpaper".utf8))410 #expect(try Data(contentsOf: secondFile) == Data("wallpaper".utf8))411 }412413 @Test("Repeated downloads of the same URL produce independent temp files")414 func repeatedDownloadsAreIndependent() async throws {415 let manager = WallpaperManager(session: managerSession())416 let remote = URL(string: "https://example.com/\(UUID().uuidString).jpg")!417418 let first = try await manager.download(from: remote)419 let second = try await manager.download(from: remote)420 defer {421 try? FileManager.default.removeItem(at: first)422 try? FileManager.default.removeItem(at: second)423 }424425 #expect(first.path != second.path)426 #expect(try Data(contentsOf: first) == Data("wallpaper".utf8))427 #expect(try Data(contentsOf: second) == Data("wallpaper".utf8))428 }429430 @Test("makeTempURL preserves the source extension when present")431 func makeTempURLPreservesExtension() {432 let withExtension = URL(string: "https://example.com/foo/wallpaper.png")!433 let withoutExtension = URL(string: "https://example.com/foo/wallpaper")!434435 #expect(WallpaperManager.makeTempURL(for: withExtension).pathExtension == "png")436 #expect(WallpaperManager.makeTempURL(for: withoutExtension).pathExtension == "img")437 #expect(438 WallpaperManager.makeTempURL(for: withExtension).path439 != WallpaperManager.makeTempURL(for: withExtension).path)440 }441}442443@Suite("Autostart persistence", .serialized)444@MainActor445struct AutostartManagerTests {446 @Test("Enable writes a launch agent and disable removes it")447 func enableAndDisable() throws {448 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)449 defer { try? FileManager.default.removeItem(at: root) }450 let manager = AutostartManager(451 libraryDirectory: root, executablePath: { "/Applications/Haven.app/Contents/MacOS/Haven" })452453 #expect(!manager.isEnabled)454 manager.isEnabled = true455 #expect(manager.isEnabled)456 manager.isEnabled = true457458 let plist = root.appendingPathComponent("LaunchAgents/com.june.Haven.plist")459 let contents =460 try PropertyListSerialization.propertyList(461 from: Data(contentsOf: plist), format: nil) as? [String: Any]462 #expect(contents?["Label"] as? String == "com.june.Haven")463 #expect(464 contents?["ProgramArguments"] as? [String]465 == ["/Applications/Haven.app/Contents/MacOS/Haven"])466 #expect(contents?["RunAtLoad"] as? Bool == true)467468 manager.isEnabled = false469 #expect(!manager.isEnabled)470 manager.isEnabled = false471 }472473 @Test("Missing executable leaves autostart disabled")474 func missingExecutable() {475 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)476 defer { try? FileManager.default.removeItem(at: root) }477 let manager = AutostartManager(libraryDirectory: root, executablePath: { nil })478479 manager.isEnabled = true480481 #expect(!manager.isEnabled)482 }483484 @Test("Default executable provider writes only inside the injected library")485 func defaultExecutableProvider() {486 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)487 defer { try? FileManager.default.removeItem(at: root) }488 let manager = AutostartManager(libraryDirectory: root)489 manager.isEnabled = true490 #expect(manager.isEnabled)491 manager.isEnabled = false492 }493494 @Test("Unwritable launch-agent roots do not enable autostart")495 func writeFailure() throws {496 let file = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)497 defer { try? FileManager.default.removeItem(at: file) }498 try Data("not a directory".utf8).write(to: file)499 let manager = AutostartManager(libraryDirectory: file, executablePath: { "/tmp/Haven" })500 manager.isEnabled = true501 #expect(!manager.isEnabled)502 }503504 @Test("Removal failures leave the launch agent enabled")505 func removalFailure() {506 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)507 defer { try? FileManager.default.removeItem(at: root) }508 let manager = AutostartManager(509 libraryDirectory: root, executablePath: { "/tmp/Haven" },510 removePlist: { _ in throw CocoaError(.fileWriteNoPermission) })511 manager.isEnabled = true512 manager.isEnabled = false513 #expect(manager.isEnabled)514 }515}516517@Suite("Local wallpaper library", .serialized)518@MainActor519struct LocalWallpapersManagerTests {520 private func waitForScan(_ manager: LocalWallpapersManager) async {521 for _ in 0..<100 where manager.isLoading {522 try? await Task.sleep(nanoseconds: 10_000_000)523 }524 }525526 @Test("Scan filters files, reads metadata, and deletes wallpapers")527 func scanAndDelete() async throws {528 let defaults = UserDefaults.standard529 let oldPath = defaults.string(forKey: "saveFolderPath")530 defaults.removeObject(forKey: "saveFolderPath")531 defer {532 if let oldPath {533 defaults.set(oldPath, forKey: "saveFolderPath")534 } else {535 defaults.removeObject(forKey: "saveFolderPath")536 }537 }538 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)539 defer { try? FileManager.default.removeItem(at: root) }540 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)541 try onePixelPNG.write(to: root.appendingPathComponent("wallpaper.png"))542 try Data("broken".utf8).write(to: root.appendingPathComponent("broken.png"))543 try Data("ignored".utf8).write(to: root.appendingPathComponent("notes.txt"))544 let manager = LocalWallpapersManager()545546 manager.scanFolder(path: root.path)547 manager.scanFolder(path: root.path)548 await waitForScan(manager)549550 let wallpaper = try #require(manager.wallpapers.first)551 #expect(manager.wallpapers.count == 1)552 #expect(wallpaper.thumbnailURL == wallpaper.url)553 #expect(wallpaper.filename == "wallpaper.png")554 #expect(wallpaper.resolution == "1x1")555 #expect(LocalWallpapersManager.wallpaper(at: root.appendingPathComponent("broken.png")) == nil)556 #expect(LocalWallpapersManager.wallpaper(at: root.appendingPathComponent("missing.png")) == nil)557558 manager.deleteWallpaper(wallpaper)559 #expect(manager.wallpapers.isEmpty)560 manager.deleteWallpaper(wallpaper)561 #expect(manager.errorMessage?.hasPrefix("Failed to delete") == true)562 }563564 @Test("Invalid scan roots surface an error")565 func scanError() async throws {566 let defaults = UserDefaults.standard567 defaults.removeObject(forKey: "saveFolderPath")568 let file = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)569 defer { try? FileManager.default.removeItem(at: file) }570 try Data("file".utf8).write(to: file)571 let manager = LocalWallpapersManager()572573 manager.scanFolder(path: file.path)574 await waitForScan(manager)575576 #expect(manager.errorMessage?.hasPrefix("Failed to scan folder") == true)577 #expect(!manager.isLoading)578 }579580 @Test("Concurrent scan requests are dispatched, not silently dropped")581 func concurrentScansAreDispatched() async throws {582 let defaults = UserDefaults.standard583 let oldPath = defaults.string(forKey: "saveFolderPath")584 defaults.removeObject(forKey: "saveFolderPath")585 defer {586 if let oldPath {587 defaults.set(oldPath, forKey: "saveFolderPath")588 } else {589 defaults.removeObject(forKey: "saveFolderPath")590 }591 }592 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)593 defer { try? FileManager.default.removeItem(at: root) }594 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)595596 let manager = LocalWallpapersManager()597 598 manager.scanFolder(path: root.path)599 await waitForScan(manager)600 #expect(manager.wallpapers.isEmpty)601602 603 604 605 try onePixelPNG.write(to: root.appendingPathComponent("first.png"))606 try onePixelPNG.write(to: root.appendingPathComponent("second.png"))607 manager.scanFolder(path: root.path)608 manager.scanFolder(path: root.path)609 await waitForScan(manager)610 #expect(manager.wallpapers.contains(where: { $0.filename == "first.png" }))611 #expect(manager.wallpapers.contains(where: { $0.filename == "second.png" }))612 #expect(!manager.isLoading)613 #expect(manager.errorMessage == nil)614 }615616 @Test("Reissuing a scan during an in-flight scan observes later state via generation")617 func scanDuringFlightWinsForLatestGeneration() async throws {618 let defaults = UserDefaults.standard619 let oldPath = defaults.string(forKey: "saveFolderPath")620 defaults.removeObject(forKey: "saveFolderPath")621 defer {622 if let oldPath {623 defaults.set(oldPath, forKey: "saveFolderPath")624 } else {625 defaults.removeObject(forKey: "saveFolderPath")626 }627 }628 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)629 defer { try? FileManager.default.removeItem(at: root) }630 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)631 let manager = LocalWallpapersManager()632633 634 635 636 637 manager.scanFolder(path: root.path)638 manager.scanFolder(path: root.path)639 manager.scanFolder(path: root.path)640 await waitForScan(manager)641 #expect(!manager.isLoading)642 #expect(manager.wallpapers.isEmpty)643 #expect(manager.errorMessage == nil)644645 646 try onePixelPNG.write(to: root.appendingPathComponent("added.png"))647 manager.scanFolder(path: root.path)648 await waitForScan(manager)649 #expect(manager.wallpapers.contains(where: { $0.filename == "added.png" }))650 }651652 @Test("Monitoring configured folders observes filesystem changes and stops")653 func monitoring() async throws {654 let defaults = UserDefaults.standard655 let oldPath = defaults.string(forKey: "saveFolderPath")656 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)657 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)658 defaults.set(root.path, forKey: "saveFolderPath")659 defer {660 try? FileManager.default.removeItem(at: root)661 if let oldPath {662 defaults.set(oldPath, forKey: "saveFolderPath")663 } else {664 defaults.removeObject(forKey: "saveFolderPath")665 }666 }667 let manager = LocalWallpapersManager()668 await waitForScan(manager)669670 try onePixelPNG.write(to: root.appendingPathComponent("observed.png"))671 try? await Task.sleep(nanoseconds: 100_000_000)672 await waitForScan(manager)673 #expect(manager.wallpapers.contains(where: { $0.filename == "observed.png" }))674675 manager.stopMonitoring()676 try? await Task.sleep(nanoseconds: 500_000_000)677678 let invalidMonitor = FolderMonitor(folderPath: root.appendingPathComponent("missing").path) {}679 invalidMonitor.start()680 invalidMonitor.closeFileDescriptor()681682 let descriptor = open("/dev/null", O_RDONLY)683 let descriptorMonitor = FolderMonitor(684 folderPath: "/dev/null", fileDescriptor: descriptor, callback: {})685 descriptorMonitor.closeFileDescriptor()686 #expect(fcntl(descriptor, F_GETFD) == -1)687 }688}