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
4 changes: 3 additions & 1 deletion mac-app/Sources/CloudMachineCore/MachineBudget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,9 @@ public enum MachineBudget {
/// Whether the Time Machine quota matches the limit. `nil` quota = none set.
public static func quotaMatches(currentQuotaGB: Double?, limitGB: Int) -> Bool {
guard let currentQuotaGB else { return false }
return Int(currentQuotaGB.rounded()) == timeMachineQuotaGB(forLimitGB: limitGB)
// tmutil rounds what it prints (`2,45 TB` = anything from 2445 to 2455).
let wanted = Double(timeMachineQuotaGB(forLimitGB: limitGB))
return abs(currentQuotaGB - wanted) <= max(1, wanted * 0.005)
}

/// One status line: usage against the limit, or why there is none.
Expand Down
21 changes: 18 additions & 3 deletions mac-app/Sources/CloudMachineCore/TimeMachineStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,7 @@ public enum TimeMachineStatus {
case "URL": block.url = value
case "Mount Point": block.mountPoint = value
case "ID": block.id = value
case "Quota":
let number = value.split(separator: " ").first.map(String.init) ?? value
block.quotaGB = Double(number)
case "Quota": block.quotaGB = quotaGB(value)
default: break
}
current = block
Expand All @@ -263,6 +261,23 @@ public enum TimeMachineStatus {
return result
}

/// `Quota` as tmutil prints it, in GB. tmutil formats it for the user's
/// region whatever `LC_ALL` says: 2450 GB came out as `2,45 TB` on
/// 27.10.2026, and reading only "<number> GB" took that for no quota.
/// Decimal units, as macOS shows sizes. `nil` = not a size.
static func quotaGB(_ value: String) -> Double? {
let parts = value.split(separator: " ")
guard let first = parts.first,
let number = Double(first.replacingOccurrences(of: ",", with: "."))
else { return nil }
switch parts.count > 1 ? parts[1].uppercased() : "GB" {
case "TB": return number * 1000
case "GB": return number
case "MB": return number / 1000
default: return nil
}
}

/// The registered destinations; `nil` = tmutil DID NOT ANSWER, `[]` = it
/// answered that there are none.
public static func destinationsReading() async -> [Destination]? {
Expand Down
14 changes: 14 additions & 0 deletions mac-app/Tests/CloudMachineAppTests/TimeMachineStatusTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,18 @@ final class TimeMachineStatusTests: XCTestCase {
XCTAssertEqual(TimeMachineStatus.destinations(destinationInfoOutput: answer!), [])
}
}

/// 27.10.2026: tmutil printed the 2450 GB quota as `2,45 TB` (Polish
/// region), and the status kept asking to set a quota that was set.
func testQuotaIsReadInTheUnitTmutilPrints() {
XCTAssertEqual(TimeMachineStatus.quotaGB("300 GB"), 300)
XCTAssertEqual(TimeMachineStatus.quotaGB("2,45 TB"), 2450)
XCTAssertEqual(TimeMachineStatus.quotaGB("2.45 TB"), 2450)
XCTAssertNil(TimeMachineStatus.quotaGB("none"))
let parsed = TimeMachineStatus.destinations(
destinationInfoOutput: "Name : CloudMachine\nQuota : 2,45 TB\n")
XCTAssertEqual(parsed.first?.quotaGB, 2450)
XCTAssertTrue(MachineBudget.quotaMatches(currentQuotaGB: 2450, limitGB: 3500))
XCTAssertFalse(MachineBudget.quotaMatches(currentQuotaGB: 2000, limitGB: 3500))
}
}
Loading