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

181 lines
6.6 KiB
Swift

import XCTest
@testable import MacTools
@testable import DiskCleanPlugin
final class DiskCleanExecutorTests: XCTestCase {
private let home = "/Users/tester"
func testCleanRemovesOnlySelectedAllowedCandidatesAndCountsReclaimedBytes() async throws {
let removable = candidate(id: "remove", path: "\(home)/Library/Caches/Remove", size: 10)
let unselected = candidate(id: "unselected", path: "\(home)/Library/Caches/Unselected", size: 20)
let protected = candidate(
id: "protected",
path: "\(home)/Library/Caches/Protected",
size: 30,
safety: .protected(reason: "protected")
)
let failed = candidate(id: "failed", path: "\(home)/Library/Caches/Failed", size: 40)
let fileSystem = FakeDiskCleanExecutorFileSystem(
items: [
removable.path: item(removable.path),
unselected.path: item(unselected.path),
protected.path: item(protected.path),
failed.path: item(failed.path)
],
removeErrors: [failed.path: TestRemoveError()]
)
let executor = makeExecutor(fileSystem: fileSystem)
let result = try await executor.clean(
candidates: [removable, unselected, protected, failed],
selectedCandidateIDs: [removable.id, protected.id, failed.id]
)
XCTAssertEqual(fileSystem.removedPaths, [removable.path, failed.path])
XCTAssertEqual(result.removedCount, 1)
XCTAssertEqual(result.skippedCount, 1)
XCTAssertEqual(result.failedCount, 1)
XCTAssertEqual(result.reclaimedBytes, 10)
XCTAssertEqual(result.itemResults.map(\.candidateID), [removable.id, protected.id, failed.id])
}
func testCleanRevalidatesEachPathImmediatelyBeforeDeletion() async throws {
let safe = candidate(id: "safe", path: "\(home)/Library/Caches/Safe", size: 10)
let changed = candidate(id: "changed", path: "\(home)/Library/Caches/Changed", size: 20)
let missing = candidate(id: "missing", path: "\(home)/Library/Caches/Missing", size: 30)
let fileSystem = FakeDiskCleanExecutorFileSystem(
items: [
safe.path: item(safe.path),
changed.path: item(changed.path, isSymlink: true, resolvedSymlinkTarget: "/System")
]
)
let executor = makeExecutor(fileSystem: fileSystem)
let result = try await executor.clean(
candidates: [safe, changed, missing],
selectedCandidateIDs: [safe.id, changed.id, missing.id]
)
XCTAssertEqual(fileSystem.removedPaths, [safe.path])
XCTAssertEqual(result.removedCount, 1)
XCTAssertEqual(result.skippedCount, 2)
XCTAssertEqual(result.failedCount, 0)
XCTAssertEqual(outcome(for: changed.id, in: result), .skipped(.invalid(reason: "symlink points to protected system path")))
XCTAssertEqual(outcome(for: missing.id, in: result), .skipped(.invalid(reason: "path no longer exists")))
}
func testCleanSkipsEveryNonCleanableSafetyStatus() async throws {
let candidates = [
candidate(id: "whitelisted", path: "\(home)/Library/Caches/White", safety: .whitelisted(rule: "rule")),
candidate(id: "protected", path: "\(home)/Library/Caches/Protected", safety: .protected(reason: "protected")),
candidate(id: "invalid", path: "\(home)/Library/Caches/Invalid", safety: .invalid(reason: "invalid")),
candidate(id: "admin", path: "\(home)/Library/Caches/Admin", safety: .requiresAdmin(reason: "admin")),
candidate(id: "in-use", path: "\(home)/Library/Caches/InUse", safety: .inUse(processName: "App"))
]
let fileSystem = FakeDiskCleanExecutorFileSystem(
items: Dictionary(uniqueKeysWithValues: candidates.map { ($0.path, item($0.path)) })
)
let executor = makeExecutor(fileSystem: fileSystem)
let result = try await executor.clean(
candidates: candidates,
selectedCandidateIDs: Set(candidates.map(\.id))
)
XCTAssertTrue(fileSystem.removedPaths.isEmpty)
XCTAssertEqual(result.removedCount, 0)
XCTAssertEqual(result.skippedCount, candidates.count)
XCTAssertEqual(result.failedCount, 0)
XCTAssertEqual(result.reclaimedBytes, 0)
}
private func makeExecutor(fileSystem: FakeDiskCleanExecutorFileSystem) -> DiskCleanExecutor {
DiskCleanExecutor(
fileSystem: fileSystem,
safetyPolicy: DiskCleanSafetyPolicy(
homeDirectory: home,
whitelistStore: DiskCleanWhitelistStore(homeDirectory: home, includeDefaults: false)
)
)
}
private func candidate(
id: String,
path: String,
size: Int64 = 1,
safety: DiskCleanSafetyStatus = .allowed
) -> DiskCleanCandidate {
DiskCleanCandidate(
id: id,
ruleID: "rule",
choice: .cache,
title: id,
path: path,
sizeBytes: size,
safety: safety,
risk: .low
)
}
private func item(
_ path: String,
isSymlink: Bool = false,
resolvedSymlinkTarget: String? = nil
) -> DiskCleanFileItem {
DiskCleanFileItem(
path: path,
isDirectory: true,
isSymlink: isSymlink,
resolvedSymlinkTarget: resolvedSymlinkTarget
)
}
private func outcome(
for candidateID: String,
in result: DiskCleanExecutionResult
) -> DiskCleanExecutionItemResult.Outcome? {
result.itemResults.first { $0.candidateID == candidateID }?.outcome
}
}
private struct TestRemoveError: Error {}
private final class FakeDiskCleanExecutorFileSystem: DiskCleanFileSystemProviding, @unchecked Sendable {
var items: [String: DiskCleanFileItem]
var removeErrors: [String: Error]
private(set) var removedPaths: [String] = []
init(
items: [String: DiskCleanFileItem],
removeErrors: [String: Error] = [:]
) {
self.items = items
self.removeErrors = removeErrors
}
func expandPathPattern(_ pattern: String) throws -> [DiskCleanFileItem] {
[]
}
func itemInfo(at path: String) throws -> DiskCleanFileItem? {
items[path]
}
func sizeOfItem(at path: String) throws -> Int64 {
0
}
func removeItem(at path: String) throws {
removedPaths.append(path)
if let error = removeErrors[path] {
throw error
}
items[path] = nil
}
func deduplicatedParentChildPaths(_ paths: [String]) -> [String] {
paths
}
}