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

450 lines
17 KiB
Swift

import SwiftUI
import XCTest
import MacToolsPluginKit
@testable import MacTools
@testable import SystemStatusPlugin
@MainActor
final class SystemStatusPluginTests: XCTestCase {
private let suiteName = "SystemStatusPluginTests"
override func tearDown() {
UserDefaults(suiteName: suiteName)?.removePersistentDomain(forName: suiteName)
super.tearDown()
}
func testPluginDescriptorUsesExpandedFullWidthSpan() {
let plugin = SystemStatusPlugin(storage: SystemStatusMemoryPluginStorage())
let expectedHeight = PluginComponentPanelLayoutMetrics.default.heightSpan(
fittingContentHeight: SystemStatusComponentLayout.dashboardContentHeight
)
XCTAssertEqual(plugin.metadata.id, "system-status")
XCTAssertEqual(plugin.metadata.title, "系统状态")
XCTAssertEqual(plugin.descriptor.span, PluginComponentSpan(width: 4, height: expectedHeight)!)
}
func testPluginHostIncludesSystemStatusComponentOnlyWhenProvided() {
let host = makePluginHostForTests(
plugins: [SystemStatusPlugin(storage: SystemStatusMemoryPluginStorage())],
suiteName: suiteName
)
XCTAssertTrue(host.componentItems.contains { $0.id == "system-status" })
XCTAssertFalse(host.panelItems.contains { $0.id == "system-status" })
let managementItem = host.featureManagementItems.first { $0.id == "system-status" }
XCTAssertEqual(managementItem?.presentation, .componentPanel)
}
func testPluginHostCanPresentSystemStatusSettings() {
let host = makePluginHostForTests(
plugins: [SystemStatusPlugin(storage: SystemStatusMemoryPluginStorage())],
suiteName: suiteName
)
host.presentPluginConfiguration(pluginID: "system-status")
XCTAssertEqual(host.selectedSettingsDestination, .pluginConfiguration)
XCTAssertEqual(host.selectedFeatureSettingsPane, .configuration("system-status"))
}
func testSystemStatusLayoutUsesTwoColumnCoreMetricGridOrder() {
XCTAssertEqual(SystemStatusComponentLayout.columns, 2)
XCTAssertEqual(SystemStatusComponentLayout.metricRows, 3)
XCTAssertEqual(SystemStatusComponentLayout.cardSpacing, 6)
XCTAssertEqual(SystemStatusComponentLayout.cardContentPadding, 8)
XCTAssertEqual(SystemStatusComponentLayout.dashboardContentHeight, 411)
XCTAssertEqual(
SystemStatusComponentLayout.defaultPanelMetricKinds,
[.cpu, .gpu, .network, .disk, .memory, .battery, .topProcesses]
)
XCTAssertEqual(SystemStatusComponentLayout.defaultMenuBarMetricKinds, [.cpu, .gpu, .network, .disk, .memory, .battery])
XCTAssertEqual(SystemStatusComponentLayout.position(for: .cpu), SystemStatusGridPosition(row: 0, column: 0))
XCTAssertEqual(SystemStatusComponentLayout.position(for: .gpu), SystemStatusGridPosition(row: 0, column: 1))
XCTAssertEqual(SystemStatusComponentLayout.position(for: .network), SystemStatusGridPosition(row: 1, column: 0))
XCTAssertEqual(SystemStatusComponentLayout.position(for: .disk), SystemStatusGridPosition(row: 1, column: 1))
XCTAssertEqual(SystemStatusComponentLayout.position(for: .memory), SystemStatusGridPosition(row: 2, column: 0))
XCTAssertEqual(SystemStatusComponentLayout.position(for: .battery), SystemStatusGridPosition(row: 2, column: 1))
XCTAssertNil(SystemStatusComponentLayout.position(for: .topProcesses))
}
func testSystemStatusLayoutHeightFollowsVisibleMetricRows() {
XCTAssertEqual(SystemStatusComponentLayout.contentHeight(for: []), 96)
XCTAssertEqual(SystemStatusComponentLayout.contentHeight(for: [.cpu]), 99)
XCTAssertEqual(SystemStatusComponentLayout.contentHeight(for: [.cpu, .gpu, .topProcesses]), 201)
}
func testConfigurationDefaultsShowPanelMetricsAndHideMenuBarMetrics() {
let controller = SystemStatusSettingsController(
store: SystemStatusPluginStorageConfigurationStore(storage: SystemStatusMemoryPluginStorage())
)
XCTAssertEqual(
controller.configuration.visiblePanelMetricKinds,
SystemStatusComponentLayout.defaultPanelMetricKinds
)
XCTAssertTrue(controller.configuration.visibleMenuBarMetricKinds.isEmpty)
}
func testConfigurationPersistsVisibilityAndOrder() {
let storage = SystemStatusMemoryPluginStorage()
let controller = SystemStatusSettingsController(
store: SystemStatusPluginStorageConfigurationStore(storage: storage)
)
controller.setPanelMetric(.gpu, visible: false)
controller.movePanelMetric(.battery, toOffset: 0)
controller.setMenuBarMetric(.cpu, visible: true)
controller.setMenuBarMetric(.memory, visible: true)
controller.moveMenuBarMetric(.memory, toOffset: 0)
let restoredController = SystemStatusSettingsController(
store: SystemStatusPluginStorageConfigurationStore(storage: storage)
)
XCTAssertEqual(restoredController.configuration.panelItems.map(\.kind).first, .battery)
XCTAssertFalse(restoredController.configuration.panelItems.first { $0.kind == .gpu }?.isVisible ?? true)
XCTAssertEqual(restoredController.configuration.visibleMenuBarMetricKinds, [.memory, .cpu])
}
func testPluginDescriptorShrinksWhenPanelMetricsAreHidden() {
let controller = SystemStatusSettingsController(
store: SystemStatusPluginStorageConfigurationStore(storage: SystemStatusMemoryPluginStorage())
)
controller.setPanelMetric(.gpu, visible: false)
controller.setPanelMetric(.network, visible: false)
controller.setPanelMetric(.disk, visible: false)
controller.setPanelMetric(.memory, visible: false)
controller.setPanelMetric(.battery, visible: false)
controller.setPanelMetric(.topProcesses, visible: false)
let plugin = SystemStatusPlugin(
settingsController: controller,
storage: SystemStatusMemoryPluginStorage()
)
let expectedHeight = PluginComponentPanelLayoutMetrics.default.heightSpan(
fittingContentHeight: SystemStatusComponentLayout.contentHeight(for: [.cpu])
)
XCTAssertEqual(plugin.descriptor.span, PluginComponentSpan(width: 4, height: expectedHeight)!)
}
func testMenuBarFormatterUsesSelectedOrder() {
var snapshot = SystemStatusSnapshot.empty
snapshot.cpu = SystemStatusCPUSnapshot(
usage: 0.125,
loadAverage1Minute: nil,
temperatureCelsius: 42.4,
systemPowerWatts: nil,
isCollecting: false
)
snapshot.memory = SystemStatusMemorySnapshot(
usedBytes: 6_000,
totalBytes: 10_000,
swapUsedBytes: nil,
swapTotalBytes: nil
)
snapshot.network = SystemStatusNetworkSnapshot(
interfaceName: "en0",
ipAddress: nil,
publicIPAddress: nil,
downloadBytesPerSecond: 1_024,
uploadBytesPerSecond: 2_048,
isConnected: true,
isCollecting: false
)
XCTAssertEqual(
SystemStatusMenuBarMetricsFormatter.text(snapshot: snapshot, kinds: [.memory, .cpu, .network]),
"RAM 60% | CPU 13% 42° | NET ↓1K ↑2K"
)
}
func testProductionSamplingScheduleBalancesForegroundDetailAndBackgroundCost() {
let schedule = SystemStatusSamplingSchedule.production
XCTAssertEqual(schedule.backgroundFastInterval, .seconds(30))
XCTAssertEqual(schedule.menuBarFastInterval, .seconds(3))
XCTAssertEqual(schedule.foregroundFastInterval, .seconds(3))
XCTAssertEqual(schedule.backgroundSlowInterval, 300)
XCTAssertEqual(schedule.menuBarSlowInterval, 3)
XCTAssertEqual(schedule.foregroundSlowInterval, 15)
XCTAssertEqual(schedule.backgroundProcessInterval, 300)
XCTAssertEqual(schedule.foregroundProcessInterval, 15)
XCTAssertEqual(schedule.backgroundHistoryInterval, 300)
XCTAssertEqual(schedule.foregroundHistoryInterval, 60)
}
func testViewModelMergesDiskCapacityAndActivitySamples() async throws {
let viewModel = SystemStatusViewModel(
sampler: StubSystemStatusSampler(),
historyStore: StubSystemStatusHistoryStore(),
schedule: .test
)
await viewModel.refreshSnapshotNow(referenceDate: Date(timeIntervalSince1970: 2_000))
XCTAssertEqual(viewModel.snapshot.disk.usedBytes, 50)
XCTAssertEqual(viewModel.snapshot.disk.totalBytes, 100)
XCTAssertEqual(viewModel.snapshot.disk.readBytesPerSecond, 2_048)
XCTAssertEqual(viewModel.snapshot.disk.writeBytesPerSecond, 1_024)
XCTAssertEqual(viewModel.snapshot.history.last?.diskReadBytesPerSecond, 2_048)
XCTAssertEqual(viewModel.snapshot.history.last?.diskWriteBytesPerSecond, 1_024)
}
func testDisplayHistoryFastPruneKeepsRecentSortedSamples() {
let points = (0..<1_905).map { index in
SystemStatusHistoryPoint(
timestamp: TimeInterval(index),
cpuUsage: Double(index) / 2_000
)
}
let pruned = SystemStatusViewModel.prunedSortedDisplayHistory(
points,
referenceDate: Date(timeIntervalSince1970: 1_904)
)
XCTAssertEqual(pruned.count, 1_800)
XCTAssertEqual(pruned.first?.timestamp, 105)
XCTAssertEqual(pruned.last?.timestamp, 1_904)
}
func testDisplayHistoryFallbackPruneSortsOutOfOrderSamples() {
let points = [
SystemStatusHistoryPoint(timestamp: 12),
SystemStatusHistoryPoint(timestamp: 10),
SystemStatusHistoryPoint(timestamp: 11),
SystemStatusHistoryPoint(timestamp: -2_000),
SystemStatusHistoryPoint(timestamp: 80)
]
let pruned = SystemStatusViewModel.prunedDisplayHistory(
points,
referenceDate: Date(timeIntervalSince1970: 20)
)
XCTAssertEqual(pruned.map(\.timestamp), [10, 11, 12, 80])
}
func testPluginReusesViewModelAcrossComponentViews() {
let viewModel = SystemStatusViewModel(sampler: StubSystemStatusSampler())
let plugin = SystemStatusPlugin(viewModel: viewModel, storage: SystemStatusMemoryPluginStorage())
let first = plugin.makeView(
context: PluginComponentContext(
pluginID: "system-status",
dismiss: {},
isPanelVisible: true
)
)
let second = plugin.makeView(
context: PluginComponentContext(
pluginID: "system-status",
dismiss: {},
isPanelVisible: true
)
)
XCTAssertFalse(String(describing: first).isEmpty)
XCTAssertFalse(String(describing: second).isEmpty)
}
}
private actor StubSystemStatusSampler: SystemStatusSampling {
private(set) var fastCallCount = 0
private(set) var slowCallCount = 0
private(set) var processCallCount = 0
private(set) var publicIPCallCount = 0
var callCounts: (fast: Int, slow: Int, processes: Int, publicIP: Int) {
(fastCallCount, slowCallCount, processCallCount, publicIPCallCount)
}
func collectFast(referenceDate: Date) async -> SystemStatusFastSample {
fastCallCount += 1
return SystemStatusFastSample(
cpu: SystemStatusCPUSnapshot(
usage: min(0.95, 0.20 + Double(fastCallCount) * 0.01),
loadAverage1Minute: 1.42,
temperatureCelsius: 42,
systemPowerWatts: 8.5,
isCollecting: false
),
memory: SystemStatusMemorySnapshot(
usedBytes: 4_000,
totalBytes: 8_000,
swapUsedBytes: 512,
swapTotalBytes: 2_048
),
network: SystemStatusNetworkSnapshot(
interfaceName: "en0",
ipAddress: "192.168.1.2",
publicIPAddress: nil,
downloadBytesPerSecond: 1_024,
uploadBytesPerSecond: 512,
isConnected: true,
isCollecting: false
),
disk: SystemStatusDiskSnapshot(
usedBytes: nil,
totalBytes: nil,
readBytesPerSecond: 2_048,
writeBytesPerSecond: 1_024
)
)
}
func collectSlow() async -> SystemStatusSlowSample {
slowCallCount += 1
return SystemStatusSlowSample(
disk: SystemStatusDiskSnapshot(
usedBytes: 50,
totalBytes: 100,
readBytesPerSecond: nil,
writeBytesPerSecond: nil
),
battery: SystemStatusBatterySnapshot(
isAvailable: true,
level: 0.8,
state: .acPower,
timeRemainingMinutes: nil,
adapterWatts: 70,
batteryPowerWatts: -18.5,
temperatureCelsius: 31,
healthPercent: 96,
cycleCount: 120
),
gpu: SystemStatusGPUSnapshot(
usage: 0.4,
name: "M1 Pro",
temperatureCelsius: 43,
isAvailable: true,
isCollecting: false
),
hardware: SystemStatusHardwareSnapshot(
modelName: "MacBookPro18,3",
chipName: "Apple M1 Pro",
macOSVersion: "macOS 15.0",
uptimeSeconds: 3_600,
totalMemoryBytes: 16_000
)
)
}
func collectTopProcesses(limit: Int) async -> [SystemStatusTopProcess] {
processCallCount += 1
return [
SystemStatusTopProcess(
pid: 1,
displayName: "launchd",
command: "/sbin/launchd",
cpuPercent: 1,
memoryPercent: 0.1,
memoryBytes: 12_582_912
)
]
}
func collectPublicIPAddress() async -> String? {
publicIPCallCount += 1
return "203.0.113.1"
}
}
private actor StubSystemStatusHistoryStore: SystemStatusHistoryStoring {
private(set) var appendedCount = 0
private var points: [SystemStatusHistoryPoint] = []
func load(referenceDate: Date) async -> [SystemStatusHistoryPoint] {
points
}
func append(_ point: SystemStatusHistoryPoint, referenceDate: Date) async -> [SystemStatusHistoryPoint] {
appendedCount += 1
points.append(point)
return points
}
}
@MainActor
private final class SystemStatusMemoryPluginStorage: PluginStorage {
private 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 {
removeObject(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)
}
}
private extension SystemStatusSamplingSchedule {
static let test = SystemStatusSamplingSchedule(
backgroundFastInterval: .milliseconds(20),
menuBarFastInterval: .milliseconds(20),
foregroundFastInterval: .milliseconds(20),
backgroundSlowInterval: 0,
menuBarSlowInterval: 0,
foregroundSlowInterval: 0,
backgroundProcessInterval: 0,
foregroundProcessInterval: 0,
backgroundHistoryInterval: 0,
foregroundHistoryInterval: 0
)
static let foregroundRestart = SystemStatusSamplingSchedule(
backgroundFastInterval: .seconds(30),
menuBarFastInterval: .milliseconds(20),
foregroundFastInterval: .milliseconds(20),
backgroundSlowInterval: 30,
menuBarSlowInterval: 30,
foregroundSlowInterval: 30,
backgroundProcessInterval: 30,
foregroundProcessInterval: 30,
backgroundHistoryInterval: 30,
foregroundHistoryInterval: 30
)
}