Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

537 lines
20 KiB
Swift

import XCTest
import IOKit.pwr_mgt
import MacToolsPluginKit
@testable import KeepAwakePlugin
@MainActor
final class KeepAwakePluginTests: XCTestCase {
func testPermanentSessionPersistsAndRestoresAfterHostShutdown() {
let storage = KeepAwakeMemoryStorage()
let firstFactory = KeepAwakeSessionFactory()
let firstPlugin = firstFactory.makePlugin(storage: storage)
firstPlugin.handleAction(.setSwitch(true))
XCTAssertTrue(firstPlugin.primaryPanelState.isOn)
XCTAssertEqual(storage.values["persistent-enabled"] as? Bool, true)
XCTAssertEqual(firstFactory.sessions.count, 1)
XCTAssertEqual(firstFactory.sessions[0].startedConfigurations.count, 1)
XCTAssertNil(firstFactory.sessions[0].startedConfigurations[0].endDate)
XCTAssertFalse(firstFactory.sessions[0].startedConfigurations[0].preventDisplaySleep)
firstPlugin.deactivate(reason: .hostShutdown)
XCTAssertFalse(firstPlugin.primaryPanelState.isOn)
XCTAssertEqual(storage.values["persistent-enabled"] as? Bool, true)
XCTAssertEqual(firstFactory.sessions[0].stopRequestCount, 1)
let secondFactory = KeepAwakeSessionFactory()
let secondPlugin = secondFactory.makePlugin(storage: storage)
secondPlugin.activate(context: Self.context(storage: storage))
XCTAssertTrue(secondPlugin.primaryPanelState.isOn)
XCTAssertEqual(storage.values["persistent-enabled"] as? Bool, true)
XCTAssertEqual(secondFactory.sessions.count, 1)
XCTAssertEqual(secondFactory.sessions[0].startedConfigurations.count, 1)
XCTAssertNil(secondFactory.sessions[0].startedConfigurations[0].endDate)
XCTAssertFalse(secondFactory.sessions[0].startedConfigurations[0].preventDisplaySleep)
}
func testTemporarySessionDoesNotRestoreAfterHostShutdown() {
let storage = KeepAwakeMemoryStorage()
let firstFactory = KeepAwakeSessionFactory()
let firstPlugin = firstFactory.makePlugin(storage: storage)
firstPlugin.handleAction(.setSwitch(true))
firstPlugin.handleAction(.setSelection(controlID: "duration", optionID: "oneHour"))
XCTAssertTrue(firstPlugin.primaryPanelState.isOn)
XCTAssertNil(storage.values["persistent-enabled"])
XCTAssertEqual(firstFactory.sessions.count, 1)
XCTAssertEqual(firstFactory.sessions[0].startedConfigurations.count, 2)
XCTAssertNotNil(firstFactory.sessions[0].startedConfigurations[1].endDate)
firstPlugin.deactivate(reason: .hostShutdown)
XCTAssertFalse(firstPlugin.primaryPanelState.isOn)
XCTAssertNil(storage.values["persistent-enabled"])
let secondFactory = KeepAwakeSessionFactory()
let secondPlugin = secondFactory.makePlugin(storage: storage)
secondPlugin.activate(context: Self.context(storage: storage))
XCTAssertFalse(secondPlugin.primaryPanelState.isOn)
XCTAssertTrue(secondFactory.sessions.isEmpty)
}
func testManualSwitchOffClearsPermanentRestoreState() {
let storage = KeepAwakeMemoryStorage()
let firstFactory = KeepAwakeSessionFactory()
let firstPlugin = firstFactory.makePlugin(storage: storage)
firstPlugin.handleAction(.setSwitch(true))
XCTAssertEqual(storage.values["persistent-enabled"] as? Bool, true)
firstPlugin.handleAction(.setSwitch(false))
XCTAssertFalse(firstPlugin.primaryPanelState.isOn)
XCTAssertNil(storage.values["persistent-enabled"])
XCTAssertEqual(firstFactory.sessions[0].stopRequestCount, 1)
let secondFactory = KeepAwakeSessionFactory()
let secondPlugin = secondFactory.makePlugin(storage: storage)
secondPlugin.activate(context: Self.context(storage: storage))
XCTAssertFalse(secondPlugin.primaryPanelState.isOn)
XCTAssertTrue(secondFactory.sessions.isEmpty)
}
func testKeepDisplayOnSettingDefaultsToOffAndUpdatesRunningSession() {
let storage = KeepAwakeMemoryStorage()
let factory = KeepAwakeSessionFactory()
let plugin = factory.makePlugin(storage: storage)
plugin.handleAction(.setSwitch(true))
XCTAssertEqual(factory.sessions[0].startedConfigurations.last?.preventDisplaySleep, false)
XCTAssertNil(storage.values["keep-display-on"])
XCTAssertEqual(plugin.primaryPanelState.subtitle, "不会自动停止")
XCTAssertEqual(plugin.primaryPanelState.detail?.primaryControls.map(\.id), ["duration"])
XCTAssertNotNil(plugin.configuration)
XCTAssertNil(plugin.primaryPanelIndicator)
plugin.setKeepDisplayOn(true)
XCTAssertEqual(factory.sessions[0].startedConfigurations.count, 1)
XCTAssertEqual(factory.sessions[0].displaySleepPreventionUpdates, [true])
XCTAssertEqual(storage.values["keep-display-on"] as? Bool, true)
XCTAssertEqual(plugin.primaryPanelState.subtitle, "不会自动停止")
XCTAssertEqual(
plugin.primaryPanelIndicator,
PluginPrimaryPanelIndicator(text: "屏幕常亮", systemImage: "display")
)
plugin.setKeepDisplayOn(false)
XCTAssertEqual(factory.sessions[0].displaySleepPreventionUpdates, [true, false])
XCTAssertNil(storage.values["keep-display-on"])
XCTAssertEqual(plugin.primaryPanelState.subtitle, "不会自动停止")
XCTAssertNil(plugin.primaryPanelIndicator)
}
func testKeepDisplayOnSettingAppliesToFutureSession() {
let storage = KeepAwakeMemoryStorage()
let factory = KeepAwakeSessionFactory()
let plugin = factory.makePlugin(storage: storage)
plugin.setKeepDisplayOn(true)
XCTAssertEqual(storage.values["keep-display-on"] as? Bool, true)
XCTAssertTrue(factory.sessions.isEmpty)
XCTAssertFalse(plugin.primaryPanelState.isOn)
XCTAssertNil(plugin.primaryPanelIndicator)
plugin.handleAction(.setSwitch(true))
XCTAssertEqual(factory.sessions.count, 1)
XCTAssertEqual(factory.sessions[0].startedConfigurations.last?.preventDisplaySleep, true)
XCTAssertNotNil(plugin.primaryPanelIndicator)
}
func testKeepDisplayOnUpdatePreservesTimedSessionEndDate() {
let storage = KeepAwakeMemoryStorage()
let factory = KeepAwakeSessionFactory()
let plugin = factory.makePlugin(storage: storage)
plugin.handleAction(.setSwitch(true))
plugin.handleAction(.setSelection(controlID: "duration", optionID: "oneHour"))
let session = factory.sessions[0]
let scheduledEndDate = session.startedConfigurations.last?.endDate
XCTAssertNotNil(scheduledEndDate)
plugin.setKeepDisplayOn(true)
XCTAssertEqual(session.startedConfigurations.count, 2)
XCTAssertEqual(session.startedConfigurations.last?.endDate, scheduledEndDate)
XCTAssertEqual(session.displaySleepPreventionUpdates, [true])
}
func testFailedKeepDisplayOnUpdateKeepsAppliedPreferenceAndCanRetry() {
let storage = KeepAwakeMemoryStorage()
let factory = KeepAwakeSessionFactory()
let plugin = factory.makePlugin(storage: storage)
plugin.handleAction(.setSwitch(true))
let session = factory.sessions[0]
session.displayUpdateError = MockKeepAwakeSessionError.displayUpdateFailed
plugin.setKeepDisplayOn(true)
XCTAssertEqual(session.displaySleepPreventionUpdates, [true])
XCTAssertNil(storage.values["keep-display-on"])
XCTAssertEqual(plugin.primaryPanelState.subtitle, "不会自动停止")
XCTAssertNotNil(plugin.primaryPanelState.errorMessage)
session.displayUpdateError = nil
plugin.setKeepDisplayOn(true)
XCTAssertEqual(session.displaySleepPreventionUpdates, [true, true])
XCTAssertEqual(storage.values["keep-display-on"] as? Bool, true)
XCTAssertEqual(plugin.primaryPanelState.subtitle, "不会自动停止")
XCTAssertNil(plugin.primaryPanelState.errorMessage)
}
func testKeepDisplayOnPreferenceRestoresWithPermanentSession() {
let storage = KeepAwakeMemoryStorage()
let firstFactory = KeepAwakeSessionFactory()
let firstPlugin = firstFactory.makePlugin(storage: storage)
firstPlugin.handleAction(.setSwitch(true))
firstPlugin.setKeepDisplayOn(true)
firstPlugin.deactivate(reason: .hostShutdown)
let secondFactory = KeepAwakeSessionFactory()
let secondPlugin = secondFactory.makePlugin(storage: storage)
secondPlugin.activate(context: Self.context(storage: storage))
XCTAssertTrue(secondPlugin.primaryPanelState.isOn)
XCTAssertEqual(secondFactory.sessions[0].startedConfigurations.last?.preventDisplaySleep, true)
XCTAssertEqual(storage.values["keep-display-on"] as? Bool, true)
}
func testKeepDisplayOnPreferenceSurvivesSessionOff() {
let storage = KeepAwakeMemoryStorage()
let factory = KeepAwakeSessionFactory()
let plugin = factory.makePlugin(storage: storage)
plugin.handleAction(.setSwitch(true))
plugin.setKeepDisplayOn(true)
plugin.handleAction(.setSwitch(false))
XCTAssertEqual(storage.values["keep-display-on"] as? Bool, true)
plugin.handleAction(.setSwitch(true))
XCTAssertEqual(factory.sessions.last?.startedConfigurations.last?.preventDisplaySleep, true)
}
func testTimedSessionSubtitleKeepsRemainingAndAbsoluteStopCompact() {
let storage = KeepAwakeMemoryStorage()
let factory = KeepAwakeSessionFactory()
let plugin = factory.makePlugin(storage: storage)
plugin.handleAction(.setSwitch(true))
plugin.handleAction(.setSelection(controlID: "duration", optionID: "twoHours"))
let subtitle = plugin.primaryPanelState.subtitle
XCTAssertTrue(subtitle.contains(":"), subtitle)
XCTAssertTrue(subtitle.contains("·"), subtitle)
XCTAssertTrue(subtitle.contains("剩余"), subtitle)
XCTAssertEqual(subtitle.filter { $0 == "·" }.count, 1, subtitle)
}
private static func context(storage: KeepAwakeMemoryStorage) -> PluginRuntimeContext {
PluginRuntimeContext(pluginID: "keep-awake", storage: storage)
}
}
final class KeepAwakeStopScheduleFormattingTests: XCTestCase {
private let localization = PluginLocalization(bundle: .main)
private var calendar: Calendar {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
return calendar
}
private var locale: Locale { Locale(identifier: "en_US_POSIX") }
func testSameDayStopUsesTimeOnly() {
let reference = date(year: 2026, month: 7, day: 10, hour: 14, minute: 0)
let end = date(year: 2026, month: 7, day: 10, hour: 16, minute: 30)
let label = KeepAwakeStopScheduleFormatting.absoluteStopLabel(
until: end,
referenceDate: reference,
calendar: calendar,
localization: localization,
locale: locale
)
XCTAssertTrue(label.contains("4:30"), label)
XCTAssertFalse(label.contains("16:30"), label)
}
func testSameDayStopUsesLocalePreferred24HourClock() {
let reference = date(year: 2026, month: 7, day: 10, hour: 14, minute: 0)
let end = date(year: 2026, month: 7, day: 10, hour: 16, minute: 30)
let label = KeepAwakeStopScheduleFormatting.absoluteStopLabel(
until: end,
referenceDate: reference,
calendar: calendar,
localization: localization,
locale: Locale(identifier: "en_GB")
)
XCTAssertTrue(label.contains("16:30"), label)
}
func testNextDayStopUsesTomorrowPrefix() {
let reference = date(year: 2026, month: 7, day: 10, hour: 22, minute: 0)
let end = date(year: 2026, month: 7, day: 11, hour: 1, minute: 15)
let label = KeepAwakeStopScheduleFormatting.absoluteStopLabel(
until: end,
referenceDate: reference,
calendar: calendar,
localization: localization,
locale: locale
)
// PluginLocalization falls back to default Chinese copy when the key is not in the test host bundle.
XCTAssertTrue(label.contains("1:15"), label)
XCTAssertTrue(
label.contains("Tomorrow") || label.contains("明天"),
label
)
}
func testLaterDayStopIncludesDate() {
let reference = date(year: 2026, month: 7, day: 10, hour: 12, minute: 0)
let end = date(year: 2026, month: 7, day: 12, hour: 9, minute: 5)
let label = KeepAwakeStopScheduleFormatting.absoluteStopLabel(
until: end,
referenceDate: reference,
calendar: calendar,
localization: localization,
locale: locale
)
XCTAssertTrue(label.contains("9:05"), label)
XCTAssertFalse(label.contains("Tomorrow") || label.contains("明天"), label)
XCTAssertTrue(label.contains("Jul") || label.contains("7") || label.contains("12"), label)
}
private func date(year: Int, month: Int, day: Int, hour: Int, minute: Int) -> Date {
var components = DateComponents()
components.year = year
components.month = month
components.day = day
components.hour = hour
components.minute = minute
return calendar.date(from: components)!
}
}
@MainActor
final class KeepAwakeSessionTests: XCTestCase {
func testDisplayAssertionFailureDoesNotPreventSessionStart() throws {
var createdKinds: [KeepAwakeSession.AssertionKind] = []
var releasedAssertionIDs: [IOPMAssertionID] = []
let session = KeepAwakeSession(
onEnd: { _ in },
assertionCreator: { kind in
createdKinds.append(kind)
switch kind {
case .system:
return (kIOReturnSuccess, 1)
case .display:
return (kIOReturnError, 0)
}
},
assertionReleaser: { assertionID in
releasedAssertionIDs.append(assertionID)
return kIOReturnSuccess
}
)
try session.start(until: nil, preventDisplaySleep: true)
session.requestStop(reason: .userRequested)
XCTAssertEqual(createdKinds, [.system, .display])
XCTAssertEqual(releasedAssertionIDs, [1])
}
func testDisplayAssertionCanBeUpdatedWithoutRestartingSystemAssertion() throws {
var createdKinds: [KeepAwakeSession.AssertionKind] = []
var releasedAssertionIDs: [IOPMAssertionID] = []
var nextAssertionID = IOPMAssertionID(1)
let session = KeepAwakeSession(
onEnd: { _ in },
assertionCreator: { kind in
createdKinds.append(kind)
defer { nextAssertionID += 1 }
return (kIOReturnSuccess, nextAssertionID)
},
assertionReleaser: { assertionID in
releasedAssertionIDs.append(assertionID)
return kIOReturnSuccess
}
)
try session.start(until: nil, preventDisplaySleep: false)
try session.setPreventDisplaySleep(true)
try session.setPreventDisplaySleep(false)
session.requestStop(reason: .userRequested)
XCTAssertEqual(createdKinds, [.system, .display])
XCTAssertEqual(releasedAssertionIDs, [2, 1])
}
func testFailedDisplayAssertionCreationCanBeRetried() throws {
var displayCreationAttempts = 0
var releasedAssertionIDs: [IOPMAssertionID] = []
let session = KeepAwakeSession(
onEnd: { _ in },
assertionCreator: { kind in
switch kind {
case .system:
return (kIOReturnSuccess, 1)
case .display:
displayCreationAttempts += 1
return displayCreationAttempts == 1
? (kIOReturnError, 0)
: (kIOReturnSuccess, 2)
}
},
assertionReleaser: { assertionID in
releasedAssertionIDs.append(assertionID)
return kIOReturnSuccess
}
)
try session.start(until: nil, preventDisplaySleep: false)
XCTAssertThrowsError(try session.setPreventDisplaySleep(true))
try session.setPreventDisplaySleep(true)
session.requestStop(reason: .userRequested)
XCTAssertEqual(displayCreationAttempts, 2)
XCTAssertEqual(releasedAssertionIDs, [2, 1])
}
func testFailedDisplayAssertionReleaseRetainsAssertionForRetry() throws {
var shouldFailDisplayRelease = true
var releasedAssertionIDs: [IOPMAssertionID] = []
let session = KeepAwakeSession(
onEnd: { _ in },
assertionCreator: { kind in
switch kind {
case .system:
return (kIOReturnSuccess, 1)
case .display:
return (kIOReturnSuccess, 2)
}
},
assertionReleaser: { assertionID in
releasedAssertionIDs.append(assertionID)
if assertionID == 2, shouldFailDisplayRelease {
return kIOReturnError
}
return kIOReturnSuccess
}
)
try session.start(until: nil, preventDisplaySleep: true)
XCTAssertThrowsError(try session.setPreventDisplaySleep(false))
shouldFailDisplayRelease = false
try session.setPreventDisplaySleep(false)
session.requestStop(reason: .userRequested)
XCTAssertEqual(releasedAssertionIDs, [2, 2, 1])
}
}
@MainActor
private final class KeepAwakeSessionFactory {
private(set) var sessions: [MockKeepAwakeSession] = []
func makePlugin(storage: KeepAwakeMemoryStorage) -> KeepAwakePlugin {
KeepAwakePlugin(
context: PluginRuntimeContext(pluginID: "keep-awake", storage: storage),
sessionFactory: { [weak self] _, onEnd in
let session = MockKeepAwakeSession(onEnd: onEnd)
self?.sessions.append(session)
return session
}
)
}
}
@MainActor
private final class MockKeepAwakeSession: KeepAwakeSessionManaging {
struct Configuration: Equatable {
let endDate: Date?
let preventDisplaySleep: Bool
}
private let onEnd: (KeepAwakeSession.EndReason) -> Void
private(set) var startedConfigurations: [Configuration] = []
private(set) var displaySleepPreventionUpdates: [Bool] = []
private(set) var stopRequestCount = 0
var displayUpdateError: Error?
init(onEnd: @escaping (KeepAwakeSession.EndReason) -> Void) {
self.onEnd = onEnd
}
func start(until endDate: Date?, preventDisplaySleep: Bool) throws {
startedConfigurations.append(
Configuration(endDate: endDate, preventDisplaySleep: preventDisplaySleep)
)
}
func setPreventDisplaySleep(_ preventDisplaySleep: Bool) throws {
displaySleepPreventionUpdates.append(preventDisplaySleep)
if let displayUpdateError {
throw displayUpdateError
}
}
func requestStop(reason: KeepAwakeSession.EndReason) {
stopRequestCount += 1
onEnd(reason)
}
}
private enum MockKeepAwakeSessionError: LocalizedError {
case displayUpdateFailed
var errorDescription: String? {
"无法更新屏幕状态。"
}
}
@MainActor
private final class KeepAwakeMemoryStorage: PluginStorage {
var values: [String: Any] = [:]
func object(forKey key: String) -> Any? { values[key] }
func data(forKey key: String) -> Data? { values[key] as? Data }
func string(forKey key: String) -> String? { values[key] as? String }
func stringArray(forKey key: String) -> [String]? { values[key] as? [String] }
func integer(forKey key: String) -> Int { values[key] as? Int ?? 0 }
func bool(forKey key: String) -> Bool { values[key] as? Bool ?? false }
func set(_ value: Any?, forKey key: String) {
guard let value else {
values.removeValue(forKey: key)
return
}
values[key] = value
}
func removeObject(forKey key: String) {
values.removeValue(forKey: key)
}
func migrateValueIfNeeded(fromLegacyKey legacyKey: String, to key: String) {
guard values[key] == nil, let value = values[legacyKey] else {
return
}
values[key] = value
values.removeValue(forKey: legacyKey)
}
}