diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e93274d3..78dea457 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -465,54 +465,19 @@ jobs: run: | set -euo pipefail SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" - HARNESS_DIR="${RUNNER_TEMP}/cmux-display-churn-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - mkdir -p "$HARNESS_DIR" - PREFIX="${HARNESS_DIR}/cmux-display-churn" - READY_PATH="${PREFIX}.ready" - DISPLAY_ID_PATH="${PREFIX}.id" - START_PATH="${PREFIX}.start" - DONE_PATH="${PREFIX}.done" - LOG_PATH="${PREFIX}.log" - MANIFEST_PATH="${HARNESS_DIR}/cmux-ui-test-display-harness.json" + HELPER_PATH="/tmp/create-virtual-display" + MANIFEST_PATH="/tmp/cmux-ui-test-display-harness.json" - rm -f "$READY_PATH" "$DISPLAY_ID_PATH" "$START_PATH" "$DONE_PATH" "$LOG_PATH" "$MANIFEST_PATH" + rm -f "$MANIFEST_PATH" + trap 'rm -f "$MANIFEST_PATH"' EXIT clang -framework Foundation -framework CoreGraphics \ - -o /tmp/create-virtual-display scripts/create-virtual-display.m - - /tmp/create-virtual-display \ - --modes 1920x1080,1728x1117,1600x900,1440x810 \ - --ready-path "$READY_PATH" \ - --display-id-path "$DISPLAY_ID_PATH" \ - --start-path "$START_PATH" \ - --done-path "$DONE_PATH" \ - --iterations 40 \ - --interval-ms 40 \ - >"$LOG_PATH" 2>&1 & - VDISPLAY_PID=$! - trap 'kill "$VDISPLAY_PID" >/dev/null 2>&1 || true; rm -f "$MANIFEST_PATH"' EXIT - - for _ in {1..120}; do - [ -f "$READY_PATH" ] && break - sleep 0.25 - done - [ -f "$READY_PATH" ] || { - echo "Display harness failed to start" >&2 - cat "$LOG_PATH" >&2 || true - exit 1 - } + -o "$HELPER_PATH" scripts/create-virtual-display.m cat >"$MANIFEST_PATH" <"$LOG_PATH" 2>&1 & - DISPLAY_VDISPLAY_PID=$! - trap 'kill "${DISPLAY_VDISPLAY_PID:-}" >/dev/null 2>&1 || true; rm -f "$MANIFEST_PATH"' EXIT - - for _ in {1..120}; do - [ -f "$READY_PATH" ] && break - sleep 0.25 - done - [ -f "$READY_PATH" ] || { - echo "Display harness failed to start" >&2 - cat "$LOG_PATH" >&2 || true - exit 1 - } + -o "$HELPER_PATH" scripts/create-virtual-display.m cat >"$MANIFEST_PATH" <=6.2) -@available(macOS 26.0, *) -private struct DragConfigurationOperationsSnapshot: Equatable { - let allowCopy: Bool - let allowMove: Bool - let allowDelete: Bool - let allowAlias: Bool -} - -@available(macOS 26.0, *) -private enum DragConfigurationSnapshotError: Error { - case missingBoolField(primary: String, fallback: String?) -} - -@available(macOS 26.0, *) -private func dragConfigurationOperationsSnapshot(from operations: T) throws -> DragConfigurationOperationsSnapshot { - let mirror = Mirror(reflecting: operations) - - func readBool(_ primary: String, fallback: String? = nil) throws -> Bool { - if let value = mirror.descendant(primary) as? Bool { - return value - } - if let fallback, let value = mirror.descendant(fallback) as? Bool { - return value - } - throw DragConfigurationSnapshotError.missingBoolField(primary: primary, fallback: fallback) - } - - return try DragConfigurationOperationsSnapshot( - allowCopy: readBool("allowCopy", fallback: "_allowCopy"), - allowMove: readBool("allowMove", fallback: "_allowMove"), - allowDelete: readBool("allowDelete", fallback: "_allowDelete"), - allowAlias: readBool("allowAlias", fallback: "_allowAlias") - ) -} - - final class BrowserLinkOpenSettingsTests: XCTestCase { private var suiteName: String! private var defaults: UserDefaults! diff --git a/cmuxTests/BrowserPanelTests.swift b/cmuxTests/BrowserPanelTests.swift index 104cecbf..5c61be3d 100644 --- a/cmuxTests/BrowserPanelTests.swift +++ b/cmuxTests/BrowserPanelTests.swift @@ -13,7 +13,7 @@ import UserNotifications @testable import cmux #endif -func drainMainQueue() { +private func drainBrowserPanelMainQueue() { let expectation = XCTestExpectation(description: "drain main queue") DispatchQueue.main.async { expectation.fulfill() @@ -22,7 +22,7 @@ func drainMainQueue() { } @MainActor -func makeTemporaryBrowserProfile(named prefix: String) throws -> BrowserProfileDefinition { +private func makeTemporaryBrowserPanelProfile(named prefix: String) throws -> BrowserProfileDefinition { try XCTUnwrap( BrowserProfileStore.shared.createProfile( named: "\(prefix)-\(UUID().uuidString)" @@ -107,7 +107,7 @@ final class BrowserPanelOmnibarPillBackgroundColorTests: XCTestCase { @MainActor final class BrowserPanelProfileIsolationTests: XCTestCase { func testStaleDidFinishDoesNotRecordVisitIntoSwitchedProfileHistory() throws { - let alternateProfile = try makeTemporaryBrowserProfile(named: "Switched") + let alternateProfile = try makeTemporaryBrowserPanelProfile(named: "Switched") let defaultStore = BrowserHistoryStore.shared let alternateStore = BrowserProfileStore.shared.historyStore(for: alternateProfile.id) defaultStore.clearHistory() @@ -137,7 +137,7 @@ final class BrowserPanelProfileIsolationTests: XCTestCase { alternateStore.clearHistory() staleDelegate.webView?(staleWebView, didFinish: nil) - drainMainQueue() + drainBrowserPanelMainQueue() XCTAssertTrue( defaultStore.entries.isEmpty, diff --git a/cmuxTests/WindowAndDragTests.swift b/cmuxTests/WindowAndDragTests.swift index e618949f..6964c45f 100644 --- a/cmuxTests/WindowAndDragTests.swift +++ b/cmuxTests/WindowAndDragTests.swift @@ -299,6 +299,42 @@ final class FocusFlashPatternTests: XCTestCase { } +@available(macOS 26.0, *) +private struct DragConfigurationOperationsSnapshot: Equatable { + let allowCopy: Bool + let allowMove: Bool + let allowDelete: Bool + let allowAlias: Bool +} + +@available(macOS 26.0, *) +private enum DragConfigurationSnapshotError: Error { + case missingBoolField(primary: String, fallback: String?) +} + +@available(macOS 26.0, *) +private func dragConfigurationOperationsSnapshot(from operations: T) throws -> DragConfigurationOperationsSnapshot { + let mirror = Mirror(reflecting: operations) + + func readBool(_ primary: String, fallback: String? = nil) throws -> Bool { + if let value = mirror.descendant(primary) as? Bool { + return value + } + if let fallback, let value = mirror.descendant(fallback) as? Bool { + return value + } + throw DragConfigurationSnapshotError.missingBoolField(primary: primary, fallback: fallback) + } + + return try DragConfigurationOperationsSnapshot( + allowCopy: readBool("allowCopy", fallback: "_allowCopy"), + allowMove: readBool("allowMove", fallback: "_allowMove"), + allowDelete: readBool("allowDelete", fallback: "_allowDelete"), + allowAlias: readBool("allowAlias", fallback: "_allowAlias") + ) +} + +#if compiler(>=6.2) @MainActor final class InternalTabDragConfigurationTests: XCTestCase { func testDisablesExternalOperationsForInternalTabDrags() throws { @@ -1080,3 +1116,4 @@ final class MarkdownPanelPointerObserverViewTests: XCTestCase { XCTAssertNil(overlay.hitTest(NSPoint(x: 40, y: 30))) } } +#endif diff --git a/cmuxTests/WorkspaceManualUnreadTests.swift b/cmuxTests/WorkspaceManualUnreadTests.swift index 1610dc34..6fbe30c3 100644 --- a/cmuxTests/WorkspaceManualUnreadTests.swift +++ b/cmuxTests/WorkspaceManualUnreadTests.swift @@ -7,6 +7,7 @@ import AppKit @testable import cmux #endif +@MainActor final class WorkspaceManualUnreadTests: XCTestCase { func testShouldClearManualUnreadWhenFocusMovesToDifferentPanel() { let previousFocusedPanelId = UUID() @@ -281,7 +282,7 @@ final class CommandPaletteSwitcherSearchIndexerTests: XCTestCase { XCTAssertFalse(keywords.contains("cmd-palette-indexing")) } - func testSurfaceDetailOutranksWorkspaceDetailForPathToken() { + func testSurfaceDetailOutranksWorkspaceDetailForPathToken() throws { let metadata = CommandPaletteSwitcherSearchMetadata( directories: ["/tmp/worktrees/cmux"], branches: ["feature/cmd-palette"], diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index 5bd83682..719baa43 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -1896,4 +1896,3 @@ final class SidebarWorkspaceShortcutHintMetricsTests: XCTestCase { XCTAssertGreaterThan(widened, base) } } -#endif diff --git a/cmuxUITests/DisplayResolutionRegressionUITests.swift b/cmuxUITests/DisplayResolutionRegressionUITests.swift index 579ae221..1cc5aecf 100644 --- a/cmuxUITests/DisplayResolutionRegressionUITests.swift +++ b/cmuxUITests/DisplayResolutionRegressionUITests.swift @@ -2,7 +2,7 @@ import XCTest import Foundation final class DisplayResolutionRegressionUITests: XCTestCase { - private let displayHarnessManifestPath = "/tmp/cmux-ui-test-display-harness.json" + private let defaultDisplayHarnessManifestPath = "/tmp/cmux-ui-test-display-harness.json" private var launchTag = "" private var diagnosticsPath = "" private var displayReadyPath = "" @@ -19,14 +19,17 @@ final class DisplayResolutionRegressionUITests: XCTestCase { continueAfterFailure = false let token = UUID().uuidString + let tempPrefix = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-ui-test-display-\(token)") + .path launchTag = "ui-tests-display-resolution-\(token.prefix(8))" diagnosticsPath = "/tmp/cmux-ui-test-display-churn-\(token).json" - displayReadyPath = "/tmp/cmux-ui-test-display-ready-\(token)" - displayIDPath = "/tmp/cmux-ui-test-display-id-\(token)" - displayStartPath = "/tmp/cmux-ui-test-display-start-\(token)" - displayDonePath = "/tmp/cmux-ui-test-display-done-\(token)" - helperBinaryPath = "/tmp/cmux-ui-test-display-helper-\(token)" - helperLogPath = "/tmp/cmux-ui-test-display-helper-\(token).log" + displayReadyPath = "\(tempPrefix).ready" + displayIDPath = "\(tempPrefix).id" + displayStartPath = "\(tempPrefix).start" + displayDonePath = "\(tempPrefix).done" + helperBinaryPath = "\(tempPrefix)-helper" + helperLogPath = "\(tempPrefix)-helper.log" removeTestArtifacts() } @@ -118,11 +121,29 @@ final class DisplayResolutionRegressionUITests: XCTestCase { private func prepareDisplayHarnessIfNeeded() throws { let env = ProcessInfo.processInfo.environment - if let externalHarness = loadExternalHarnessFromEnvironment(env) ?? loadExternalHarnessFromManifest() { - displayReadyPath = externalHarness.readyPath - displayIDPath = externalHarness.displayIDPath - displayStartPath = externalHarness.startPath - displayDonePath = externalHarness.donePath + if let helperBinaryPath = loadPrebuiltHelperBinaryPath(env) { + self.helperBinaryPath = helperBinaryPath + try launchDisplayHelper() + return + } + if let externalHarness = loadExternalHarnessFromEnvironment(env) ?? loadExternalHarnessFromManifest(env) { + if let helperBinaryPath = externalHarness.helperBinaryPath, !helperBinaryPath.isEmpty { + self.helperBinaryPath = helperBinaryPath + try launchDisplayHelper() + return + } + guard let readyPath = externalHarness.readyPath, !readyPath.isEmpty, + let displayIDPath = externalHarness.displayIDPath, !displayIDPath.isEmpty, + let startPath = externalHarness.startPath, !startPath.isEmpty, + let donePath = externalHarness.donePath, !donePath.isEmpty else { + throw NSError(domain: "DisplayResolutionRegressionUITests", code: 3, userInfo: [ + NSLocalizedDescriptionKey: "Incomplete external display harness configuration" + ]) + } + displayReadyPath = readyPath + self.displayIDPath = displayIDPath + displayStartPath = startPath + displayDonePath = donePath if let logPath = externalHarness.logPath, !logPath.isEmpty { helperLogPath = logPath } @@ -133,6 +154,14 @@ final class DisplayResolutionRegressionUITests: XCTestCase { try launchDisplayHelper() } + private func loadPrebuiltHelperBinaryPath(_ env: [String: String]) -> String? { + guard let helperBinaryPath = env["CMUX_UI_TEST_DISPLAY_HELPER_BINARY_PATH"], + !helperBinaryPath.isEmpty else { + return nil + } + return helperBinaryPath + } + private func loadExternalHarnessFromEnvironment(_ env: [String: String]) -> ExternalDisplayHarness? { guard let readyPath = env["CMUX_UI_TEST_DISPLAY_READY_PATH"], !readyPath.isEmpty, let displayIDPath = env["CMUX_UI_TEST_DISPLAY_ID_PATH"], !displayIDPath.isEmpty, @@ -146,12 +175,14 @@ final class DisplayResolutionRegressionUITests: XCTestCase { displayIDPath: displayIDPath, startPath: startPath, donePath: donePath, - logPath: env["CMUX_UI_TEST_DISPLAY_LOG_PATH"] + logPath: env["CMUX_UI_TEST_DISPLAY_LOG_PATH"], + helperBinaryPath: nil ) } - private func loadExternalHarnessFromManifest() -> ExternalDisplayHarness? { - let manifestURL = URL(fileURLWithPath: displayHarnessManifestPath) + private func loadExternalHarnessFromManifest(_ env: [String: String]) -> ExternalDisplayHarness? { + let manifestPath = env["CMUX_UI_TEST_DISPLAY_HARNESS_MANIFEST_PATH"] ?? defaultDisplayHarnessManifestPath + let manifestURL = URL(fileURLWithPath: manifestPath) guard let data = try? Data(contentsOf: manifestURL) else { return nil } @@ -377,10 +408,11 @@ final class DisplayResolutionRegressionUITests: XCTestCase { } private struct ExternalDisplayHarness: Decodable { - let readyPath: String - let displayIDPath: String - let startPath: String - let donePath: String + let readyPath: String? + let displayIDPath: String? + let startPath: String? + let donePath: String? let logPath: String? + let helperBinaryPath: String? } }