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

291 lines
10 KiB
Swift

import XCTest
@testable import MacTools
@testable import DiskCleanPlugin
final class DiskCleanScannerTests: XCTestCase {
private let home = "/Users/tester"
func testScanReturnsOnlySelectedChoicesAndNeverDeletesDuringDryRun() async throws {
let cachePath = "\(home)/Library/Caches/App"
let developerPath = "\(home)/Library/Caches/DeveloperTool"
let fileSystem = FakeDiskCleanFileSystem(
expansions: [
"\(home)/Library/Caches/*": [item(cachePath), item(developerPath)],
"\(home)/Library/Developer/*": [item(developerPath)]
],
sizes: [
cachePath: 10,
developerPath: 20
]
)
let catalog = DiskCleanRuleCatalog(rules: [
DiskCleanRule(
id: "cache-rule",
choice: .cache,
title: "Cache",
risk: .low,
targets: [.path("\(home)/Library/Caches/*")]
),
DiskCleanRule(
id: "developer-rule",
choice: .developer,
title: "Developer",
risk: .medium,
targets: [.path("\(home)/Library/Developer/*")]
)
])
let scanner = makeScanner(catalog: catalog, fileSystem: fileSystem)
let result = try await scanner.scan(choices: [.cache])
XCTAssertEqual(result.choices, [.cache])
XCTAssertEqual(result.candidates.map(\.choice), [.cache, .cache])
XCTAssertEqual(Set(result.candidates.map(\.path)), [cachePath, developerPath])
XCTAssertTrue(fileSystem.removedPaths.isEmpty)
}
func testScanPreservesWhitelistedProtectedAndInUseCandidatesAsNonCleanable() async throws {
let allowedPath = "\(home)/Library/Caches/Regular"
let whitelistedPath = "\(home)/Library/Caches/KeepMe/data"
let protectedPath = "\(home)/Library/Application Support/Google/Chrome/Default/Cookies"
let runningPath = "\(home)/Library/Caches/Running"
let fileSystem = FakeDiskCleanFileSystem(
expansions: [
"\(home)/Library/Caches/Regular": [item(allowedPath)],
"\(home)/Library/Caches/KeepMe/data": [item(whitelistedPath)],
"\(home)/Library/Application Support/Google/Chrome/Default/Cookies": [item(protectedPath)],
"\(home)/Library/Caches/Running": [item(runningPath)]
],
sizes: [
allowedPath: 10,
whitelistedPath: 20,
protectedPath: 30,
runningPath: 40
]
)
let whitelist = DiskCleanWhitelistStore(
homeDirectory: home,
includeDefaults: false,
customRules: ["\(home)/Library/Caches/KeepMe*"]
)
let catalog = DiskCleanRuleCatalog(rules: [
DiskCleanRule(
id: "allowed",
choice: .cache,
title: "Allowed",
risk: .low,
targets: [.path("\(home)/Library/Caches/Regular")]
),
DiskCleanRule(
id: "whitelisted",
choice: .cache,
title: "Whitelisted",
risk: .low,
targets: [.path("\(home)/Library/Caches/KeepMe/data")]
),
DiskCleanRule(
id: "protected",
choice: .cache,
title: "Protected",
risk: .high,
targets: [.path("\(home)/Library/Application Support/Google/Chrome/Default/Cookies")]
),
DiskCleanRule(
id: "running",
choice: .cache,
title: "Running",
risk: .medium,
targets: [.path("\(home)/Library/Caches/Running")],
skipWhenProcessIsRunning: ["Google Chrome"]
)
])
let scanner = makeScanner(
catalog: catalog,
fileSystem: fileSystem,
whitelistStore: whitelist,
processInspector: FakeDiskCleanProcessInspector(runningProcessNames: ["Google Chrome"])
)
let result = try await scanner.scan(choices: [.cache])
XCTAssertEqual(result.cleanableCandidates.map(\.path), [allowedPath])
XCTAssertEqual(result.cleanableSizeBytes, 10)
XCTAssertEqual(result.protectedCount, 2)
XCTAssertEqual(status(for: whitelistedPath, in: result), .whitelisted(rule: "\(home)/Library/Caches/KeepMe*"))
XCTAssertEqual(status(for: protectedPath, in: result), .protected(reason: "browser profile data is protected"))
XCTAssertEqual(status(for: runningPath, in: result), .inUse(processName: "Google Chrome"))
}
func testScanDeduplicatesParentChildCandidatesBeforeSizingTotals() async throws {
let parent = "\(home)/Library/Caches/App"
let child = "\(home)/Library/Caches/App/Nested"
let sibling = "\(home)/Library/Caches/Other"
let fileSystem = FakeDiskCleanFileSystem(
expansions: [
"\(home)/Library/Caches/App": [item(parent)],
"\(home)/Library/Caches/App/Nested": [item(child)],
"\(home)/Library/Caches/Other": [item(sibling)]
],
sizes: [
parent: 100,
child: 75,
sibling: 25
]
)
let catalog = DiskCleanRuleCatalog(rules: [
DiskCleanRule(
id: "dedupe",
choice: .cache,
title: "Dedupe",
risk: .low,
targets: [
.path("\(home)/Library/Caches/App"),
.path("\(home)/Library/Caches/App/Nested"),
.path("\(home)/Library/Caches/Other")
]
)
])
let scanner = makeScanner(catalog: catalog, fileSystem: fileSystem)
let result = try await scanner.scan(choices: [.cache])
XCTAssertEqual(result.candidates.map(\.path), [parent, sibling])
XCTAssertEqual(result.cleanableSizeBytes, 125)
}
func testScanEmitsProgressMessagesForRulesCandidatesAndCompletion() async throws {
let cachePath = "\(home)/Library/Caches/App"
let fileSystem = FakeDiskCleanFileSystem(
expansions: ["\(home)/Library/Caches/App": [item(cachePath)]],
sizes: [cachePath: 10]
)
let catalog = DiskCleanRuleCatalog(rules: [
DiskCleanRule(
id: "cache",
choice: .cache,
title: "Cache",
risk: .low,
targets: [.path("\(home)/Library/Caches/App")]
)
])
let scanner = makeScanner(catalog: catalog, fileSystem: fileSystem)
let recorder = DiskCleanProgressRecorder()
_ = try await scanner.scan(choices: [.cache]) { message in
await recorder.record(message)
}
let messages = await recorder.messages()
let texts = messages.map(\.text)
XCTAssertTrue(texts.contains("扫描范围:缓存清理"))
XCTAssertTrue(texts.contains("展开规则:Cache"))
XCTAssertTrue(texts.contains("匹配 1 项:Cache"))
XCTAssertTrue(texts.contains("可清理:\(cachePath)"))
XCTAssertEqual(texts.last, "扫描完成:1 项,1 项可清理")
}
private func makeScanner(
catalog: DiskCleanRuleCatalog,
fileSystem: FakeDiskCleanFileSystem,
whitelistStore: DiskCleanWhitelistStore? = nil,
processInspector: DiskCleanProcessInspecting = FakeDiskCleanProcessInspector()
) -> DiskCleanScanner {
DiskCleanScanner(
ruleCatalog: catalog,
fileSystem: fileSystem,
safetyPolicy: DiskCleanSafetyPolicy(
homeDirectory: home,
whitelistStore: whitelistStore ?? DiskCleanWhitelistStore(homeDirectory: home, includeDefaults: false)
),
processInspector: processInspector,
now: { Date(timeIntervalSince1970: 0) }
)
}
private func status(for path: String, in result: DiskCleanScanResult) -> DiskCleanSafetyStatus? {
result.candidates.first { $0.path == path }?.safety
}
private func item(_ path: String, isDirectory: Bool = true) -> DiskCleanFileItem {
DiskCleanFileItem(
path: path,
isDirectory: isDirectory,
isSymlink: false,
resolvedSymlinkTarget: nil
)
}
}
private final class FakeDiskCleanFileSystem: DiskCleanFileSystemProviding, @unchecked Sendable {
let expansions: [String: [DiskCleanFileItem]]
let sizes: [String: Int64]
private(set) var removedPaths: [String] = []
init(
expansions: [String: [DiskCleanFileItem]],
sizes: [String: Int64]
) {
self.expansions = expansions
self.sizes = sizes
}
func expandPathPattern(_ pattern: String) throws -> [DiskCleanFileItem] {
expansions[pattern] ?? []
}
func itemInfo(at path: String) throws -> DiskCleanFileItem? {
expansions.values
.flatMap { $0 }
.first { $0.path == path }
}
func sizeOfItem(at path: String) throws -> Int64 {
sizes[path] ?? 0
}
func removeItem(at path: String) throws {
removedPaths.append(path)
}
func deduplicatedParentChildPaths(_ paths: [String]) -> [String] {
let uniqueSortedPaths = Array(Set(paths)).sorted { lhs, rhs in
if lhs.count == rhs.count {
return lhs < rhs
}
return lhs.count < rhs.count
}
var kept: [String] = []
for path in uniqueSortedPaths {
if kept.contains(where: { path == $0 || path.hasPrefix($0 + "/") }) {
continue
}
kept.append(path)
}
return kept.sorted()
}
}
private struct FakeDiskCleanProcessInspector: DiskCleanProcessInspecting {
var runningProcessNames: Set<String> = []
func runningProcessName(from names: [String]) -> String? {
names.first { runningProcessNames.contains($0) }
}
}
private actor DiskCleanProgressRecorder {
private var recordedMessages: [DiskCleanScanLogMessage] = []
func record(_ message: DiskCleanScanLogMessage) {
recordedMessages.append(message)
}
func messages() -> [DiskCleanScanLogMessage] {
recordedMessages
}
}