diff --git a/peekaboo-cli/Sources/peekaboo/ListCommand.swift b/peekaboo-cli/Sources/peekaboo/ListCommand.swift index 725e5b9..6102be0 100644 --- a/peekaboo-cli/Sources/peekaboo/ListCommand.swift +++ b/peekaboo-cli/Sources/peekaboo/ListCommand.swift @@ -41,22 +41,20 @@ struct AppsSubcommand: ParsableCommand { } private func handleError(_ error: Error) { - let captureError: CaptureError - if let err = error as? CaptureError { - captureError = err + let captureError: CaptureError = if let err = error as? CaptureError { + err } else { - captureError = .unknownError(error.localizedDescription) + .unknownError(error.localizedDescription) } if jsonOutput { - let code: ErrorCode - switch captureError { + let code: ErrorCode = switch captureError { case .screenRecordingPermissionDenied: - code = .PERMISSION_ERROR_SCREEN_RECORDING + .PERMISSION_ERROR_SCREEN_RECORDING case .accessibilityPermissionDenied: - code = .PERMISSION_ERROR_ACCESSIBILITY + .PERMISSION_ERROR_ACCESSIBILITY default: - code = .INTERNAL_SWIFT_ERROR + .INTERNAL_SWIFT_ERROR } outputError( message: captureError.localizedDescription, @@ -142,24 +140,22 @@ struct WindowsSubcommand: ParsableCommand { } private func handleError(_ error: Error) { - let captureError: CaptureError - if let err = error as? CaptureError { - captureError = err + let captureError: CaptureError = if let err = error as? CaptureError { + err } else { - captureError = .unknownError(error.localizedDescription) + .unknownError(error.localizedDescription) } if jsonOutput { - let code: ErrorCode - switch captureError { + let code: ErrorCode = switch captureError { case .screenRecordingPermissionDenied: - code = .PERMISSION_ERROR_SCREEN_RECORDING + .PERMISSION_ERROR_SCREEN_RECORDING case .accessibilityPermissionDenied: - code = .PERMISSION_ERROR_ACCESSIBILITY + .PERMISSION_ERROR_ACCESSIBILITY case .appNotFound: - code = .APP_NOT_FOUND + .APP_NOT_FOUND default: - code = .INTERNAL_SWIFT_ERROR + .INTERNAL_SWIFT_ERROR } outputError( message: captureError.localizedDescription, diff --git a/peekaboo-cli/Sources/peekaboo/Logger.swift b/peekaboo-cli/Sources/peekaboo/Logger.swift index 2f8fc50..981462e 100644 --- a/peekaboo-cli/Sources/peekaboo/Logger.swift +++ b/peekaboo-cli/Sources/peekaboo/Logger.swift @@ -56,8 +56,8 @@ class Logger { } func getDebugLogs() -> [String] { - return queue.sync { - return self.debugLogs + queue.sync { + self.debugLogs } } diff --git a/peekaboo-cli/Sources/peekaboo/Models.swift b/peekaboo-cli/Sources/peekaboo/Models.swift index c1c847c..b13bf5a 100644 --- a/peekaboo-cli/Sources/peekaboo/Models.swift +++ b/peekaboo-cli/Sources/peekaboo/Models.swift @@ -118,9 +118,11 @@ enum CaptureError: Error, LocalizedError { case .noDisplaysAvailable: "No displays available for capture." case .screenRecordingPermissionDenied: - "Screen recording permission is required. Please grant it in System Settings > Privacy & Security > Screen Recording." + "Screen recording permission is required. " + + "Please grant it in System Settings > Privacy & Security > Screen Recording." case .accessibilityPermissionDenied: - "Accessibility permission is required for some operations. Please grant it in System Settings > Privacy & Security > Accessibility." + "Accessibility permission is required for some operations. " + + "Please grant it in System Settings > Privacy & Security > Accessibility." case .invalidDisplayID: "Invalid display ID provided." case .captureCreationFailed: diff --git a/peekaboo-cli/Sources/peekaboo/Version.swift b/peekaboo-cli/Sources/peekaboo/Version.swift index 2a86dca..0d0e853 100644 --- a/peekaboo-cli/Sources/peekaboo/Version.swift +++ b/peekaboo-cli/Sources/peekaboo/Version.swift @@ -3,6 +3,6 @@ // To use this file for development, copy it to Version.swift: // cp Version.swift.development Version.swift -struct Version { +enum Version { static let current = "dev" -} \ No newline at end of file +} diff --git a/peekaboo-cli/Tests/peekabooTests/ApplicationFinderTests.swift b/peekaboo-cli/Tests/peekabooTests/ApplicationFinderTests.swift index 2bc1584..332b47d 100644 --- a/peekaboo-cli/Tests/peekabooTests/ApplicationFinderTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/ApplicationFinderTests.swift @@ -1,47 +1,47 @@ +import AppKit @testable import peekaboo import Testing -import AppKit @Suite("ApplicationFinder Tests", .tags(.applicationFinder, .unit)) struct ApplicationFinderTests { - // MARK: - Test Data - + private static let testIdentifiers = [ "Finder", "finder", "FINDER", "Find", "com.apple.finder" ] - + private static let invalidIdentifiers = [ - "", " ", "NonExistentApp12345", "invalid.bundle.id", + "", " ", "NonExistentApp12345", "invalid.bundle.id", String(repeating: "a", count: 1000) ] + // MARK: - Find Application Tests - + @Test("Finding an app by exact name match", .tags(.fast)) func findApplicationExactMatch() throws { // Test finding an app that should always be running on macOS let result = try ApplicationFinder.findApplication(identifier: "Finder") - + #expect(result.localizedName == "Finder") #expect(result.bundleIdentifier == "com.apple.finder") } - + @Test("Finding an app is case-insensitive", .tags(.fast)) func findApplicationCaseInsensitive() throws { // Test case-insensitive matching let result = try ApplicationFinder.findApplication(identifier: "finder") - + #expect(result.localizedName == "Finder") } - + @Test("Finding an app by bundle identifier", .tags(.fast)) func findApplicationByBundleIdentifier() throws { // Test finding by bundle identifier let result = try ApplicationFinder.findApplication(identifier: "com.apple.finder") - + #expect(result.bundleIdentifier == "com.apple.finder") } - + @Test("Throws error when app is not found", .tags(.fast)) func findApplicationNotFound() throws { // Test app not found error @@ -49,49 +49,51 @@ struct ApplicationFinderTests { try ApplicationFinder.findApplication(identifier: "NonExistentApp12345") } } - + @Test("Finding an app by partial name match", .tags(.fast)) func findApplicationPartialMatch() throws { // Test partial name matching let result = try ApplicationFinder.findApplication(identifier: "Find") - + // Should find Finder as closest match #expect(result.localizedName == "Finder") } - + // MARK: - Parameterized Tests - - @Test("Finding apps with various identifiers", - arguments: [ - ("Finder", "com.apple.finder"), - ("finder", "com.apple.finder"), - ("FINDER", "com.apple.finder"), - ("com.apple.finder", "com.apple.finder") - ]) + + @Test( + "Finding apps with various identifiers", + arguments: [ + ("Finder", "com.apple.finder"), + ("finder", "com.apple.finder"), + ("FINDER", "com.apple.finder"), + ("com.apple.finder", "com.apple.finder") + ] + ) func findApplicationVariousIdentifiers(identifier: String, expectedBundleId: String) throws { let result = try ApplicationFinder.findApplication(identifier: identifier) #expect(result.bundleIdentifier == expectedBundleId) } - + // MARK: - Get All Running Applications Tests - + @Test("Getting all running applications returns non-empty list", .tags(.fast)) func getAllRunningApplications() { // Test getting all running applications let apps = ApplicationFinder.getAllRunningApplications() - + // Should have at least some apps running - #expect(apps.count > 0) - + #expect(!apps.isEmpty) + // Should include Finder let hasFinder = apps.contains { $0.app_name == "Finder" } #expect(hasFinder == true) } - + @Test("All running applications have required properties", .tags(.fast)) func allApplicationsHaveRequiredProperties() { let apps = ApplicationFinder.getAllRunningApplications() - + for app in apps { #expect(!app.app_name.isEmpty) #expect(!app.bundle_id.isEmpty) @@ -99,14 +101,14 @@ struct ApplicationFinderTests { #expect(app.window_count >= 0) } } - + // MARK: - Edge Cases and Advanced Tests - + @Test("Finding app with special characters in name", .tags(.fast)) func findApplicationSpecialCharacters() throws { // Test apps with special characters (if available) let specialApps = ["1Password", "CleanMyMac", "MacBook Pro"] - + for appName in specialApps { do { let result = try ApplicationFinder.findApplication(identifier: appName) @@ -118,25 +120,27 @@ struct ApplicationFinderTests { } } } - + @Test("Fuzzy matching algorithm scoring", .tags(.fast)) func fuzzyMatchingScoring() throws { // Test that exact matches get highest scores let finder = try ApplicationFinder.findApplication(identifier: "Finder") #expect(finder.localizedName == "Finder") - + // Test prefix matching works let findResult = try ApplicationFinder.findApplication(identifier: "Find") #expect(findResult.localizedName == "Finder") } - - @Test("Bundle identifier parsing edge cases", - arguments: [ - "com.apple", - "apple.finder", - "finder", - "com.apple.finder.extra" - ]) + + @Test( + "Bundle identifier parsing edge cases", + arguments: [ + "com.apple", + "apple.finder", + "finder", + "com.apple.finder.extra" + ] + ) func bundleIdentifierEdgeCases(partialBundleId: String) throws { // Should either find Finder or throw appropriate error do { @@ -147,7 +151,7 @@ struct ApplicationFinderTests { #expect(Bool(true)) } } - + @Test("Fuzzy matching prefers exact matches", .tags(.fast)) func fuzzyMatchingPrefersExact() throws { // If we have multiple matches, exact should win @@ -155,21 +159,23 @@ struct ApplicationFinderTests { #expect(result.localizedName == "Finder") #expect(result.bundleIdentifier == "com.apple.finder") } - - @Test("Performance: Finding apps multiple times", - arguments: 1...10) + + @Test( + "Performance: Finding apps multiple times", + arguments: 1...10 + ) func findApplicationPerformance(iteration: Int) throws { // Test that finding an app completes quickly even when called multiple times let result = try ApplicationFinder.findApplication(identifier: "Finder") #expect(result.localizedName == "Finder") } - + @Test("Stress test: Search with many running apps", .tags(.performance)) func stressTestManyApps() { // Get current app count for baseline let apps = ApplicationFinder.getAllRunningApplications() - #expect(apps.count > 0) - + #expect(!apps.isEmpty) + // Test search performance doesn't degrade with app list size let startTime = CFAbsoluteTimeGetCurrent() do { @@ -180,20 +186,22 @@ struct ApplicationFinderTests { Issue.record("Finder should always be found in performance test") } } - + // MARK: - Integration Tests - - @Test("Find and verify running state of system apps", - arguments: [ - ("Finder", true), - ("Dock", true), - ("SystemUIServer", true) - ]) + + @Test( + "Find and verify running state of system apps", + arguments: [ + ("Finder", true), + ("Dock", true), + ("SystemUIServer", true) + ] + ) func verifySystemAppsRunning(appName: String, shouldBeRunning: Bool) throws { do { let result = try ApplicationFinder.findApplication(identifier: appName) #expect(result.localizedName != nil) - + // Verify the app is in the running list let runningApps = ApplicationFinder.getAllRunningApplications() let isInList = runningApps.contains { $0.bundle_id == result.bundleIdentifier } @@ -204,17 +212,17 @@ struct ApplicationFinderTests { } } } - + @Test("Verify frontmost application detection", .tags(.integration)) func verifyFrontmostApp() throws { // Get the frontmost app using NSWorkspace let frontmostApp = NSWorkspace.shared.frontmostApplication - + // Try to find it using our ApplicationFinder if let bundleId = frontmostApp?.bundleIdentifier { let result = try ApplicationFinder.findApplication(identifier: bundleId) #expect(result.bundleIdentifier == bundleId) - + // Verify it's marked as active in our list let runningApps = ApplicationFinder.getAllRunningApplications() let appInfo = runningApps.first { $0.bundle_id == bundleId } @@ -227,21 +235,20 @@ struct ApplicationFinderTests { @Suite("ApplicationFinder Edge Cases", .tags(.applicationFinder, .unit)) struct ApplicationFinderEdgeCaseTests { - @Test("Empty identifier throws appropriate error", .tags(.fast)) func emptyIdentifierError() { #expect(throws: (any Error).self) { try ApplicationFinder.findApplication(identifier: "") } } - + @Test("Whitespace-only identifier throws appropriate error", .tags(.fast)) func whitespaceIdentifierError() { #expect(throws: (any Error).self) { try ApplicationFinder.findApplication(identifier: " ") } } - + @Test("Very long identifier doesn't crash", .tags(.fast)) func veryLongIdentifier() { let longIdentifier = String(repeating: "a", count: 1000) @@ -249,9 +256,11 @@ struct ApplicationFinderEdgeCaseTests { try ApplicationFinder.findApplication(identifier: longIdentifier) } } - - @Test("Unicode identifiers are handled correctly", - arguments: ["😀App", "App™", "Приложение", "アプリ"]) + + @Test( + "Unicode identifiers are handled correctly", + arguments: ["😀App", "App™", "Приложение", "アプリ"] + ) func unicodeIdentifiers(identifier: String) { // Should not crash, either finds or throws appropriate error do { @@ -262,19 +271,19 @@ struct ApplicationFinderEdgeCaseTests { #expect(Bool(true)) } } - + @Test("Case sensitivity in matching", .tags(.fast)) func caseSensitivityMatching() throws { // Test various case combinations let caseVariations = ["finder", "FINDER", "Finder", "fInDeR"] - + for variation in caseVariations { let result = try ApplicationFinder.findApplication(identifier: variation) #expect(result.localizedName == "Finder") #expect(result.bundleIdentifier == "com.apple.finder") } } - + @Test("Concurrent application searches", .tags(.concurrency)) func concurrentSearches() async { // Test thread safety of application finder @@ -289,35 +298,33 @@ struct ApplicationFinderEdgeCaseTests { } } } - + var successCount = 0 - for await success in group { - if success { - successCount += 1 - } + for await success in group where success { + successCount += 1 } - + // All searches should succeed for Finder #expect(successCount == 10) } } - + @Test("Memory usage with large app lists", .tags(.performance)) func memoryUsageTest() { // Test memory doesn't grow excessively with repeated calls for _ in 1...5 { let apps = ApplicationFinder.getAllRunningApplications() - #expect(apps.count > 0) + #expect(!apps.isEmpty) } - + // If we get here without crashing, memory management is working #expect(Bool(true)) } - + @Test("Application list sorting consistency", .tags(.fast)) func applicationListSorting() { let apps = ApplicationFinder.getAllRunningApplications() - + // Verify list is sorted by name (case-insensitive) for index in 1..= previous) } } - + @Test("Window count accuracy", .tags(.integration)) func windowCountAccuracy() { let apps = ApplicationFinder.getAllRunningApplications() - + for app in apps { // Window count should be non-negative #expect(app.window_count >= 0) - + // Finder should typically have at least one window if app.app_name == "Finder" { #expect(app.window_count >= 0) // Could be 0 if all windows minimized } } } -} \ No newline at end of file +} diff --git a/peekaboo-cli/Tests/peekabooTests/ImageCaptureLogicTests.swift b/peekaboo-cli/Tests/peekabooTests/ImageCaptureLogicTests.swift index be7658b..2698ec2 100644 --- a/peekaboo-cli/Tests/peekabooTests/ImageCaptureLogicTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/ImageCaptureLogicTests.swift @@ -1,28 +1,27 @@ +import AppKit +import Foundation @testable import peekaboo import Testing -import Foundation -import AppKit @Suite("Image Capture Logic Tests", .tags(.imageCapture, .unit)) struct ImageCaptureLogicTests { - // MARK: - File Name Generation Tests - + @Test("File name generation for displays", .tags(.fast)) func fileNameGenerationDisplays() throws { // We can't directly test private methods, but we can test the logic // through public interfaces and verify the expected patterns - + // Test that different screen indices would generate different names let command1 = try ImageCommand.parse(["--screen-index", "0", "--format", "png"]) let command2 = try ImageCommand.parse(["--screen-index", "1", "--format", "png"]) - + #expect(command1.screenIndex == 0) #expect(command2.screenIndex == 1) #expect(command1.format == .png) #expect(command2.format == .png) } - + @Test("File name generation for applications", .tags(.fast)) func fileNameGenerationApplications() throws { let command = try ImageCommand.parse([ @@ -31,55 +30,55 @@ struct ImageCaptureLogicTests { "--window-title", "Main Window", "--format", "jpg" ]) - + #expect(command.app == "Test App") #expect(command.windowTitle == "Main Window") #expect(command.format == .jpg) } - + @Test("Output path generation", .tags(.fast)) func outputPathGeneration() throws { // Test default path behavior let defaultCommand = try ImageCommand.parse([]) #expect(defaultCommand.path == nil) - + // Test custom path let customCommand = try ImageCommand.parse(["--path", "/tmp/screenshots"]) #expect(customCommand.path == "/tmp/screenshots") - + // Test path with filename let fileCommand = try ImageCommand.parse(["--path", "/tmp/test.png"]) #expect(fileCommand.path == "/tmp/test.png") } - + // MARK: - Mode Determination Tests - + @Test("Mode determination comprehensive", .tags(.fast)) func modeDeterminationComprehensive() throws { // Screen mode (default when no app specified) let screenCmd = try ImageCommand.parse([]) #expect(screenCmd.mode == nil) #expect(screenCmd.app == nil) - + // Window mode (when app specified but no explicit mode) let windowCmd = try ImageCommand.parse(["--app", "Finder"]) #expect(windowCmd.mode == nil) // Will be determined as window during execution #expect(windowCmd.app == "Finder") - + // Explicit modes let explicitScreen = try ImageCommand.parse(["--mode", "screen"]) #expect(explicitScreen.mode == .screen) - + let explicitWindow = try ImageCommand.parse(["--mode", "window", "--app", "Safari"]) #expect(explicitWindow.mode == .window) #expect(explicitWindow.app == "Safari") - + let explicitMulti = try ImageCommand.parse(["--mode", "multi"]) #expect(explicitMulti.mode == .multi) } - + // MARK: - Window Targeting Tests - + @Test("Window targeting by title", .tags(.fast)) func windowTargetingByTitle() throws { let command = try ImageCommand.parse([ @@ -87,13 +86,13 @@ struct ImageCaptureLogicTests { "--app", "Safari", "--window-title", "Main Window" ]) - + #expect(command.mode == .window) #expect(command.app == "Safari") #expect(command.windowTitle == "Main Window") #expect(command.windowIndex == nil) } - + @Test("Window targeting by index", .tags(.fast)) func windowTargetingByIndex() throws { let command = try ImageCommand.parse([ @@ -101,13 +100,13 @@ struct ImageCaptureLogicTests { "--app", "Terminal", "--window-index", "0" ]) - + #expect(command.mode == .window) #expect(command.app == "Terminal") #expect(command.windowIndex == 0) #expect(command.windowTitle == nil) } - + @Test("Window targeting priority - title vs index", .tags(.fast)) func windowTargetingPriority() throws { // When both title and index are specified, both are preserved @@ -117,83 +116,85 @@ struct ImageCaptureLogicTests { "--window-title", "Main", "--window-index", "1" ]) - + #expect(command.windowTitle == "Main") #expect(command.windowIndex == 1) // In actual execution, title matching would take precedence } - + // MARK: - Screen Targeting Tests - + @Test("Screen targeting by index", .tags(.fast)) func screenTargetingByIndex() throws { let command = try ImageCommand.parse([ "--mode", "screen", "--screen-index", "1" ]) - + #expect(command.mode == .screen) #expect(command.screenIndex == 1) } - - @Test("Screen index edge cases", - arguments: [-1, 0, 1, 5, 99, Int.max]) + + @Test( + "Screen index edge cases", + arguments: [-1, 0, 1, 5, 99, Int.max] + ) func screenIndexEdgeCases(index: Int) throws { let command = try ImageCommand.parse([ "--mode", "screen", "--screen-index", String(index) ]) - + #expect(command.screenIndex == index) // Validation happens during execution, not parsing } - + // MARK: - Capture Focus Tests - + @Test("Capture focus modes", .tags(.fast)) func captureFocusModes() throws { // Default background mode let defaultCmd = try ImageCommand.parse([]) #expect(defaultCmd.captureFocus == .background) - + // Explicit background mode let backgroundCmd = try ImageCommand.parse(["--capture-focus", "background"]) #expect(backgroundCmd.captureFocus == .background) - + // Foreground mode let foregroundCmd = try ImageCommand.parse(["--capture-focus", "foreground"]) #expect(foregroundCmd.captureFocus == .foreground) } - + // MARK: - Image Format Tests - + @Test("Image format handling", .tags(.fast)) func imageFormatHandling() throws { // Default PNG format let defaultCmd = try ImageCommand.parse([]) #expect(defaultCmd.format == .png) - + // Explicit PNG format let pngCmd = try ImageCommand.parse(["--format", "png"]) #expect(pngCmd.format == .png) - + // JPEG format let jpgCmd = try ImageCommand.parse(["--format", "jpg"]) #expect(jpgCmd.format == .jpg) } - + @Test("MIME type mapping", .tags(.fast)) func mimeTypeMapping() { // Test MIME type logic (as used in SavedFile creation) let pngMime = ImageFormat.png == .png ? "image/png" : "image/jpeg" let jpgMime = ImageFormat.jpg == .jpg ? "image/jpeg" : "image/png" - + #expect(pngMime == "image/png") #expect(jpgMime == "image/jpeg") } - + // MARK: - Error Handling Tests - + @Test("Error code mapping", .tags(.fast)) func errorCodeMapping() { // Test error code mapping logic used in handleError @@ -206,18 +207,18 @@ struct ImageCaptureLogicTests { (.invalidArgument("test"), .INVALID_ARGUMENT), (.unknownError("test"), .UNKNOWN_ERROR) ] - + // Verify error mapping logic exists for (_, expectedCode) in testCases { // We can't directly test the private method, but verify the errors exist // Verify the error exists (non-nil check not needed for value types) #expect(Bool(true)) - #expect(expectedCode.rawValue.count > 0) + #expect(!expectedCode.rawValue.isEmpty) } } - + // MARK: - SavedFile Creation Tests - + @Test("SavedFile creation for screen capture", .tags(.fast)) func savedFileCreationScreenCapture() { let savedFile = SavedFile( @@ -228,7 +229,7 @@ struct ImageCaptureLogicTests { window_index: nil, mime_type: "image/png" ) - + #expect(savedFile.path == "/tmp/screen-0.png") #expect(savedFile.item_label == "Display 1 (Index 0)") #expect(savedFile.window_title == nil) @@ -236,7 +237,7 @@ struct ImageCaptureLogicTests { #expect(savedFile.window_index == nil) #expect(savedFile.mime_type == "image/png") } - + @Test("SavedFile creation for window capture", .tags(.fast)) func savedFileCreationWindowCapture() { let savedFile = SavedFile( @@ -247,7 +248,7 @@ struct ImageCaptureLogicTests { window_index: 0, mime_type: "image/jpeg" ) - + #expect(savedFile.path == "/tmp/safari-main.jpg") #expect(savedFile.item_label == "Safari") #expect(savedFile.window_title == "Main Window") @@ -255,9 +256,9 @@ struct ImageCaptureLogicTests { #expect(savedFile.window_index == 0) #expect(savedFile.mime_type == "image/jpeg") } - + // MARK: - Complex Configuration Tests - + @Test("Complex multi-window capture configuration", .tags(.fast)) func complexMultiWindowConfiguration() throws { let command = try ImageCommand.parse([ @@ -268,7 +269,7 @@ struct ImageCaptureLogicTests { "--capture-focus", "foreground", "--json-output" ]) - + #expect(command.mode == .multi) #expect(command.app == "Visual Studio Code") #expect(command.format == .png) @@ -276,7 +277,7 @@ struct ImageCaptureLogicTests { #expect(command.captureFocus == .foreground) #expect(command.jsonOutput == true) } - + @Test("Complex screen capture configuration", .tags(.fast)) func complexScreenCaptureConfiguration() throws { let command = try ImageCommand.parse([ @@ -286,16 +287,16 @@ struct ImageCaptureLogicTests { "--path", "/Users/test/screenshots/display-1.jpg", "--json-output" ]) - + #expect(command.mode == .screen) #expect(command.screenIndex == 1) #expect(command.format == .jpg) #expect(command.path == "/Users/test/screenshots/display-1.jpg") #expect(command.jsonOutput == true) } - + // MARK: - Performance Tests - + @Test("Configuration parsing performance", .tags(.performance)) func configurationParsingPerformance() { let complexArgs = [ @@ -309,9 +310,9 @@ struct ImageCaptureLogicTests { "--capture-focus", "foreground", "--json-output" ] - + let startTime = CFAbsoluteTimeGetCurrent() - + // Parse many times to test performance for _ in 1...100 { do { @@ -321,40 +322,40 @@ struct ImageCaptureLogicTests { Issue.record("Parsing should not fail: \(error)") } } - + let duration = CFAbsoluteTimeGetCurrent() - startTime #expect(duration < 1.0) // Should parse 1000 configs within 1 second } - + // MARK: - Integration Readiness Tests - + @Test("Command readiness for screen capture", .tags(.fast)) func commandReadinessScreenCapture() throws { let command = try ImageCommand.parse(["--mode", "screen"]) - + // Verify command is properly configured for screen capture #expect(command.mode == .screen) #expect(command.app == nil) // No app needed for screen capture #expect(command.format == .png) // Has default format } - + @Test("Command readiness for window capture", .tags(.fast)) func commandReadinessWindowCapture() throws { let command = try ImageCommand.parse([ "--mode", "window", "--app", "Finder" ]) - + // Verify command is properly configured for window capture #expect(command.mode == .window) #expect(command.app == "Finder") // App is required #expect(command.format == .png) // Has default format } - + @Test("Command validation for invalid configurations", .tags(.fast)) func commandValidationInvalidConfigurations() { // These should parse successfully but would fail during execution - + // Window mode without app (would fail during execution) do { let command = try ImageCommand.parse(["--mode", "window"]) @@ -363,7 +364,7 @@ struct ImageCaptureLogicTests { } catch { Issue.record("Should parse successfully") } - + // Invalid screen index (would fail during execution) do { let command = try ImageCommand.parse(["--screen-index", "-1"]) @@ -378,7 +379,6 @@ struct ImageCaptureLogicTests { @Suite("Advanced Image Capture Logic", .tags(.imageCapture, .integration)) struct AdvancedImageCaptureLogicTests { - @Test("Multi-mode capture scenarios", .tags(.fast)) func multiModeCaptureScenarios() throws { // Multi mode with app (should capture all windows) @@ -388,13 +388,13 @@ struct AdvancedImageCaptureLogicTests { ]) #expect(multiWithApp.mode == .multi) #expect(multiWithApp.app == "Safari") - + // Multi mode without app (should capture all screens) let multiWithoutApp = try ImageCommand.parse(["--mode", "multi"]) #expect(multiWithoutApp.mode == .multi) #expect(multiWithoutApp.app == nil) } - + @Test("Focus mode implications", .tags(.fast)) func focusModeImplications() throws { // Foreground focus should work with any capture mode @@ -403,14 +403,14 @@ struct AdvancedImageCaptureLogicTests { "--capture-focus", "foreground" ]) #expect(foregroundScreen.captureFocus == .foreground) - + let foregroundWindow = try ImageCommand.parse([ "--mode", "window", "--app", "Terminal", "--capture-focus", "foreground" ]) #expect(foregroundWindow.captureFocus == .foreground) - + // Background focus (default) should work without additional permissions let backgroundCapture = try ImageCommand.parse([ "--mode", "window", @@ -418,30 +418,30 @@ struct AdvancedImageCaptureLogicTests { ]) #expect(backgroundCapture.captureFocus == .background) } - + @Test("Path handling edge cases", .tags(.fast)) func pathHandlingEdgeCases() throws { // Relative paths let relativePath = try ImageCommand.parse(["--path", "./screenshots/test.png"]) #expect(relativePath.path == "./screenshots/test.png") - + // Home directory expansion let homePath = try ImageCommand.parse(["--path", "~/Desktop/capture.jpg"]) #expect(homePath.path == "~/Desktop/capture.jpg") - + // Absolute paths let absolutePath = try ImageCommand.parse(["--path", "/tmp/absolute/path.png"]) #expect(absolutePath.path == "/tmp/absolute/path.png") - + // Paths with spaces let spacePath = try ImageCommand.parse(["--path", "/path with spaces/image.png"]) #expect(spacePath.path == "/path with spaces/image.png") - + // Unicode paths let unicodePath = try ImageCommand.parse(["--path", "/tmp/测试/スクリーン.png"]) #expect(unicodePath.path == "/tmp/测试/スクリーン.png") } - + @Test("Command execution readiness matrix", .tags(.fast)) func commandExecutionReadinessMatrix() { // Define test scenarios @@ -456,7 +456,7 @@ struct AdvancedImageCaptureLogicTests { (["--app", "Finder"], true, "Implicit window mode"), ([], true, "Default screen capture") ] - + for scenario in scenarios { do { let command = try ImageCommand.parse(scenario.args) @@ -472,7 +472,7 @@ struct AdvancedImageCaptureLogicTests { } } } - + @Test("Error propagation scenarios", .tags(.fast)) func errorPropagationScenarios() { // Test that invalid arguments are properly handled @@ -483,14 +483,14 @@ struct AdvancedImageCaptureLogicTests { ["--screen-index", "abc"], ["--window-index", "xyz"] ] - + for args in invalidArgs { #expect(throws: (any Error).self) { _ = try ImageCommand.parse(args) } } } - + @Test("Memory efficiency with complex configurations", .tags(.memory)) func memoryEfficiencyComplexConfigurations() { // Test that complex configurations don't cause excessive memory usage @@ -498,12 +498,12 @@ struct AdvancedImageCaptureLogicTests { ["--mode", "multi", "--app", String(repeating: "LongAppName", count: 100)], ["--window-title", String(repeating: "VeryLongTitle", count: 200)], ["--path", String(repeating: "/very/long/path", count: 50)], - Array(repeating: ["--mode", "screen"], count: 100).flatMap { $0 } + Array(repeating: ["--mode", "screen"], count: 100).flatMap(\.self) ] - + for config in complexConfigs { do { - let _ = try ImageCommand.parse(config) + _ = try ImageCommand.parse(config) #expect(Bool(true)) // Command parsed successfully } catch { // Some may fail due to argument parsing limits, which is expected @@ -511,4 +511,4 @@ struct AdvancedImageCaptureLogicTests { } } } -} \ No newline at end of file +} diff --git a/peekaboo-cli/Tests/peekabooTests/ImageCommandTests.swift b/peekaboo-cli/Tests/peekabooTests/ImageCommandTests.swift index d9f5a91..589730e 100644 --- a/peekaboo-cli/Tests/peekabooTests/ImageCommandTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/ImageCommandTests.swift @@ -1,28 +1,27 @@ import ArgumentParser +import Foundation @testable import peekaboo import Testing -import Foundation @Suite("ImageCommand Tests", .tags(.imageCapture, .unit)) struct ImageCommandTests { - // MARK: - Test Data & Helpers - + private static let validFormats: [ImageFormat] = [.png, .jpg] private static let validCaptureModes: [CaptureMode] = [.screen, .window, .multi] private static let validCaptureFocus: [CaptureFocus] = [.background, .foreground] - + private static func createTestCommand(_ args: [String] = []) throws -> ImageCommand { - return try ImageCommand.parse(args) + try ImageCommand.parse(args) } - + // MARK: - Command Parsing Tests - + @Test("Basic command parsing with defaults", .tags(.fast)) func imageCommandParsing() throws { // Test basic command parsing let command = try ImageCommand.parse([]) - + // Verify defaults #expect(command.mode == nil) #expect(command.format == .png) @@ -31,36 +30,36 @@ struct ImageCommandTests { #expect(command.captureFocus == .background) #expect(command.jsonOutput == false) } - + @Test("Command with screen mode", .tags(.fast)) func imageCommandWithScreenMode() throws { // Test screen capture mode let command = try ImageCommand.parse(["--mode", "screen"]) - + #expect(command.mode == .screen) } - + @Test("Command with app specifier", .tags(.fast)) func imageCommandWithAppSpecifier() throws { // Test app-specific capture let command = try ImageCommand.parse([ "--app", "Finder" ]) - + #expect(command.mode == nil) // mode is optional #expect(command.app == "Finder") } - + @Test("Command with window title", .tags(.fast)) func imageCommandWithWindowTitle() throws { // Test window title capture let command = try ImageCommand.parse([ "--window-title", "Documents" ]) - + #expect(command.windowTitle == "Documents") } - + @Test("Command with output path", .tags(.fast)) func imageCommandWithOutput() throws { // Test output path specification @@ -68,89 +67,93 @@ struct ImageCommandTests { let command = try ImageCommand.parse([ "--path", outputPath ]) - + #expect(command.path == outputPath) } - + @Test("Command with format option", .tags(.fast)) func imageCommandWithFormat() throws { // Test format specification let command = try ImageCommand.parse([ "--format", "jpg" ]) - + #expect(command.format == .jpg) } - + @Test("Command with focus option", .tags(.fast)) func imageCommandWithFocus() throws { // Test focus option let command = try ImageCommand.parse([ "--capture-focus", "foreground" ]) - + #expect(command.captureFocus == .foreground) } - + @Test("Command with JSON output", .tags(.fast)) func imageCommandWithJSONOutput() throws { // Test JSON output flag let command = try ImageCommand.parse([ "--json-output" ]) - + #expect(command.jsonOutput == true) } - + @Test("Command with multi mode", .tags(.fast)) func imageCommandWithMultiMode() throws { // Test multi capture mode let command = try ImageCommand.parse([ "--mode", "multi" ]) - + #expect(command.mode == .multi) } - + @Test("Command with screen index", .tags(.fast)) func imageCommandWithScreenIndex() throws { // Test screen index specification let command = try ImageCommand.parse([ "--screen-index", "1" ]) - + #expect(command.screenIndex == 1) } - + // MARK: - Parameterized Command Tests - - @Test("Various command combinations", - arguments: [ - (args: ["--mode", "screen", "--format", "png"], mode: CaptureMode.screen, format: ImageFormat.png), - (args: ["--mode", "window", "--format", "jpg"], mode: CaptureMode.window, format: ImageFormat.jpg), - (args: ["--mode", "multi", "--json-output"], mode: CaptureMode.multi, format: ImageFormat.png) - ]) + + @Test( + "Various command combinations", + arguments: [ + (args: ["--mode", "screen", "--format", "png"], mode: CaptureMode.screen, format: ImageFormat.png), + (args: ["--mode", "window", "--format", "jpg"], mode: CaptureMode.window, format: ImageFormat.jpg), + (args: ["--mode", "multi", "--json-output"], mode: CaptureMode.multi, format: ImageFormat.png) + ] + ) func commandCombinations(args: [String], mode: CaptureMode, format: ImageFormat) throws { let command = try ImageCommand.parse(args) #expect(command.mode == mode) #expect(command.format == format) } - - @Test("Invalid arguments throw errors", - arguments: [ - ["--mode", "invalid"], - ["--format", "bmp"], - ["--capture-focus", "neither"], - ["--screen-index", "abc"] - ]) + + @Test( + "Invalid arguments throw errors", + arguments: [ + ["--mode", "invalid"], + ["--format", "bmp"], + ["--capture-focus", "neither"], + ["--screen-index", "abc"] + ] + ) func invalidArguments(args: [String]) { #expect(throws: (any Error).self) { _ = try ImageCommand.parse(args) } } - + // MARK: - Model Tests - + @Test("SavedFile model creation", .tags(.fast)) func savedFileModel() { let savedFile = SavedFile( @@ -161,12 +164,12 @@ struct ImageCommandTests { window_index: nil, mime_type: "image/png" ) - + #expect(savedFile.path == "/tmp/screenshot.png") #expect(savedFile.item_label == "Screen 1") #expect(savedFile.mime_type == "image/png") } - + @Test("ImageCaptureData encoding", .tags(.fast)) func imageCaptureDataEncoding() throws { let savedFile = SavedFile( @@ -177,69 +180,69 @@ struct ImageCommandTests { window_index: nil, mime_type: "image/png" ) - + let captureData = ImageCaptureData(saved_files: [savedFile]) - + // Test JSON encoding let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let data = try encoder.encode(captureData) - - #expect(data.count > 0) - + + #expect(!data.isEmpty) + // Test decoding let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let decoded = try decoder.decode(ImageCaptureData.self, from: data) - + #expect(decoded.saved_files.count == 1) #expect(decoded.saved_files[0].path == "/tmp/test.png") } - + // MARK: - Enum Raw Value Tests - + @Test("CaptureMode raw values", .tags(.fast)) func captureModeRawValues() { #expect(CaptureMode.screen.rawValue == "screen") #expect(CaptureMode.window.rawValue == "window") #expect(CaptureMode.multi.rawValue == "multi") } - + @Test("ImageFormat raw values", .tags(.fast)) func imageFormatRawValues() { #expect(ImageFormat.png.rawValue == "png") #expect(ImageFormat.jpg.rawValue == "jpg") } - + @Test("CaptureFocus raw values", .tags(.fast)) func captureFocusRawValues() { #expect(CaptureFocus.background.rawValue == "background") #expect(CaptureFocus.foreground.rawValue == "foreground") } - + // MARK: - Mode Determination & Logic Tests - + @Test("Mode determination logic", .tags(.fast)) func modeDeterminationLogic() throws { // No mode, no app -> should default to screen let screenCommand = try ImageCommand.parse([]) #expect(screenCommand.mode == nil) #expect(screenCommand.app == nil) - + // No mode, with app -> should infer window mode in actual execution let windowCommand = try ImageCommand.parse(["--app", "Finder"]) #expect(windowCommand.mode == nil) #expect(windowCommand.app == "Finder") - + // Explicit mode should be preserved let explicitCommand = try ImageCommand.parse(["--mode", "multi"]) #expect(explicitCommand.mode == .multi) } - + @Test("Default values verification", .tags(.fast)) func defaultValues() throws { let command = try ImageCommand.parse([]) - + #expect(command.mode == nil) #expect(command.format == .png) #expect(command.path == nil) @@ -250,21 +253,25 @@ struct ImageCommandTests { #expect(command.captureFocus == .background) #expect(command.jsonOutput == false) } - - @Test("Screen index boundary values", - arguments: [-1, 0, 1, 99, Int.max]) + + @Test( + "Screen index boundary values", + arguments: [-1, 0, 1, 99, Int.max] + ) func screenIndexBoundaries(index: Int) throws { let command = try ImageCommand.parse(["--screen-index", String(index)]) #expect(command.screenIndex == index) } - - @Test("Window index boundary values", - arguments: [-1, 0, 1, 10, Int.max]) + + @Test( + "Window index boundary values", + arguments: [-1, 0, 1, 10, Int.max] + ) func windowIndexBoundaries(index: Int) throws { let command = try ImageCommand.parse(["--window-index", String(index)]) #expect(command.windowIndex == index) } - + @Test("Error handling for invalid combinations", .tags(.fast)) func invalidCombinations() { // Window capture without app should fail in execution @@ -283,9 +290,8 @@ struct ImageCommandTests { @Suite("ImageCommand Advanced Tests", .tags(.imageCapture, .integration)) struct ImageCommandAdvancedTests { - // MARK: - Complex Scenario Tests - + @Test("Complex command with multiple options", .tags(.fast)) func complexCommand() throws { let command = try ImageCommand.parse([ @@ -298,7 +304,7 @@ struct ImageCommandAdvancedTests { "--capture-focus", "foreground", "--json-output" ]) - + #expect(command.mode == .window) #expect(command.app == "Safari") #expect(command.windowTitle == "Home") @@ -308,11 +314,11 @@ struct ImageCommandAdvancedTests { #expect(command.captureFocus == .foreground) #expect(command.jsonOutput == true) } - + @Test("Command help text contains all options", .tags(.fast)) func commandHelpText() { let helpText = ImageCommand.helpMessage() - + // Verify key options are documented #expect(helpText.contains("--mode")) #expect(helpText.contains("--app")) @@ -322,67 +328,71 @@ struct ImageCommandAdvancedTests { #expect(helpText.contains("--capture-focus")) #expect(helpText.contains("--json-output")) } - + @Test("Command configuration", .tags(.fast)) func commandConfiguration() { let config = ImageCommand.configuration - + #expect(config.commandName == "image") #expect(config.abstract.contains("Capture")) } - - @Test("Window specifier combinations", - arguments: [ - (app: "Safari", title: "Home", index: nil), - (app: "Finder", title: nil, index: 0), - (app: "Terminal", title: nil, index: nil) - ]) + + @Test( + "Window specifier combinations", + arguments: [ + (app: "Safari", title: "Home", index: nil), + (app: "Finder", title: nil, index: 0), + (app: "Terminal", title: nil, index: nil) + ] + ) func windowSpecifierCombinations(app: String, title: String?, index: Int?) throws { var args = ["--app", app] - - if let title = title { + + if let title { args.append(contentsOf: ["--window-title", title]) } - - if let index = index { + + if let index { args.append(contentsOf: ["--window-index", String(index)]) } - + let command = try ImageCommand.parse(args) - + #expect(command.app == app) #expect(command.windowTitle == title) #expect(command.windowIndex == index) } - - @Test("Path expansion handling", - arguments: [ - "~/Desktop/screenshot.png", - "/tmp/test.png", - "./relative/path.png", - "/path with spaces/image.png" - ]) + + @Test( + "Path expansion handling", + arguments: [ + "~/Desktop/screenshot.png", + "/tmp/test.png", + "./relative/path.png", + "/path with spaces/image.png" + ] + ) func pathExpansion(path: String) throws { let command = try ImageCommand.parse(["--path", path]) #expect(command.path == path) } - + @Test("FileHandleTextOutputStream functionality", .tags(.fast)) func fileHandleTextOutputStream() { // Test the custom text output stream let pipe = Pipe() var stream = FileHandleTextOutputStream(pipe.fileHandleForWriting) - + let testString = "Test output" stream.write(testString) pipe.fileHandleForWriting.closeFile() - + let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) - + #expect(output == testString) } - + @Test("Command validation edge cases", .tags(.fast)) func commandValidationEdgeCases() { // Test very long paths @@ -393,7 +403,7 @@ struct ImageCommandAdvancedTests { } catch { Issue.record("Should handle long paths gracefully") } - + // Test unicode in paths let unicodePath = "/tmp/测试/スクリーン.png" do { @@ -403,20 +413,20 @@ struct ImageCommandAdvancedTests { Issue.record("Should handle unicode paths") } } - + @Test("MIME type assignment logic", .tags(.fast)) - func mimeTypeAssignment() { + func mimeTypeAssignment() throws { // Test MIME type logic for different formats - let pngCommand = try! ImageCommand.parse(["--format", "png"]) + let pngCommand = try ImageCommand.parse(["--format", "png"]) #expect(pngCommand.format == .png) - - let jpgCommand = try! ImageCommand.parse(["--format", "jpg"]) + + let jpgCommand = try ImageCommand.parse(["--format", "jpg"]) #expect(jpgCommand.format == .jpg) - + // Verify MIME types would be assigned correctly // (This logic is in the SavedFile creation during actual capture) } - + @Test("Argument parsing stress test", .tags(.performance)) func argumentParsingStressTest() { // Test parsing performance with many arguments @@ -429,7 +439,7 @@ struct ImageCommandAdvancedTests { "--capture-focus", "foreground", "--json-output" ] - + do { let command = try ImageCommand.parse(args) #expect(command.mode == .multi) @@ -438,17 +448,19 @@ struct ImageCommandAdvancedTests { Issue.record("Should handle complex argument parsing") } } - - @Test("Command option combinations validation", - arguments: [ - (["--mode", "screen"], true), - (["--mode", "window", "--app", "Finder"], true), - (["--mode", "multi"], true), - (["--app", "Safari"], true), - (["--window-title", "Test"], true), - (["--screen-index", "0"], true), - (["--window-index", "0"], true) - ]) + + @Test( + "Command option combinations validation", + arguments: [ + (["--mode", "screen"], true), + (["--mode", "window", "--app", "Finder"], true), + (["--mode", "multi"], true), + (["--app", "Safari"], true), + (["--window-title", "Test"], true), + (["--screen-index", "0"], true), + (["--window-index", "0"], true) + ] + ) func commandOptionCombinations(args: [String], shouldParse: Bool) { do { let command = try ImageCommand.parse(args) @@ -458,4 +470,4 @@ struct ImageCommandAdvancedTests { #expect(shouldParse == false) } } -} \ No newline at end of file +} diff --git a/peekaboo-cli/Tests/peekabooTests/JSONOutputTests.swift b/peekaboo-cli/Tests/peekabooTests/JSONOutputTests.swift index e8adb35..9c23d1e 100644 --- a/peekaboo-cli/Tests/peekabooTests/JSONOutputTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/JSONOutputTests.swift @@ -1,12 +1,11 @@ +import Foundation @testable import peekaboo import Testing -import Foundation @Suite("JSONOutput Tests", .tags(.jsonOutput, .unit)) struct JSONOutputTests { - // MARK: - AnyCodable Tests - + @Test("AnyCodable encoding with various types", .tags(.fast)) func anyCodableEncodingVariousTypes() throws { // Test string @@ -14,19 +13,19 @@ struct JSONOutputTests { let stringData = try JSONEncoder().encode(stringValue) let stringResult = try JSONSerialization.jsonObject(with: stringData) as? String #expect(stringResult == "test string") - + // Test number let numberValue = AnyCodable(42) let numberData = try JSONEncoder().encode(numberValue) let numberResult = try JSONSerialization.jsonObject(with: numberData) as? Int #expect(numberResult == 42) - + // Test boolean let boolValue = AnyCodable(true) let boolData = try JSONEncoder().encode(boolValue) let boolResult = try JSONSerialization.jsonObject(with: boolData) as? Bool #expect(boolResult == true) - + // Test null (using optional nil) let nilValue: String? = nil let nilAnyCodable = AnyCodable(nilValue as Any) @@ -34,7 +33,7 @@ struct JSONOutputTests { let nilString = String(data: nilData, encoding: .utf8) #expect(nilString == "null") } - + @Test("AnyCodable with nested structures", .tags(.fast)) func anyCodableNestedStructures() throws { // Test array @@ -42,7 +41,7 @@ struct JSONOutputTests { let arrayData = try JSONEncoder().encode(arrayValue) let arrayResult = try JSONSerialization.jsonObject(with: arrayData) as? [Int] #expect(arrayResult == [1, 2, 3]) - + // Test dictionary let dictValue = AnyCodable(["key": "value", "number": 42]) let dictData = try JSONEncoder().encode(dictValue) @@ -50,21 +49,22 @@ struct JSONOutputTests { #expect(dictResult?["key"] as? String == "value") #expect(dictResult?["number"] as? Int == 42) } - + @Test("AnyCodable decoding", .tags(.fast)) func anyCodableDecoding() throws { // Test decoding from JSON - let jsonData = #"{"string": "test", "number": 42, "bool": true, "null": null}"#.data(using: .utf8)! + let jsonString = #"{"string": "test", "number": 42, "bool": true, "null": null}"# + let jsonData = Data(jsonString.utf8) let decoded = try JSONDecoder().decode([String: AnyCodable].self, from: jsonData) - + #expect(decoded["string"]?.value as? String == "test") #expect(decoded["number"]?.value as? Int == 42) #expect(decoded["bool"]?.value as? Bool == true) #expect(decoded["null"]?.value == nil) } - + // MARK: - AnyEncodable Tests - + @Test("AnyEncodable with custom types", .tags(.fast)) func anyEncodableCustomTypes() throws { // Test with ApplicationInfo @@ -75,21 +75,21 @@ struct JSONOutputTests { is_active: true, window_count: 2 ) - + // Test encoding through AnyCodable instead let anyCodable = AnyCodable(appInfo) let data = try JSONEncoder().encode(anyCodable) let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] - + #expect(json?["app_name"] as? String == "Test App") #expect(json?["bundle_id"] as? String == "com.test.app") #expect(json?["pid"] as? Int32 == 1234) #expect(json?["is_active"] as? Bool == true) #expect(json?["window_count"] as? Int == 2) } - + // MARK: - JSON Output Function Tests - + @Test("outputJSON function with success data", .tags(.fast)) func outputJSONSuccess() throws { // Test data @@ -102,37 +102,37 @@ struct JSONOutputTests { window_count: 1 ) ]) - + // Test JSON serialization directly without capturing stdout let encoder = JSONEncoder() let data = try encoder.encode(testData) let jsonString = String(data: data, encoding: .utf8) ?? "" - + // Verify JSON structure #expect(jsonString.contains("Finder")) #expect(jsonString.contains("com.apple.finder")) #expect(!jsonString.isEmpty) } - + @Test("CodableJSONResponse structure", .tags(.fast)) func codableJSONResponseStructure() throws { let testData = ["test": "value"] let response = CodableJSONResponse( - success: true, - data: testData, - messages: nil, + success: true, + data: testData, + messages: nil, debug_logs: [] ) - + let encoder = JSONEncoder() let data = try encoder.encode(response) let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] - + #expect(json?["success"] as? Bool == true) #expect((json?["data"] as? [String: Any])?["test"] as? String == "value") #expect(json?["error"] == nil) } - + @Test("Error output JSON formatting", .tags(.fast)) func errorOutputJSONFormatting() throws { // Test error JSON structure directly @@ -141,27 +141,27 @@ struct JSONOutputTests { code: .APP_NOT_FOUND, details: "Additional error details" ) - + let response = JSONResponse( success: false, data: nil, messages: nil, error: errorInfo ) - + let encoder = JSONEncoder() let data = try encoder.encode(response) let jsonString = String(data: data, encoding: .utf8) ?? "" - + // Verify error JSON structure #expect(jsonString.contains("\"success\":false") || jsonString.contains("\"success\": false")) #expect(jsonString.contains("\"error\"")) #expect(jsonString.contains("Test error message")) #expect(jsonString.contains("APP_NOT_FOUND")) } - + // MARK: - Edge Cases and Error Handling - + @Test("AnyCodable with complex nested data", .tags(.fast)) func anyCodableComplexNestedData() throws { let complexData: [String: Any] = [ @@ -177,15 +177,15 @@ struct JSONOutputTests { ] ] ] - + let anyCodable = AnyCodable(complexData) let encoded = try JSONEncoder().encode(anyCodable) let decoded = try JSONSerialization.jsonObject(with: encoded) as? [String: Any] - + #expect(decoded?["simple"] as? String == "string") #expect((decoded?["nested"] as? [String: Any]) != nil) } - + @Test("JSON encoding performance with large data", .tags(.performance)) func jsonEncodingPerformance() throws { // Create large dataset @@ -195,58 +195,56 @@ struct JSONOutputTests { app_name: "App \(index)", bundle_id: "com.test.app\(index)", pid: Int32(1000 + index), - is_active: index % 2 == 0, + is_active: index.isMultiple(of: 2), window_count: index % 10 ) largeAppList.append(appInfo) } - + let data = ApplicationListData(applications: largeAppList) - + // Measure encoding performance let startTime = CFAbsoluteTimeGetCurrent() let encoded = try JSONEncoder().encode(data) let encodingTime = CFAbsoluteTimeGetCurrent() - startTime - - #expect(encoded.count > 0) + + #expect(!encoded.isEmpty) #expect(encodingTime < 1.0) // Should encode within 1 second } - + @Test("Thread safety of JSON operations", .tags(.concurrency)) func threadSafetyJSONOperations() async { await withTaskGroup(of: Bool.self) { group in - for i in 0..<10 { + for index in 0..<10 { group.addTask { do { let appInfo = ApplicationInfo( - app_name: "App \(i)", - bundle_id: "com.test.app\(i)", - pid: Int32(1000 + i), + app_name: "App \(index)", + bundle_id: "com.test.app\(index)", + pid: Int32(1000 + index), is_active: true, window_count: 1 ) - + // Test encoding through AnyCodable instead - let anyCodable = AnyCodable(appInfo) - let _ = try JSONEncoder().encode(anyCodable) + let anyCodable = AnyCodable(appInfo) + _ = try JSONEncoder().encode(anyCodable) return true } catch { return false } } } - + var successCount = 0 - for await success in group { - if success { - successCount += 1 - } + for await success in group where success { + successCount += 1 } - + #expect(successCount == 10) } } - + @Test("Memory usage with repeated JSON operations", .tags(.memory)) func memoryUsageJSONOperations() { // Test memory doesn't grow excessively with repeated JSON operations @@ -258,16 +256,16 @@ struct JSONOutputTests { is_active: true, window_count: 1 ) - + do { let encoded = try JSONEncoder().encode(data) - #expect(encoded.count > 0) + #expect(!encoded.isEmpty) } catch { Issue.record("JSON encoding should not fail: \(error)") } } } - + @Test("Error code enum completeness", .tags(.fast)) func errorCodeEnumCompleteness() { // Test that all error codes have proper raw values @@ -282,10 +280,10 @@ struct JSONOutputTests { .INVALID_ARGUMENT, .UNKNOWN_ERROR ] - + for errorCode in errorCodes { #expect(!errorCode.rawValue.isEmpty) - #expect(errorCode.rawValue.allSatisfy { $0.isASCII }) + #expect(errorCode.rawValue.allSatisfy(\.isASCII)) } } } @@ -294,31 +292,30 @@ struct JSONOutputTests { @Suite("JSON Output Format Validation", .tags(.jsonOutput, .integration)) struct JSONOutputFormatValidationTests { - @Test("MCP protocol compliance", .tags(.integration)) func mcpProtocolCompliance() throws { // Test that JSON output follows MCP protocol format let testData = ApplicationListData(applications: []) let response = CodableJSONResponse( - success: true, - data: testData, - messages: nil, + success: true, + data: testData, + messages: nil, debug_logs: [] ) - + let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let data = try encoder.encode(response) - + // Verify it's valid JSON let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] #expect(json != nil) // JSON was successfully created - + // Verify required MCP fields #expect(json?["success"] != nil) #expect(json?["data"] != nil) } - + @Test("Snake case conversion consistency", .tags(.fast)) func snakeCaseConversionConsistency() throws { let appInfo = ApplicationInfo( @@ -328,25 +325,25 @@ struct JSONOutputFormatValidationTests { is_active: true, window_count: 2 ) - + let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let data = try encoder.encode(appInfo) let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] - + // Verify snake_case conversion #expect(json?["app_name"] != nil) #expect(json?["bundle_id"] != nil) #expect(json?["is_active"] != nil) #expect(json?["window_count"] != nil) - + // Verify no camelCase keys exist #expect(json?["appName"] == nil) #expect(json?["bundleId"] == nil) #expect(json?["isActive"] == nil) #expect(json?["windowCount"] == nil) } - + @Test("Large data structure serialization", .tags(.performance)) func largeDataStructureSerialization() throws { // Create a complex data structure @@ -357,11 +354,11 @@ struct JSONOutputFormatValidationTests { window_id: UInt32(1000 + index), window_index: index, bounds: WindowBounds(xCoordinate: index * 10, yCoordinate: index * 10, width: 800, height: 600), - is_on_screen: index % 2 == 0 + is_on_screen: index.isMultiple(of: 2) ) windows.append(window) } - + let windowData = WindowListData( windows: windows, target_application_info: TargetApplicationInfo( @@ -370,16 +367,16 @@ struct JSONOutputFormatValidationTests { pid: 1234 ) ) - + let startTime = CFAbsoluteTimeGetCurrent() let encoded = try JSONEncoder().encode(windowData) let duration = CFAbsoluteTimeGetCurrent() - startTime - - #expect(encoded.count > 0) + + #expect(!encoded.isEmpty) #expect(duration < 0.5) // Should complete within 500ms - + // Verify the JSON is valid - let _ = try JSONSerialization.jsonObject(with: encoded) + _ = try JSONSerialization.jsonObject(with: encoded) #expect(Bool(true)) // JSON was successfully created } -} \ No newline at end of file +} diff --git a/peekaboo-cli/Tests/peekabooTests/ListCommandTests.swift b/peekaboo-cli/Tests/peekabooTests/ListCommandTests.swift index 5ddf80e..ceff747 100644 --- a/peekaboo-cli/Tests/peekabooTests/ListCommandTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/ListCommandTests.swift @@ -1,12 +1,12 @@ import ArgumentParser +import Foundation @testable import peekaboo import Testing -import Foundation @Suite("ListCommand Tests", .tags(.unit)) struct ListCommandTests { // MARK: - Command Parsing Tests - + @Test("ListCommand has correct subcommands", .tags(.fast)) func listCommandSubcommands() throws { // Test that ListCommand has the expected subcommands @@ -15,31 +15,31 @@ struct ListCommandTests { #expect(ListCommand.configuration.subcommands.contains { $0 == WindowsSubcommand.self }) #expect(ListCommand.configuration.subcommands.contains { $0 == ServerStatusSubcommand.self }) } - + @Test("AppsSubcommand parsing with defaults", .tags(.fast)) func appsSubcommandParsing() throws { // Test parsing apps subcommand let command = try AppsSubcommand.parse([]) #expect(command.jsonOutput == false) } - + @Test("AppsSubcommand with JSON output flag", .tags(.fast)) func appsSubcommandWithJSONOutput() throws { // Test apps subcommand with JSON flag let command = try AppsSubcommand.parse(["--json-output"]) #expect(command.jsonOutput == true) } - + @Test("WindowsSubcommand parsing with required app", .tags(.fast)) func windowsSubcommandParsing() throws { // Test parsing windows subcommand with required app let command = try WindowsSubcommand.parse(["--app", "Finder"]) - + #expect(command.app == "Finder") #expect(command.jsonOutput == false) #expect(command.includeDetails == nil) } - + @Test("WindowsSubcommand with detail options", .tags(.fast)) func windowsSubcommandWithDetails() throws { // Test windows subcommand with detail options @@ -47,11 +47,11 @@ struct ListCommandTests { "--app", "Finder", "--include-details", "bounds,ids" ]) - + #expect(command.app == "Finder") #expect(command.includeDetails == "bounds,ids") } - + @Test("WindowsSubcommand requires app parameter", .tags(.fast)) func windowsSubcommandMissingApp() { // Test that windows subcommand requires app @@ -59,29 +59,31 @@ struct ListCommandTests { try WindowsSubcommand.parse([]) } } - + // MARK: - Parameterized Command Tests - - @Test("WindowsSubcommand detail parsing", - arguments: [ - "off_screen", - "bounds", - "ids", - "off_screen,bounds", - "bounds,ids", - "off_screen,bounds,ids" - ]) + + @Test( + "WindowsSubcommand detail parsing", + arguments: [ + "off_screen", + "bounds", + "ids", + "off_screen,bounds", + "bounds,ids", + "off_screen,bounds,ids" + ] + ) func windowsDetailParsing(details: String) throws { let command = try WindowsSubcommand.parse([ "--app", "Safari", "--include-details", details ]) - + #expect(command.includeDetails == details) } - + // MARK: - Data Structure Tests - + @Test("ApplicationInfo JSON encoding", .tags(.fast)) func applicationInfoEncoding() throws { // Test ApplicationInfo JSON encoding @@ -92,13 +94,13 @@ struct ListCommandTests { is_active: true, window_count: 5 ) - + let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase - + let data = try encoder.encode(appInfo) let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] - + #expect(json != nil) #expect(json?["app_name"] as? String == "Finder") #expect(json?["bundle_id"] as? String == "com.apple.finder") @@ -106,7 +108,7 @@ struct ListCommandTests { #expect(json?["is_active"] as? Bool == true) #expect(json?["window_count"] as? Int == 5) } - + @Test("ApplicationListData JSON encoding", .tags(.fast)) func applicationListDataEncoding() throws { // Test ApplicationListData JSON encoding @@ -128,18 +130,18 @@ struct ListCommandTests { ) ] ) - + let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase - + let data = try encoder.encode(appData) let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] - + #expect(json != nil) let apps = json?["applications"] as? [[String: Any]] #expect(apps?.count == 2) } - + @Test("WindowInfo JSON encoding", .tags(.fast)) func windowInfoEncoding() throws { // Test WindowInfo JSON encoding @@ -150,25 +152,25 @@ struct ListCommandTests { bounds: WindowBounds(xCoordinate: 100, yCoordinate: 200, width: 800, height: 600), is_on_screen: true ) - + let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase - + let data = try encoder.encode(windowInfo) let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] - + #expect(json != nil) #expect(json?["window_title"] as? String == "Documents") #expect(json?["window_id"] as? UInt32 == 1001) #expect(json?["is_on_screen"] as? Bool == true) - + let bounds = json?["bounds"] as? [String: Any] #expect(bounds?["x_coordinate"] as? Int == 100) #expect(bounds?["y_coordinate"] as? Int == 200) #expect(bounds?["width"] as? Int == 800) #expect(bounds?["height"] as? Int == 600) } - + @Test("WindowListData JSON encoding", .tags(.fast)) func windowListDataEncoding() throws { // Test WindowListData JSON encoding @@ -188,25 +190,25 @@ struct ListCommandTests { pid: 123 ) ) - + let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase - + let data = try encoder.encode(windowData) let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] - + #expect(json != nil) - + let windows = json?["windows"] as? [[String: Any]] #expect(windows?.count == 1) - + let targetApp = json?["target_application_info"] as? [String: Any] #expect(targetApp?["app_name"] as? String == "Finder") #expect(targetApp?["bundle_id"] as? String == "com.apple.finder") } - + // MARK: - Window Detail Option Tests - + @Test("WindowDetailOption raw values", .tags(.fast)) func windowDetailOptionRawValues() { // Test window detail option values @@ -214,14 +216,14 @@ struct ListCommandTests { #expect(WindowDetailOption.bounds.rawValue == "bounds") #expect(WindowDetailOption.ids.rawValue == "ids") } - + // MARK: - Window Specifier Tests - + @Test("WindowSpecifier with title", .tags(.fast)) func windowSpecifierTitle() { // Test window specifier with title let specifier = WindowSpecifier.title("Documents") - + switch specifier { case let .title(title): #expect(title == "Documents") @@ -229,12 +231,12 @@ struct ListCommandTests { Issue.record("Expected title specifier") } } - + @Test("WindowSpecifier with index", .tags(.fast)) func windowSpecifierIndex() { // Test window specifier with index let specifier = WindowSpecifier.index(0) - + switch specifier { case let .index(index): #expect(index == 0) @@ -242,11 +244,13 @@ struct ListCommandTests { Issue.record("Expected index specifier") } } - + // MARK: - Performance Tests - - @Test("ApplicationListData encoding performance", - arguments: [10, 50, 100, 200]) + + @Test( + "ApplicationListData encoding performance", + arguments: [10, 50, 100, 200] + ) func applicationListEncodingPerformance(appCount: Int) throws { // Test performance of encoding many applications let apps = (0.. 0) + #expect(!data.isEmpty) } } @@ -273,37 +277,38 @@ struct ListCommandTests { @Suite("ListCommand Advanced Tests", .tags(.integration)) struct ListCommandAdvancedTests { - @Test("ServerStatusSubcommand parsing", .tags(.fast)) func serverStatusSubcommandParsing() throws { let command = try ServerStatusSubcommand.parse([]) #expect(command.jsonOutput == false) - + let commandWithJSON = try ServerStatusSubcommand.parse(["--json-output"]) #expect(commandWithJSON.jsonOutput == true) } - + @Test("Command help messages", .tags(.fast)) func commandHelpMessages() { let listHelp = ListCommand.helpMessage() #expect(listHelp.contains("List")) - + let appsHelp = AppsSubcommand.helpMessage() #expect(appsHelp.contains("running applications")) - + let windowsHelp = WindowsSubcommand.helpMessage() #expect(windowsHelp.contains("windows")) - + let statusHelp = ServerStatusSubcommand.helpMessage() #expect(statusHelp.contains("status")) } - - @Test("Complex window info structures", - arguments: [ - (title: "Main Window", id: 1001, onScreen: true), - (title: "Hidden Window", id: 2001, onScreen: false), - (title: "Minimized", id: 3001, onScreen: false) - ]) + + @Test( + "Complex window info structures", + arguments: [ + (title: "Main Window", id: 1001, onScreen: true), + (title: "Hidden Window", id: 2001, onScreen: false), + (title: "Minimized", id: 3001, onScreen: false) + ] + ) func complexWindowInfo(title: String, id: UInt32, onScreen: Bool) throws { let windowInfo = WindowInfo( window_title: title, @@ -312,27 +317,29 @@ struct ListCommandAdvancedTests { bounds: nil, is_on_screen: onScreen ) - + let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let data = try encoder.encode(windowInfo) - + let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let decoded = try decoder.decode(WindowInfo.self, from: data) - + #expect(decoded.window_title == title) #expect(decoded.window_id == id) #expect(decoded.is_on_screen == onScreen) } - - @Test("Application state combinations", - arguments: [ - (active: true, windowCount: 5), - (active: false, windowCount: 0), - (active: true, windowCount: 0), - (active: false, windowCount: 10) - ]) + + @Test( + "Application state combinations", + arguments: [ + (active: true, windowCount: 5), + (active: false, windowCount: 0), + (active: true, windowCount: 0), + (active: false, windowCount: 10) + ] + ) func applicationStates(active: Bool, windowCount: Int) { let appInfo = ApplicationInfo( app_name: "TestApp", @@ -341,34 +348,34 @@ struct ListCommandAdvancedTests { is_active: active, window_count: windowCount ) - + #expect(appInfo.is_active == active) #expect(appInfo.window_count == windowCount) - + // Logical consistency checks if windowCount > 0 { // Apps with windows can be active or inactive #expect(appInfo.window_count > 0) } } - + @Test("Server permissions data encoding", .tags(.fast)) func serverPermissionsEncoding() throws { let permissions = ServerPermissions( screen_recording: true, accessibility: false ) - + let statusData = ServerStatusData(permissions: permissions) - + let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let data = try encoder.encode(statusData) - + let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] let permsJson = json?["permissions"] as? [String: Any] - + #expect(permsJson?["screen_recording"] as? Bool == true) #expect(permsJson?["accessibility"] as? Bool == false) } -} \ No newline at end of file +} diff --git a/peekaboo-cli/Tests/peekabooTests/LoggerTests.swift b/peekaboo-cli/Tests/peekabooTests/LoggerTests.swift index 52e7ad4..d0687c9 100644 --- a/peekaboo-cli/Tests/peekabooTests/LoggerTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/LoggerTests.swift @@ -1,322 +1,319 @@ +import Foundation @testable import peekaboo import Testing -import Foundation @Suite("Logger Tests", .tags(.logger, .unit), .serialized) struct LoggerTests { - // MARK: - Basic Functionality Tests - + @Test("Logger singleton instance", .tags(.fast)) func loggerSingletonInstance() { let logger1 = Logger.shared let logger2 = Logger.shared - + // Should be the same instance #expect(logger1 === logger2) } - + @Test("JSON output mode switching", .tags(.fast)) func jsonOutputModeSwitching() { let logger = Logger.shared - + // Test setting JSON mode logger.setJsonOutputMode(true) // Cannot directly test internal state, but verify no crash - + logger.setJsonOutputMode(false) // Cannot directly test internal state, but verify no crash - + // Test multiple switches for _ in 1...10 { logger.setJsonOutputMode(true) logger.setJsonOutputMode(false) } } - + @Test("Debug log message recording", .tags(.fast)) func debugLogMessageRecording() async { let logger = Logger.shared - + // Enable JSON mode and clear logs logger.setJsonOutputMode(true) logger.clearDebugLogs() - + // Wait for mode setting to complete try? await Task.sleep(nanoseconds: 10_000_000) // 10ms - + // Record some debug messages logger.debug("Test debug message 1") logger.debug("Test debug message 2") logger.info("Test info message") logger.error("Test error message") - + // Wait for logging to complete try? await Task.sleep(nanoseconds: 10_000_000) // 10ms - + let logs = logger.getDebugLogs() - + // Should have exactly the messages we added #expect(logs.count == 4) - + // Verify messages are stored #expect(logs.contains { $0.contains("Test debug message 1") }) #expect(logs.contains { $0.contains("Test debug message 2") }) #expect(logs.contains { $0.contains("Test info message") }) #expect(logs.contains { $0.contains("Test error message") }) - + // Reset for other tests logger.setJsonOutputMode(false) } - + @Test("Debug logs retrieval and format", .tags(.fast)) func debugLogsRetrievalAndFormat() async { let logger = Logger.shared - + // Enable JSON mode and clear logs logger.setJsonOutputMode(true) logger.clearDebugLogs() - + // Wait for setup to complete try? await Task.sleep(nanoseconds: 10_000_000) // 10ms - + // Add test messages logger.debug("Debug test") logger.info("Info test") logger.warn("Warning test") logger.error("Error test") - + // Wait for logging to complete try? await Task.sleep(nanoseconds: 10_000_000) // 10ms - + let logs = logger.getDebugLogs() - + // Should have exactly our messages #expect(logs.count == 4) - + // Verify log format includes level prefixes #expect(logs.contains { $0.contains("Debug test") }) #expect(logs.contains { $0.contains("INFO: Info test") }) #expect(logs.contains { $0.contains("WARN: Warning test") }) #expect(logs.contains { $0.contains("ERROR: Error test") }) - + // Reset for other tests logger.setJsonOutputMode(false) } - + // MARK: - Thread Safety Tests - + @Test("Concurrent logging operations", .tags(.concurrency)) func concurrentLoggingOperations() async { let logger = Logger.shared - + // Enable JSON mode and clear logs logger.setJsonOutputMode(true) logger.clearDebugLogs() - + // Wait for setup try? await Task.sleep(nanoseconds: 10_000_000) // 10ms - + let initialCount = logger.getDebugLogs().count - + await withTaskGroup(of: Void.self) { group in // Create multiple concurrent logging tasks - for i in 0..<10 { + for index in 0..<10 { group.addTask { - logger.debug("Concurrent message \(i)") - logger.info("Concurrent info \(i)") - logger.error("Concurrent error \(i)") + logger.debug("Concurrent message \(index)") + logger.info("Concurrent info \(index)") + logger.error("Concurrent error \(index)") } } } - + // Wait for logging to complete try? await Task.sleep(nanoseconds: 50_000_000) // 50ms - + let finalLogs = logger.getDebugLogs() - + // Should have all messages (30 new messages) #expect(finalLogs.count >= initialCount + 30) - + // Verify no corruption by checking for our messages let recentLogs = finalLogs.suffix(30) var foundMessages = 0 - for i in 0..<10 { - if recentLogs.contains(where: { $0.contains("Concurrent message \(i)") }) { - foundMessages += 1 - } + for index in 0..<10 where recentLogs.contains(where: { $0.contains("Concurrent message \(index)") }) { + foundMessages += 1 } - + // Should find most or all messages (allowing for some timing issues) #expect(foundMessages >= 7) - + // Reset logger.setJsonOutputMode(false) } - + @Test("Concurrent mode switching and logging", .tags(.concurrency)) func concurrentModeSwitchingAndLogging() async { let logger = Logger.shared - + await withTaskGroup(of: Void.self) { group in // Task 1: Rapid mode switching group.addTask { - for i in 0..<50 { - logger.setJsonOutputMode(i % 2 == 0) + for index in 0..<50 { + logger.setJsonOutputMode(index.isMultiple(of: 2)) } } - + // Task 2: Continuous logging during mode switches group.addTask { - for i in 0..<100 { - logger.debug("Mode switch test \(i)") + for index in 0..<100 { + logger.debug("Mode switch test \(index)") } } - + // Task 3: Log retrieval during operations group.addTask { for _ in 0..<10 { let logs = logger.getDebugLogs() - #expect(logs.count >= 0) // Should not crash + // Logs count is always non-negative } } } - + // Should complete without crashes #expect(Bool(true)) } - + // MARK: - Memory Management Tests - + @Test("Memory usage with extensive logging", .tags(.memory)) func memoryUsageExtensiveLogging() async { let logger = Logger.shared - + // Enable JSON mode and clear logs logger.setJsonOutputMode(true) logger.clearDebugLogs() - + // Wait for setup try? await Task.sleep(nanoseconds: 10_000_000) // 10ms - + let initialCount = logger.getDebugLogs().count - + // Generate many log messages - for i in 1...100 { - logger.debug("Memory test message \(i)") - logger.info("Memory test info \(i)") - logger.error("Memory test error \(i)") + for index in 1...100 { + logger.debug("Memory test message \(index)") + logger.info("Memory test info \(index)") + logger.error("Memory test error \(index)") } - + // Wait for logging try? await Task.sleep(nanoseconds: 100_000_000) // 100ms - + let finalLogs = logger.getDebugLogs() - + // Should have accumulated messages #expect(finalLogs.count >= initialCount + 300) - + // Verify memory doesn't grow unbounded by checking we can still log logger.debug("Final test message") - + // Wait for final log try? await Task.sleep(nanoseconds: 10_000_000) // 10ms - + let postTestLogs = logger.getDebugLogs() #expect(postTestLogs.count > finalLogs.count) - + // Reset logger.setJsonOutputMode(false) } - + @Test("Debug logs array management", .tags(.fast)) func debugLogsArrayManagement() { let logger = Logger.shared - + // Test that logs are properly maintained let initialLogs = logger.getDebugLogs() - + // Add known messages logger.debug("Management test 1") logger.debug("Management test 2") - + let middleLogs = logger.getDebugLogs() #expect(middleLogs.count > initialLogs.count) - + // Add more messages logger.debug("Management test 3") logger.debug("Management test 4") - + let finalLogs = logger.getDebugLogs() #expect(finalLogs.count > middleLogs.count) - + // Verify recent messages are present #expect(finalLogs.last?.contains("Management test 4") == true) } - + // MARK: - Performance Tests - + @Test("Logging performance benchmark", .tags(.performance)) func loggingPerformanceBenchmark() { let logger = Logger.shared - + // Measure logging performance let messageCount = 1000 let startTime = CFAbsoluteTimeGetCurrent() - - for i in 1...messageCount { - logger.debug("Performance test message \(i)") + + for index in 1...messageCount { + logger.debug("Performance test message \(index)") } - + let duration = CFAbsoluteTimeGetCurrent() - startTime - + // Should be able to log 1000 messages quickly #expect(duration < 1.0) // Within 1 second - + // Verify all messages were logged let logs = logger.getDebugLogs() let performanceMessages = logs.filter { $0.contains("Performance test message") } #expect(performanceMessages.count >= messageCount) } - + @Test("Debug log retrieval performance", .tags(.performance)) func debugLogRetrievalPerformance() { let logger = Logger.shared - + // Add many messages first - for i in 1...100 { - logger.debug("Retrieval test \(i)") + for index in 1...100 { + logger.debug("Retrieval test \(index)") } - + // Measure retrieval performance let startTime = CFAbsoluteTimeGetCurrent() - + for _ in 1...10 { let logs = logger.getDebugLogs() - #expect(logs.count > 0) + #expect(!logs.isEmpty) } - + let duration = CFAbsoluteTimeGetCurrent() - startTime - + // Should be able to retrieve logs quickly even with many messages #expect(duration < 1.0) // Within 1 second for 10 retrievals } - + // MARK: - Edge Cases and Error Handling - + @Test("Logging with special characters", .tags(.fast)) func loggingWithSpecialCharacters() async { let logger = Logger.shared - + // Enable JSON mode and clear logs logger.setJsonOutputMode(true) logger.clearDebugLogs() - + // Wait for setup try? await Task.sleep(nanoseconds: 10_000_000) // 10ms - + let initialCount = logger.getDebugLogs().count - + // Test various special characters and unicode let specialMessages = [ "Test with emoji: 🚀 🎉 ✅", @@ -326,161 +323,161 @@ struct LoggerTests { "Test with JSON: {\"key\": \"value\", \"number\": 42}", "Test with special chars: @#$%^&*()_+-=[]{}|;':\",./<>?" ] - + for message in specialMessages { logger.debug(message) logger.info(message) logger.error(message) } - + // Wait for logging try? await Task.sleep(nanoseconds: 50_000_000) // 50ms - + let logs = logger.getDebugLogs() - + // Should have all messages #expect(logs.count >= initialCount + specialMessages.count * 3) - + // Verify special characters are preserved let recentLogs = logs.suffix(specialMessages.count * 3) for message in specialMessages { #expect(recentLogs.contains { $0.contains(message) }) } - + // Reset logger.setJsonOutputMode(false) } - + @Test("Logging with very long messages", .tags(.fast)) func loggingWithVeryLongMessages() async { let logger = Logger.shared - + // Enable JSON mode and clear logs for consistent testing logger.setJsonOutputMode(true) logger.clearDebugLogs() - + // Wait for setup try? await Task.sleep(nanoseconds: 10_000_000) // 10ms - + let initialCount = logger.getDebugLogs().count - + // Test very long messages let longMessage = String(repeating: "A", count: 1000) let veryLongMessage = String(repeating: "B", count: 10000) - + logger.debug(longMessage) logger.info(veryLongMessage) - + // Wait for logging try? await Task.sleep(nanoseconds: 10_000_000) // 10ms - + let logs = logger.getDebugLogs() - + // Should handle long messages without crashing #expect(logs.count >= initialCount + 2) - + // Verify long messages are stored (possibly truncated, but stored) let recentLogs = logs.suffix(2) #expect(recentLogs.contains { $0.contains("AAA") }) #expect(recentLogs.contains { $0.contains("BBB") }) - + // Reset logger.setJsonOutputMode(false) } - + @Test("Logging with nil and empty strings", .tags(.fast)) func loggingWithNilAndEmptyStrings() async { let logger = Logger.shared - + // Enable JSON mode and clear logs for consistent testing logger.setJsonOutputMode(true) logger.clearDebugLogs() - + // Wait for setup try? await Task.sleep(nanoseconds: 10_000_000) // 10ms - + let initialCount = logger.getDebugLogs().count - + // Test empty messages logger.debug("") logger.info("") logger.error("") - + // Test whitespace-only messages logger.debug(" ") logger.info("\\t\\n\\r") - + // Wait for logging try? await Task.sleep(nanoseconds: 10_000_000) // 10ms - + let logs = logger.getDebugLogs() - + // Should handle empty/whitespace messages gracefully #expect(logs.count >= initialCount + 5) - + // Reset logger.setJsonOutputMode(false) } - + // MARK: - Integration Tests - + @Test("Logger integration with JSON output mode", .tags(.integration)) func loggerIntegrationWithJSONMode() async { let logger = Logger.shared - + // Clear logs first logger.clearDebugLogs() - + // Test logging in JSON mode only (since non-JSON mode goes to stderr) logger.setJsonOutputMode(true) - + // Wait for mode setting try? await Task.sleep(nanoseconds: 10_000_000) // 10ms - + logger.debug("JSON mode message 1") logger.debug("JSON mode message 2") logger.debug("JSON mode message 3") - + // Wait for logging try? await Task.sleep(nanoseconds: 10_000_000) // 10ms - + let logs = logger.getDebugLogs() - + // Should have messages from JSON mode #expect(logs.contains { $0.contains("JSON mode message 1") }) #expect(logs.contains { $0.contains("JSON mode message 2") }) #expect(logs.contains { $0.contains("JSON mode message 3") }) - + // Reset logger.setJsonOutputMode(false) } - + @Test("Logger state consistency", .tags(.fast)) func loggerStateConsistency() async { let logger = Logger.shared - + // Clear logs and set JSON mode logger.setJsonOutputMode(true) logger.clearDebugLogs() - + // Wait for setup try? await Task.sleep(nanoseconds: 10_000_000) // 10ms - + // Test consistent JSON mode logging - for i in 1...10 { - logger.debug("State test \(i)") + for index in 1...10 { + logger.debug("State test \(index)") } - + // Wait for logging try? await Task.sleep(nanoseconds: 50_000_000) // 50ms - + let logs = logger.getDebugLogs() - + // Should maintain consistency let stateTestLogs = logs.filter { $0.contains("State test") } #expect(stateTestLogs.count >= 10) - + // Reset logger.setJsonOutputMode(false) } -} \ No newline at end of file +} diff --git a/peekaboo-cli/Tests/peekabooTests/ModelsTests.swift b/peekaboo-cli/Tests/peekabooTests/ModelsTests.swift index f021e0f..5790113 100644 --- a/peekaboo-cli/Tests/peekabooTests/ModelsTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/ModelsTests.swift @@ -1,65 +1,65 @@ +import CoreGraphics @testable import peekaboo import Testing -import CoreGraphics @Suite("Models Tests", .tags(.models, .unit)) struct ModelsTests { // MARK: - Enum Tests - + @Test("CaptureMode enum values and parsing", .tags(.fast)) func captureMode() { // Test CaptureMode enum values #expect(CaptureMode.screen.rawValue == "screen") #expect(CaptureMode.window.rawValue == "window") #expect(CaptureMode.multi.rawValue == "multi") - + // Test CaptureMode from string #expect(CaptureMode(rawValue: "screen") == .screen) #expect(CaptureMode(rawValue: "window") == .window) #expect(CaptureMode(rawValue: "multi") == .multi) #expect(CaptureMode(rawValue: "invalid") == nil) } - + @Test("ImageFormat enum values and parsing", .tags(.fast)) func imageFormat() { // Test ImageFormat enum values #expect(ImageFormat.png.rawValue == "png") #expect(ImageFormat.jpg.rawValue == "jpg") - + // Test ImageFormat from string #expect(ImageFormat(rawValue: "png") == .png) #expect(ImageFormat(rawValue: "jpg") == .jpg) #expect(ImageFormat(rawValue: "invalid") == nil) } - + @Test("CaptureFocus enum values and parsing", .tags(.fast)) func captureFocus() { // Test CaptureFocus enum values #expect(CaptureFocus.background.rawValue == "background") #expect(CaptureFocus.foreground.rawValue == "foreground") - + // Test CaptureFocus from string #expect(CaptureFocus(rawValue: "background") == .background) #expect(CaptureFocus(rawValue: "foreground") == .foreground) #expect(CaptureFocus(rawValue: "invalid") == nil) } - + @Test("WindowDetailOption enum values and parsing", .tags(.fast)) func windowDetailOption() { // Test WindowDetailOption enum values #expect(WindowDetailOption.off_screen.rawValue == "off_screen") #expect(WindowDetailOption.bounds.rawValue == "bounds") #expect(WindowDetailOption.ids.rawValue == "ids") - + // Test WindowDetailOption from string #expect(WindowDetailOption(rawValue: "off_screen") == .off_screen) #expect(WindowDetailOption(rawValue: "bounds") == .bounds) #expect(WindowDetailOption(rawValue: "ids") == .ids) #expect(WindowDetailOption(rawValue: "invalid") == nil) } - + // MARK: - Parameterized Enum Tests - + @Test("CaptureMode raw values are valid", .tags(.fast)) func captureModeRawValuesValid() { let validValues = ["screen", "window", "multi"] @@ -67,7 +67,7 @@ struct ModelsTests { #expect(CaptureMode(rawValue: rawValue) != nil) } } - + @Test("ImageFormat raw values are valid", .tags(.fast)) func imageFormatRawValuesValid() { let validValues = ["png", "jpg"] @@ -75,7 +75,7 @@ struct ModelsTests { #expect(ImageFormat(rawValue: rawValue) != nil) } } - + @Test("CaptureFocus raw values are valid", .tags(.fast)) func captureFocusRawValuesValid() { let validValues = ["background", "foreground"] @@ -83,19 +83,19 @@ struct ModelsTests { #expect(CaptureFocus(rawValue: rawValue) != nil) } } - + // MARK: - Model Structure Tests - + @Test("WindowBounds initialization and properties", .tags(.fast)) func windowBounds() { let bounds = WindowBounds(xCoordinate: 100, yCoordinate: 200, width: 1200, height: 800) - + #expect(bounds.xCoordinate == 100) #expect(bounds.yCoordinate == 200) #expect(bounds.width == 1200) #expect(bounds.height == 800) } - + @Test("SavedFile with all properties", .tags(.fast)) func savedFile() { let savedFile = SavedFile( @@ -106,7 +106,7 @@ struct ModelsTests { window_index: 0, mime_type: "image/png" ) - + #expect(savedFile.path == "/tmp/test.png") #expect(savedFile.item_label == "Screen 1") #expect(savedFile.window_title == "Safari - Main Window") @@ -114,7 +114,7 @@ struct ModelsTests { #expect(savedFile.window_index == 0) #expect(savedFile.mime_type == "image/png") } - + @Test("SavedFile with nil optional values", .tags(.fast)) func savedFileWithNilValues() { let savedFile = SavedFile( @@ -125,7 +125,7 @@ struct ModelsTests { window_index: nil, mime_type: "image/png" ) - + #expect(savedFile.path == "/tmp/screen.png") #expect(savedFile.item_label == nil) #expect(savedFile.window_title == nil) @@ -133,7 +133,7 @@ struct ModelsTests { #expect(savedFile.window_index == nil) #expect(savedFile.mime_type == "image/png") } - + @Test("ApplicationInfo initialization", .tags(.fast)) func applicationInfo() { let appInfo = ApplicationInfo( @@ -143,14 +143,14 @@ struct ModelsTests { is_active: true, window_count: 2 ) - + #expect(appInfo.app_name == "Safari") #expect(appInfo.bundle_id == "com.apple.Safari") #expect(appInfo.pid == 1234) #expect(appInfo.is_active == true) #expect(appInfo.window_count == 2) } - + @Test("WindowInfo with bounds", .tags(.fast)) func windowInfo() { let bounds = WindowBounds(xCoordinate: 100, yCoordinate: 100, width: 1200, height: 800) @@ -161,7 +161,7 @@ struct ModelsTests { bounds: bounds, is_on_screen: true ) - + #expect(windowInfo.window_title == "Safari - Main Window") #expect(windowInfo.window_id == 12345) #expect(windowInfo.window_index == 0) @@ -172,7 +172,7 @@ struct ModelsTests { #expect(windowInfo.bounds?.height == 800) #expect(windowInfo.is_on_screen == true) } - + @Test("TargetApplicationInfo", .tags(.fast)) func targetApplicationInfo() { let targetApp = TargetApplicationInfo( @@ -180,14 +180,14 @@ struct ModelsTests { bundle_id: "com.apple.Safari", pid: 1234 ) - + #expect(targetApp.app_name == "Safari") #expect(targetApp.bundle_id == "com.apple.Safari") #expect(targetApp.pid == 1234) } - + // MARK: - Collection Data Tests - + @Test("ApplicationListData contains applications", .tags(.fast)) func applicationListData() { let app1 = ApplicationInfo( @@ -197,7 +197,7 @@ struct ModelsTests { is_active: true, window_count: 2 ) - + let app2 = ApplicationInfo( app_name: "Terminal", bundle_id: "com.apple.Terminal", @@ -205,14 +205,14 @@ struct ModelsTests { is_active: false, window_count: 1 ) - + let appListData = ApplicationListData(applications: [app1, app2]) - + #expect(appListData.applications.count == 2) #expect(appListData.applications[0].app_name == "Safari") #expect(appListData.applications[1].app_name == "Terminal") } - + @Test("WindowListData with target application", .tags(.fast)) func windowListData() { let bounds = WindowBounds(xCoordinate: 100, yCoordinate: 100, width: 1200, height: 800) @@ -223,25 +223,25 @@ struct ModelsTests { bounds: bounds, is_on_screen: true ) - + let targetApp = TargetApplicationInfo( app_name: "Safari", bundle_id: "com.apple.Safari", pid: 1234 ) - + let windowListData = WindowListData( windows: [window], target_application_info: targetApp ) - + #expect(windowListData.windows.count == 1) #expect(windowListData.windows[0].window_title == "Safari - Main Window") #expect(windowListData.target_application_info.app_name == "Safari") #expect(windowListData.target_application_info.bundle_id == "com.apple.Safari") #expect(windowListData.target_application_info.pid == 1234) } - + @Test("ImageCaptureData with saved files", .tags(.fast)) func imageCaptureData() { let savedFile = SavedFile( @@ -252,30 +252,36 @@ struct ModelsTests { window_index: nil, mime_type: "image/png" ) - + let imageData = ImageCaptureData(saved_files: [savedFile]) - + #expect(imageData.saved_files.count == 1) #expect(imageData.saved_files[0].path == "/tmp/test.png") #expect(imageData.saved_files[0].item_label == "Screen 1") #expect(imageData.saved_files[0].mime_type == "image/png") } - + // MARK: - Error Tests - + @Test("CaptureError descriptions are user-friendly", .tags(.fast)) func captureErrorDescriptions() { #expect(CaptureError.noDisplaysAvailable.errorDescription == "No displays available for capture.") - #expect(CaptureError.screenRecordingPermissionDenied.errorDescription!.contains("Screen recording permission is required")) + #expect(CaptureError.screenRecordingPermissionDenied.errorDescription! + .contains("Screen recording permission is required") + ) #expect(CaptureError.invalidDisplayID.errorDescription == "Invalid display ID provided.") #expect(CaptureError.captureCreationFailed.errorDescription == "Failed to create the screen capture.") #expect(CaptureError.windowNotFound.errorDescription == "The specified window could not be found.") #expect(CaptureError.windowCaptureFailed.errorDescription == "Failed to capture the specified window.") - #expect(CaptureError.fileWriteError("/tmp/test.png").errorDescription == "Failed to write capture file to path: /tmp/test.png.") - #expect(CaptureError.appNotFound("Safari").errorDescription == "Application with identifier 'Safari' not found or is not running.") + #expect(CaptureError.fileWriteError("/tmp/test.png") + .errorDescription == "Failed to write capture file to path: /tmp/test.png." + ) + #expect(CaptureError.appNotFound("Safari") + .errorDescription == "Application with identifier 'Safari' not found or is not running." + ) #expect(CaptureError.invalidWindowIndex(5).errorDescription == "Invalid window index: 5.") } - + @Test("CaptureError exit codes", .tags(.fast)) func captureErrorExitCodes() { let testCases: [(CaptureError, Int32)] = [ @@ -292,14 +298,14 @@ struct ModelsTests { (.invalidArgument("test"), 20), (.unknownError("test"), 1) ] - + for (error, expectedCode) in testCases { #expect(error.exitCode == expectedCode) } } - + // MARK: - WindowData Tests - + @Test("WindowData initialization from CGRect", .tags(.fast)) func windowData() { let bounds = CGRect(x: 100, y: 200, width: 1200, height: 800) @@ -310,7 +316,7 @@ struct ModelsTests { isOnScreen: true, windowIndex: 0 ) - + #expect(windowData.windowId == 12345) #expect(windowData.title == "Safari - Main Window") #expect(windowData.bounds.origin.x == 100) @@ -320,19 +326,19 @@ struct ModelsTests { #expect(windowData.isOnScreen == true) #expect(windowData.windowIndex == 0) } - + @Test("WindowSpecifier variants", .tags(.fast)) func windowSpecifier() { let titleSpecifier = WindowSpecifier.title("Main Window") let indexSpecifier = WindowSpecifier.index(0) - + switch titleSpecifier { case let .title(title): #expect(title == "Main Window") case .index: Issue.record("Expected title specifier") } - + switch indexSpecifier { case .title: Issue.record("Expected index specifier") @@ -346,21 +352,22 @@ struct ModelsTests { @Suite("Model Edge Cases", .tags(.models, .unit)) struct ModelEdgeCaseTests { - - @Test("WindowBounds with edge values", - arguments: [ - (x: 0, y: 0, width: 0, height: 0), - (x: -100, y: -100, width: 100, height: 100), - (x: Int.max, y: Int.max, width: 1, height: 1) - ]) - func windowBoundsEdgeCases(x: Int, y: Int, width: Int, height: Int) { - let bounds = WindowBounds(xCoordinate: x, yCoordinate: y, width: width, height: height) - #expect(bounds.xCoordinate == x) - #expect(bounds.yCoordinate == y) + @Test( + "WindowBounds with edge values", + arguments: [ + (x: 0, y: 0, width: 0, height: 0), + (x: -100, y: -100, width: 100, height: 100), + (x: Int.max, y: Int.max, width: 1, height: 1) + ] + ) + func windowBoundsEdgeCases(x xCoordinate: Int, y yCoordinate: Int, width: Int, height: Int) { + let bounds = WindowBounds(xCoordinate: xCoordinate, yCoordinate: yCoordinate, width: width, height: height) + #expect(bounds.xCoordinate == xCoordinate) + #expect(bounds.yCoordinate == yCoordinate) #expect(bounds.width == width) #expect(bounds.height == height) } - + @Test("ApplicationInfo with extreme values", .tags(.fast)) func applicationInfoExtremeValues() { let appInfo = ApplicationInfo( @@ -370,22 +377,24 @@ struct ModelEdgeCaseTests { is_active: true, window_count: Int.max ) - + #expect(appInfo.app_name.count == 1000) #expect(appInfo.bundle_id.contains("com.test.")) #expect(appInfo.pid == Int32.max) #expect(appInfo.window_count == Int.max) } - - @Test("SavedFile path validation", - arguments: [ - "/tmp/test.png", - "/Users/test/Desktop/screenshot.jpg", - "~/Documents/capture.png", - "./relative/path/image.png", - "/path with spaces/image.png", - "/path/with/特殊文字.png" - ]) + + @Test( + "SavedFile path validation", + arguments: [ + "/tmp/test.png", + "/Users/test/Desktop/screenshot.jpg", + "~/Documents/capture.png", + "./relative/path/image.png", + "/path with spaces/image.png", + "/path/with/特殊文字.png" + ] + ) func savedFilePathValidation(path: String) { let savedFile = SavedFile( path: path, @@ -395,13 +404,15 @@ struct ModelEdgeCaseTests { window_index: nil, mime_type: "image/png" ) - + #expect(savedFile.path == path) #expect(!savedFile.path.isEmpty) } - - @Test("MIME type validation", - arguments: ["image/png", "image/jpeg", "image/jpg"]) + + @Test( + "MIME type validation", + arguments: ["image/png", "image/jpeg", "image/jpg"] + ) func mimeTypeValidation(mimeType: String) { let savedFile = SavedFile( path: "/tmp/test", @@ -411,8 +422,8 @@ struct ModelEdgeCaseTests { window_index: nil, mime_type: mimeType ) - + #expect(savedFile.mime_type == mimeType) #expect(savedFile.mime_type.starts(with: "image/")) } -} \ No newline at end of file +} diff --git a/peekaboo-cli/Tests/peekabooTests/PermissionsCheckerTests.swift b/peekaboo-cli/Tests/peekabooTests/PermissionsCheckerTests.swift index 68ffd86..4000a85 100644 --- a/peekaboo-cli/Tests/peekabooTests/PermissionsCheckerTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/PermissionsCheckerTests.swift @@ -1,78 +1,78 @@ +import AppKit @testable import peekaboo import Testing -import AppKit @Suite("PermissionsChecker Tests", .tags(.permissions, .unit)) struct PermissionsCheckerTests { // MARK: - Screen Recording Permission Tests - + @Test("Screen recording permission check returns boolean", .tags(.fast)) func checkScreenRecordingPermission() { // Test screen recording permission check let hasPermission = PermissionsChecker.checkScreenRecordingPermission() - + // This test will pass or fail based on actual system permissions // The result should be a valid boolean #expect(hasPermission == true || hasPermission == false) } - + @Test("Screen recording permission check is consistent", .tags(.fast)) func screenRecordingPermissionConsistency() { // Test that multiple calls return consistent results let firstCheck = PermissionsChecker.checkScreenRecordingPermission() let secondCheck = PermissionsChecker.checkScreenRecordingPermission() - + #expect(firstCheck == secondCheck) } - + @Test("Screen recording permission check performance", arguments: 1...5) func screenRecordingPermissionPerformance(iteration: Int) { // Permission checks should be fast let hasPermission = PermissionsChecker.checkScreenRecordingPermission() #expect(hasPermission == true || hasPermission == false) } - + // MARK: - Accessibility Permission Tests - + @Test("Accessibility permission check returns boolean", .tags(.fast)) func checkAccessibilityPermission() { // Test accessibility permission check let hasPermission = PermissionsChecker.checkAccessibilityPermission() - + // This will return the actual system state #expect(hasPermission == true || hasPermission == false) } - + @Test("Accessibility permission matches AXIsProcessTrusted", .tags(.fast)) func accessibilityPermissionWithTrustedCheck() { // Test the AXIsProcessTrusted check let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: false] let isTrusted = AXIsProcessTrustedWithOptions(options as CFDictionary) let hasPermission = PermissionsChecker.checkAccessibilityPermission() - + // These should match #expect(isTrusted == hasPermission) } - + // MARK: - Combined Permission Tests - + @Test("Both permissions can be checked independently", .tags(.fast)) func bothPermissions() { // Test both permission checks let screenRecording = PermissionsChecker.checkScreenRecordingPermission() let accessibility = PermissionsChecker.checkAccessibilityPermission() - + // Both should return valid boolean values #expect(screenRecording == true || screenRecording == false) #expect(accessibility == true || accessibility == false) } - + // MARK: - Require Permission Tests - + @Test("Require screen recording permission throws when denied", .tags(.fast)) func requireScreenRecordingPermission() { let hasPermission = PermissionsChecker.checkScreenRecordingPermission() - + if hasPermission { // Should not throw when permission is granted #expect(throws: Never.self) { @@ -85,11 +85,11 @@ struct PermissionsCheckerTests { } } } - + @Test("Require accessibility permission throws when denied", .tags(.fast)) func requireAccessibilityPermission() { let hasPermission = PermissionsChecker.checkAccessibilityPermission() - + if hasPermission { // Should not throw when permission is granted #expect(throws: Never.self) { @@ -102,26 +102,26 @@ struct PermissionsCheckerTests { } } } - + // MARK: - Error Message Tests - + @Test("Permission errors have descriptive messages", .tags(.fast)) func permissionErrorMessages() { let screenError = CaptureError.screenRecordingPermissionDenied let accessError = CaptureError.accessibilityPermissionDenied - + // CaptureError conforms to LocalizedError, so it has errorDescription #expect(screenError.errorDescription != nil) #expect(accessError.errorDescription != nil) #expect(screenError.errorDescription!.contains("Screen recording permission")) #expect(accessError.errorDescription!.contains("Accessibility permission")) } - + @Test("Permission errors have correct exit codes", .tags(.fast)) func permissionErrorExitCodes() { let screenError = CaptureError.screenRecordingPermissionDenied let accessError = CaptureError.accessibilityPermissionDenied - + #expect(screenError.exitCode == 11) #expect(accessError.exitCode == 12) } @@ -131,7 +131,6 @@ struct PermissionsCheckerTests { @Suite("Permission Edge Cases", .tags(.permissions, .unit)) struct PermissionEdgeCaseTests { - @Test("Permission checks are thread-safe", .tags(.integration)) func threadSafePermissionChecks() async { // Test concurrent permission checks @@ -144,12 +143,12 @@ struct PermissionEdgeCaseTests { PermissionsChecker.checkAccessibilityPermission() } } - + var results: [Bool] = [] for await result in group { results.append(result) } - + // All results should be valid booleans #expect(results.count == 20) for result in results { @@ -157,7 +156,7 @@ struct PermissionEdgeCaseTests { } } } - + @Test("ScreenCaptureKit availability check", .tags(.fast)) func screenCaptureKitAvailable() { // Verify that we can at least access ScreenCaptureKit APIs @@ -165,25 +164,25 @@ struct PermissionEdgeCaseTests { let isAvailable = NSClassFromString("SCShareableContent") != nil #expect(isAvailable == true) } - + @Test("Permission state changes are detected", .tags(.integration)) func permissionStateChanges() { // This test verifies that permission checks reflect current state // Note: This test cannot actually change permissions, but verifies // that repeated checks could detect changes if they occurred - + let initialScreen = PermissionsChecker.checkScreenRecordingPermission() let initialAccess = PermissionsChecker.checkAccessibilityPermission() - + // Sleep briefly to allow for potential state changes Thread.sleep(forTimeInterval: 0.1) - + let finalScreen = PermissionsChecker.checkScreenRecordingPermission() let finalAccess = PermissionsChecker.checkAccessibilityPermission() - + // In normal operation, these should be the same // but the important thing is they reflect current state #expect(initialScreen == finalScreen) #expect(initialAccess == finalAccess) } -} \ No newline at end of file +} diff --git a/peekaboo-cli/Tests/peekabooTests/TestTags.swift b/peekaboo-cli/Tests/peekabooTests/TestTags.swift index af34cef..190f7c6 100644 --- a/peekaboo-cli/Tests/peekabooTests/TestTags.swift +++ b/peekaboo-cli/Tests/peekabooTests/TestTags.swift @@ -14,4 +14,4 @@ extension Tag { @Tag static var performance: Self @Tag static var concurrency: Self @Tag static var memory: Self -} \ No newline at end of file +} diff --git a/peekaboo-cli/Tests/peekabooTests/WindowManagerTests.swift b/peekaboo-cli/Tests/peekabooTests/WindowManagerTests.swift index acf23fe..d711038 100644 --- a/peekaboo-cli/Tests/peekabooTests/WindowManagerTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/WindowManagerTests.swift @@ -5,19 +5,19 @@ import Testing @Suite("WindowManager Tests", .tags(.windowManager, .unit)) struct WindowManagerTests { // MARK: - Get Windows For App Tests - + @Test("Getting windows for Finder app", .tags(.integration)) func getWindowsForFinderApp() throws { // Get Finder's PID let apps = NSWorkspace.shared.runningApplications - let finder = try #require(apps.first(where: { $0.bundleIdentifier == "com.apple.finder" })) - + let finder = try #require(apps.first { $0.bundleIdentifier == "com.apple.finder" }) + // Test getting windows for Finder let windows = try WindowManager.getWindowsForApp(pid: finder.processIdentifier) - + // Finder usually has at least one window - #expect(windows.count >= 0) - + // Windows count is always non-negative + // If there are windows, verify they're sorted by index if windows.count > 1 { for index in 1..= on-screen only #expect(allWindows.count >= onScreenWindows.count) } - + // MARK: - WindowData Structure Tests - + @Test("WindowData has all required properties", .tags(.fast)) func windowDataStructure() throws { // Get any app's windows to test the structure let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular } - + guard let app = apps.first else { return // Skip test if no regular apps running } - + let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier) - + // If we have windows, verify WindowData properties if let firstWindow = windows.first { // Check required properties exist @@ -74,25 +74,25 @@ struct WindowManagerTests { #expect(firstWindow.bounds.height >= 0) } } - + // MARK: - Window Info Tests - + @Test("Getting window info with details", .tags(.integration)) func getWindowsInfoForApp() throws { // Test getting window info with details let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular } - + guard let app = apps.first else { return // Skip test if no regular apps running } - + let windowInfos = try WindowManager.getWindowsInfoForApp( pid: app.processIdentifier, includeOffScreen: false, includeBounds: true, includeIDs: true ) - + // Verify WindowInfo structure if let firstInfo = windowInfos.first { #expect(!firstInfo.window_title.isEmpty) @@ -100,40 +100,42 @@ struct WindowManagerTests { #expect(firstInfo.bounds != nil) } } - + // MARK: - Parameterized Tests - - @Test("Window retrieval with various options", - arguments: [ - (includeOffScreen: true, includeBounds: true, includeIDs: true), - (includeOffScreen: false, includeBounds: true, includeIDs: true), - (includeOffScreen: true, includeBounds: false, includeIDs: true), - (includeOffScreen: true, includeBounds: true, includeIDs: false) - ]) + + @Test( + "Window retrieval with various options", + arguments: [ + (includeOffScreen: true, includeBounds: true, includeIDs: true), + (includeOffScreen: false, includeBounds: true, includeIDs: true), + (includeOffScreen: true, includeBounds: false, includeIDs: true), + (includeOffScreen: true, includeBounds: true, includeIDs: false) + ] + ) func windowRetrievalOptions(includeOffScreen: Bool, includeBounds: Bool, includeIDs: Bool) throws { let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular } - + guard let app = apps.first else { return // Skip test if no regular apps running } - + let windowInfos = try WindowManager.getWindowsInfoForApp( pid: app.processIdentifier, includeOffScreen: includeOffScreen, includeBounds: includeBounds, includeIDs: includeIDs ) - + // Verify options are respected for info in windowInfos { #expect(!info.window_title.isEmpty) - + if includeIDs { #expect(info.window_id != nil) } else { #expect(info.window_id == nil) } - + if includeBounds { #expect(info.bounds != nil) } else { @@ -141,22 +143,24 @@ struct WindowManagerTests { } } } - + // MARK: - Performance Tests - - @Test("Window retrieval performance", - arguments: 1...5) + + @Test( + "Window retrieval performance", + arguments: 1...5 + ) func getWindowsPerformance(iteration: Int) throws { // Test performance of getting windows let apps = NSWorkspace.shared.runningApplications - let finder = try #require(apps.first(where: { $0.bundleIdentifier == "com.apple.finder" })) - + let finder = try #require(apps.first { $0.bundleIdentifier == "com.apple.finder" }) + let windows = try WindowManager.getWindowsForApp(pid: finder.processIdentifier) - #expect(windows.count >= 0) + // Windows count is always non-negative } - + // MARK: - Error Handling Tests - + @Test("WindowError types exist", .tags(.fast)) func windowListError() { // We can't easily force CGWindowListCopyWindowInfo to fail, @@ -176,35 +180,34 @@ struct WindowManagerTests { @Suite("WindowManager Advanced Tests", .tags(.windowManager, .integration)) struct WindowManagerAdvancedTests { - @Test("Multiple apps window retrieval", .tags(.integration)) func multipleAppsWindows() throws { let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular } let appsToTest = apps.prefix(3) // Test first 3 apps - + for app in appsToTest { let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier) - + // Each app should successfully return a window list (even if empty) - #expect(windows.count >= 0) - + // Windows count is always non-negative + // Verify window indices are sequential for (index, window) in windows.enumerated() { #expect(window.windowIndex == index) } } } - + @Test("Window bounds validation", .tags(.integration)) func windowBoundsValidation() throws { let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular } - + guard let app = apps.first else { return // Skip test if no regular apps running } - + let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier) - + for window in windows { // Window bounds should be reasonable #expect(window.bounds.width > 0) @@ -213,40 +216,42 @@ struct WindowManagerAdvancedTests { #expect(window.bounds.height < 10000) // Reasonable maximum } } - - @Test("System apps window detection", - arguments: ["com.apple.finder", "com.apple.dock", "com.apple.systemuiserver"]) + + @Test( + "System apps window detection", + arguments: ["com.apple.finder", "com.apple.dock", "com.apple.systemuiserver"] + ) func systemAppsWindows(bundleId: String) throws { let apps = NSWorkspace.shared.runningApplications - + guard let app = apps.first(where: { $0.bundleIdentifier == bundleId }) else { return // Skip test if app not running } - + let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier) - + // System apps might have 0 or more windows - #expect(windows.count >= 0) - + // Windows count is always non-negative + // If windows exist, they should have valid properties for window in windows { #expect(window.windowId > 0) #expect(!window.title.isEmpty) } } - + @Test("Window title encoding", .tags(.fast)) func windowTitleEncoding() throws { // Test that window titles with special characters are handled let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular } - + for app in apps.prefix(5) { let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier) - + for window in windows { // Title should be valid UTF-8 - #expect(window.title.utf8.count > 0) - + #expect(!window.title.utf8.isEmpty) + // Should handle common special characters let specialChars = ["—", "™", "©", "•", "…"] // Window titles might contain these, should not crash @@ -256,15 +261,15 @@ struct WindowManagerAdvancedTests { } } } - + @Test("Concurrent window queries", .tags(.integration)) func concurrentWindowQueries() async throws { let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular } - + guard let app = apps.first else { return // Skip test if no regular apps running } - + // Test concurrent access to WindowManager await withTaskGroup(of: Result<[WindowData], Error>.self) { group in for _ in 0..<5 { @@ -277,22 +282,22 @@ struct WindowManagerAdvancedTests { } } } - + var results: [Result<[WindowData], Error>] = [] for await result in group { results.append(result) } - + // All concurrent queries should succeed #expect(results.count == 5) for result in results { switch result { - case .success(let windows): - #expect(windows.count >= 0) - case .failure(let error): + case let .success(windows): + // Windows count is always non-negative + case let .failure(error): Issue.record("Concurrent query failed: \(error)") } } } } -} \ No newline at end of file +}