Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/ProcessBarMonitor/Localization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ enum L10n {
String(format: string(key), locale: Locale.current, arguments: arguments)
}

private static func localizationCandidates(for language: String) -> [String] {
static func localizationCandidates(for language: String) -> [String] {
let parts = language.split(separator: "-").map(String.init)
guard !parts.isEmpty else { return [language] }

Expand Down
7 changes: 6 additions & 1 deletion Sources/ProcessBarMonitor/SystemMetricsProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,18 @@ actor SystemMetricsProvider {
return extractTemperature(from: raw)
}

private func extractTemperature(from raw: String) -> Double? {
private nonisolated func extractTemperature(from raw: String) -> Double? {
let matches = raw.matches(of: /-?\d+(?:\.\d+)?/)
guard let first = matches.first,
let value = Double(first.output), value > 1, value < 120 else { return nil }
return value
}

/// Test-only: exposes extractTemperature to the test target without breaking actor isolation.
nonisolated func extractTemperatureTest(from raw: String) -> Double? {
extractTemperature(from: raw)
}

private func architectureAndTemperatureNote(temperatureAvailable: Bool, mode: TemperatureMode) -> String {
if temperatureAvailable {
return L10n.format("note.temperature.available", mode.title)
Expand Down
149 changes: 149 additions & 0 deletions Tests/ProcessBarMonitorTests/DiagnosticsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import XCTest
@testable import ProcessBarMonitor

/// Regression tests for ProcessSnapshotDiagnostics state machine (issue #30).
/// Verifies that each mutating method produces the expected side-effects
/// without interfering with other fields.
final class DiagnosticsTests: XCTestCase {

private var diagnostics: ProcessSnapshotDiagnostics!
private let fixedDate = Date(timeIntervalSince1970: 1_234_567_890)

override func setUp() {
super.setUp()
diagnostics = ProcessSnapshotDiagnostics()
}

override func tearDown() {
diagnostics = nil
super.tearDown()
}

// MARK: - Initial state

func testInitialState_allCountersZero() {
XCTAssertEqual(diagnostics.attemptCount, 0)
XCTAssertEqual(diagnostics.successCount, 0)
XCTAssertEqual(diagnostics.failureCount, 0)
}

func testInitialState_allDatesNil() {
XCTAssertNil(diagnostics.lastAttemptAt)
XCTAssertNil(diagnostics.lastSuccessAt)
XCTAssertNil(diagnostics.lastFailureAt)
}

func testInitialState_noErrorMessage() {
XCTAssertNil(diagnostics.lastFailureMessage)
XCTAssertNil(diagnostics.lastFailureDetails)
}

func testInitialState_zeroProcessCounts() {
XCTAssertEqual(diagnostics.lastSnapshotProcessCount, 0)
XCTAssertEqual(diagnostics.lastTopCPUCount, 0)
XCTAssertEqual(diagnostics.lastTopMemoryCount, 0)
}

// MARK: - markAttempt

func testMarkAttempt_incrementsAttemptCount() {
diagnostics.markAttempt(at: fixedDate)
XCTAssertEqual(diagnostics.attemptCount, 1)
XCTAssertEqual(diagnostics.lastAttemptAt, fixedDate)
}

func testMarkAttempt_multipleCallsAccumulate() {
let d1 = Date(timeIntervalSince1970: 1)
let d2 = Date(timeIntervalSince1970: 2)
let d3 = Date(timeIntervalSince1970: 3)
diagnostics.markAttempt(at: d1)
diagnostics.markAttempt(at: d2)
diagnostics.markAttempt(at: d3)
XCTAssertEqual(diagnostics.attemptCount, 3)
XCTAssertEqual(diagnostics.lastAttemptAt, d3)
}

func testMarkAttempt_doesNotAffectSuccessFailureCounts() {
diagnostics.markAttempt(at: fixedDate)
XCTAssertEqual(diagnostics.successCount, 0)
XCTAssertEqual(diagnostics.failureCount, 0)
}

// MARK: - markSuccess

func testMarkSuccess_incrementsSuccessCount() {
diagnostics.markSuccess(processCount: 42, topCPUCount: 5, topMemoryCount: 5, at: fixedDate)
XCTAssertEqual(diagnostics.successCount, 1)
}

func testMarkSuccess_setsLastSuccessAt() {
diagnostics.markSuccess(processCount: 42, topCPUCount: 5, topMemoryCount: 5, at: fixedDate)
XCTAssertEqual(diagnostics.lastSuccessAt, fixedDate)
}

func testMarkSuccess_capturesProcessCounts() {
diagnostics.markSuccess(processCount: 42, topCPUCount: 8, topMemoryCount: 12, at: fixedDate)
XCTAssertEqual(diagnostics.lastSnapshotProcessCount, 42)
XCTAssertEqual(diagnostics.lastTopCPUCount, 8)
XCTAssertEqual(diagnostics.lastTopMemoryCount, 12)
}

func testMarkSuccess_doesNotAffectAttemptOrFailureCount() {
diagnostics.markSuccess(processCount: 1, topCPUCount: 1, topMemoryCount: 1, at: fixedDate)
XCTAssertEqual(diagnostics.attemptCount, 0)
XCTAssertEqual(diagnostics.failureCount, 0)
}

// MARK: - markFailure

func testMarkFailure_incrementsFailureCount() {
diagnostics.markFailure(message: "ps exited 1", details: "signal 9", at: fixedDate)
XCTAssertEqual(diagnostics.failureCount, 1)
}

func testMarkFailure_setsLastFailureAt() {
diagnostics.markFailure(message: "ps exited 1", details: "signal 9", at: fixedDate)
XCTAssertEqual(diagnostics.lastFailureAt, fixedDate)
}

func testMarkFailure_capturesMessageAndDetails() {
diagnostics.markFailure(message: "access denied", details: "PermissionError", at: fixedDate)
XCTAssertEqual(diagnostics.lastFailureMessage, "access denied")
XCTAssertEqual(diagnostics.lastFailureDetails, "PermissionError")
}

func testMarkFailure_doesNotAffectAttemptOrSuccessCount() {
diagnostics.markFailure(message: "err", details: "detail", at: fixedDate)
XCTAssertEqual(diagnostics.attemptCount, 0)
XCTAssertEqual(diagnostics.successCount, 0)
}

// MARK: - Full lifecycle

func testFullLifecycle_attemptThenSuccess() {
let d1 = Date(timeIntervalSince1970: 10)
let d2 = Date(timeIntervalSince1970: 20)
diagnostics.markAttempt(at: d1)
diagnostics.markSuccess(processCount: 10, topCPUCount: 5, topMemoryCount: 5, at: d2)

XCTAssertEqual(diagnostics.attemptCount, 1)
XCTAssertEqual(diagnostics.successCount, 1)
XCTAssertEqual(diagnostics.failureCount, 0)
XCTAssertEqual(diagnostics.lastAttemptAt, d1)
XCTAssertEqual(diagnostics.lastSuccessAt, d2)
}

func testFullLifecycle_attemptThenFailure() {
let d1 = Date(timeIntervalSince1970: 10)
let d2 = Date(timeIntervalSince1970: 20)
diagnostics.markAttempt(at: d1)
diagnostics.markFailure(message: "err", details: "det", at: d2)

XCTAssertEqual(diagnostics.attemptCount, 1)
XCTAssertEqual(diagnostics.successCount, 0)
XCTAssertEqual(diagnostics.failureCount, 1)
XCTAssertEqual(diagnostics.lastAttemptAt, d1)
XCTAssertEqual(diagnostics.lastFailureAt, d2)
XCTAssertEqual(diagnostics.lastFailureMessage, "err")
}
}
99 changes: 99 additions & 0 deletions Tests/ProcessBarMonitorTests/EnumSavedValueTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import XCTest
@testable import ProcessBarMonitor

/// Regression tests for legacy settings migration via savedValue initialisers
/// on TemperatureMode and MenuBarDisplayMode (issue #30).
///
/// These initialisers bridge old display-name strings (e.g. "Hottest CPU")
/// stored in UserDefaults with the new rawValue strings (e.g. "hottestCPU"),
/// ensuring users' existing preferences are preserved after UI rename.
///
final class EnumSavedValueTests: XCTestCase {

// MARK: - TemperatureMode — modern rawValue strings

func testTemperatureMode_modernHottestCPU() {
XCTAssertEqual(TemperatureMode(savedValue: "hottestCPU"), .hottestCPU)
}

func testTemperatureMode_modernAverageCPU() {
XCTAssertEqual(TemperatureMode(savedValue: "averageCPU"), .averageCPU)
}

func testTemperatureMode_modernHottestSoC() {
XCTAssertEqual(TemperatureMode(savedValue: "hottestSoC"), .hottestSoC)
}

// MARK: - TemperatureMode — legacy display-name strings

func testTemperatureMode_legacyHottestCPU() {
XCTAssertEqual(TemperatureMode(savedValue: "Hottest CPU"), .hottestCPU)
}

func testTemperatureMode_legacyAverageCPU() {
XCTAssertEqual(TemperatureMode(savedValue: "Average CPU"), .averageCPU)
}

func testTemperatureMode_legacyHottestSoC() {
XCTAssertEqual(TemperatureMode(savedValue: "Hottest SoC"), .hottestSoC)
}

// MARK: - TemperatureMode — invalid / unknown strings

func testTemperatureMode_invalidString() {
XCTAssertNil(TemperatureMode(savedValue: "not a mode"))
}

func testTemperatureMode_emptyString() {
XCTAssertNil(TemperatureMode(savedValue: ""))
}

func testTemperatureMode_caseSensitive() {
// Modern rawValue is lowercase-first camel.
XCTAssertNil(TemperatureMode(savedValue: "HottestCPU"))
XCTAssertNil(TemperatureMode(savedValue: "HOTTESTCPU"))
}

// MARK: - MenuBarDisplayMode — modern rawValue strings

func testMenuBarDisplayMode_modernCompact() {
XCTAssertEqual(MenuBarDisplayMode(savedValue: "compact"), .compact)
}

func testMenuBarDisplayMode_modernLabeled() {
XCTAssertEqual(MenuBarDisplayMode(savedValue: "labeled"), .labeled)
}

func testMenuBarDisplayMode_modernTemperatureFirst() {
XCTAssertEqual(MenuBarDisplayMode(savedValue: "temperatureFirst"), .temperatureFirst)
}

// MARK: - MenuBarDisplayMode — legacy display-name strings

func testMenuBarDisplayMode_legacyCompact() {
XCTAssertEqual(MenuBarDisplayMode(savedValue: "Compact"), .compact)
}

func testMenuBarDisplayMode_legacyLabeled() {
XCTAssertEqual(MenuBarDisplayMode(savedValue: "Labeled"), .labeled)
}

func testMenuBarDisplayMode_legacyTemperatureFirst() {
XCTAssertEqual(MenuBarDisplayMode(savedValue: "Temperature First"), .temperatureFirst)
}

// MARK: - MenuBarDisplayMode — invalid / unknown strings

func testMenuBarDisplayMode_invalidString() {
XCTAssertNil(MenuBarDisplayMode(savedValue: "foobar"))
}

func testMenuBarDisplayMode_emptyString() {
XCTAssertNil(MenuBarDisplayMode(savedValue: ""))
}

func testMenuBarDisplayMode_caseSensitive() {
XCTAssertNil(MenuBarDisplayMode(savedValue: "compact "))
XCTAssertNil(MenuBarDisplayMode(savedValue: "COMPACT"))
}
}
97 changes: 97 additions & 0 deletions Tests/ProcessBarMonitorTests/LocalizationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import XCTest
@testable import ProcessBarMonitor

/// Regression tests for localisation candidate chain (issue #30).
/// Verifies the fallback precedence order in L10n.localizationCandidates.
final class LocalizationTests: XCTestCase {

// MARK: - English variants

func testLocalizationCandidates_enUS() {
let got = L10n.localizationCandidatesTest(for: "en-US")
XCTAssertEqual(got, ["en-US", "en"])
}

func testLocalizationCandidates_enGB() {
let got = L10n.localizationCandidatesTest(for: "en-GB")
XCTAssertEqual(got, ["en-GB", "en"])
}

func testLocalizationCandidates_enAU() {
let got = L10n.localizationCandidatesTest(for: "en-AU")
XCTAssertEqual(got, ["en-AU", "en"])
}

// MARK: - Chinese variants

func testLocalizationCandidates_zhHansCN() {
let got = L10n.localizationCandidatesTest(for: "zh-Hans-CN")
// Order: zh-Hans-CN (full), zh-Hans (script), zh (language)
XCTAssertEqual(got, ["zh-Hans-CN", "zh-Hans", "zh"])
}

func testLocalizationCandidates_zhHantTW() {
let got = L10n.localizationCandidatesTest(for: "zh-Hant-TW")
XCTAssertEqual(got, ["zh-Hant-TW", "zh-Hant", "zh"])
}

func testLocalizationCandidates_zhHans() {
let got = L10n.localizationCandidatesTest(for: "zh-Hans")
XCTAssertEqual(got, ["zh-Hans", "zh"])
}

func testLocalizationCandidates_zhHant() {
let got = L10n.localizationCandidatesTest(for: "zh-Hant")
XCTAssertEqual(got, ["zh-Hant", "zh"])
}

func testLocalizationCandidates_zh() {
let got = L10n.localizationCandidatesTest(for: "zh")
XCTAssertEqual(got, ["zh"])
}

// MARK: - Other languages

func testLocalizationCandidates_de() {
let got = L10n.localizationCandidatesTest(for: "de")
XCTAssertEqual(got, ["de"])
}

func testLocalizationCandidates_frFR() {
let got = L10n.localizationCandidatesTest(for: "fr-FR")
XCTAssertEqual(got, ["fr-FR", "fr"])
}

func testLocalizationCandidates_jaJP() {
let got = L10n.localizationCandidatesTest(for: "ja-JP")
XCTAssertEqual(got, ["ja-JP", "ja"])
}

// MARK: - Deduplication

func testLocalizationCandidates_noDuplicates() {
// en-US produces ["en-us", "en"] — no duplicate "en".
let got = L10n.localizationCandidatesTest(for: "en-US")
XCTAssertEqual(got.count, Set<String>(got).count, "No duplicate entries expected")
}

func testLocalizationCandidates_zhHansCN_noDuplicates() {
let got = L10n.localizationCandidatesTest(for: "zh-Hans-CN")
XCTAssertEqual(got.count, Set<String>(got).count, "No duplicate entries expected")
}

// MARK: - Language-only input (no region)

func testLocalizationCandidates_languageOnly() {
let got = L10n.localizationCandidatesTest(for: "de")
XCTAssertEqual(got, ["de"])
}

// MARK: - Empty / malformed

func testLocalizationCandidates_emptyString() {
let got = L10n.localizationCandidatesTest(for: "")
// Empty string is treated as-is (matches guard clause in implementation).
XCTAssertEqual(got, [""])
}
}
Loading