apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README | LICENSE

RadrootsFileMaintenanceTests.swift (13504B)


      1 import Darwin
      2 import Foundation
      3 @testable import RadrootsKit
      4 import Testing
      5 
      6 @Test func fileMaintenanceSharedUseExcludesMaintenanceUntilLastRelease() throws {
      7     let f = try FileMaintenanceFixture()
      8     defer { f.remove() }
      9     var first = try #require(try f.coordinator.reserveUse())
     10     var second = try f.coordinator.reserveUse()
     11     #expect(second != nil)
     12     try first.validate()
     13     #expect(try f.coordinator.reserveMaintenance() == nil)
     14     second = nil
     15     #expect(second == nil)
     16     #expect(try f.coordinator.reserveMaintenance() == nil)
     17     // Retain first explicitly through the final contention observation.
     18     withExtendedLifetime(first) {}
     19     // A distinct root remains independent.
     20     first = try #require(try RadrootsAppleFileMaintenance(root: f.base.appendingPathComponent("other")).reserveUse())
     21     let maintenance = try #require(try f.coordinator.reserveMaintenance())
     22     #expect(try f.coordinator.reserveUse() == nil)
     23     #expect(try f.coordinator.reserveMaintenance() == nil)
     24     try maintenance.validate()
     25     withExtendedLifetime((first, maintenance)) {}
     26 }
     27 
     28 @Test func fileMaintenanceScanRetainsExclusiveOwnerAndRejectsCrossScanEntry() throws {
     29     let f = try FileMaintenanceFixture()
     30     defer { f.remove() }
     31     try f.write("blob")
     32     var reservation = try f.coordinator.reserveMaintenance()
     33     var scan: RadrootsFileMaintenanceScan? = try #require(reservation).openDirectory()
     34     reservation = nil
     35     #expect(try f.coordinator.reserveUse() == nil)
     36     let entry = try #require(scan).next().entries.first
     37     let captured = try #require(entry)
     38     scan = nil
     39     let next = try #require(try f.coordinator.reserveMaintenance())
     40     let newScan = try next.openDirectory()
     41     #expect(throws: RadrootsAppleFileError.invalidRequest) { _ = try newScan.remove(captured) }
     42     #expect(FileManager.default.fileExists(atPath: f.path("blob").path))
     43 }
     44 
     45 @Test func fileMaintenanceBoundedTraversalAndInterruptedCollectionResume() throws {
     46     let f = try FileMaintenanceFixture()
     47     defer { f.remove() }
     48     for index in 0 ..< 150 {
     49         try f.write("blob-\(index)")
     50     }
     51     do {
     52         let reservation = try #require(try f.coordinator.reserveMaintenance())
     53         let scan = try reservation.openDirectory()
     54         #expect(throws: RadrootsAppleFileError.invalidRequest) { _ = try scan.next(limit: 0) }
     55         #expect(throws: RadrootsAppleFileError.invalidRequest) { _ = try scan.next(limit: 65) }
     56         let page = try scan.next(limit: 7)
     57         #expect(page.scannedEntries == 7 && !page.reachedEnd)
     58         #expect(page.entries.count <= 7)
     59         for entry in page.entries {
     60             #expect(try scan.remove(entry))
     61         }
     62     }
     63     let reservation = try #require(try f.coordinator.reserveMaintenance())
     64     let scan = try reservation.openDirectory()
     65     var names: Set<String> = []
     66     var ended = false
     67     for _ in 0 ..< 30 {
     68         let page = try scan.next(limit: 7)
     69         #expect(page.scannedEntries <= 7 && page.entries.count <= 7)
     70         for entry in page.entries {
     71             #expect(names.insert(entry.name).inserted)
     72             #expect(entry.kind == .regularFile && entry.sizeBytes == 3)
     73         }
     74         if page.reachedEnd {
     75             ended = true; break
     76         }
     77     }
     78     #expect(ended && names.count >= 143 && names.count < 150)
     79     #expect(try scan.next().reachedEnd)
     80     #expect(!names.contains(RadrootsFileMaintenanceGate.name))
     81 }
     82 
     83 @Test func fileMaintenanceRetainsReplacedChangedHardlinkedAndNonregularFiles() throws {
     84     let f = try FileMaintenanceFixture()
     85     defer { f.remove() }
     86     for name in ["changed", "replaced", "hardlink", "removed", "eligible"] {
     87         try f.write(name)
     88     }
     89     try FileManager.default.linkItem(at: f.path("hardlink"), to: f.path("alias"))
     90     try FileManager.default.createDirectory(at: f.path("directory"), withIntermediateDirectories: false)
     91     try FileManager.default.createSymbolicLink(at: f.path("symlink"), withDestinationURL: f.path("eligible"))
     92     let reservation = try #require(try f.coordinator.reserveMaintenance())
     93     let scan = try reservation.openDirectory()
     94     let entries = try Dictionary(uniqueKeysWithValues: scan.next().entries.map { ($0.name, $0) })
     95     try Data("different bytes".utf8).write(to: f.path("changed"))
     96     try Data("new".utf8).write(to: f.path("replaced"), options: .atomic)
     97     try FileManager.default.removeItem(at: f.path("removed"))
     98     for name in ["changed", "replaced", "hardlink", "alias", "directory", "symlink", "removed"] {
     99         #expect(try !scan.remove(#require(entries[name])))
    100     }
    101     #expect(try scan.remove(#require(entries["eligible"])))
    102     #expect(try !scan.remove(#require(entries["eligible"])))
    103     #expect(try Data(contentsOf: f.path("replaced")) == Data("new".utf8))
    104     #expect(try Data(contentsOf: f.path("changed")) == Data("different bytes".utf8))
    105 }
    106 
    107 @Test func fileMaintenanceRejectsChangedParentsCoordinationAndUnsafePaths() throws {
    108     let f = try FileMaintenanceFixture()
    109     defer { f.remove() }
    110     try f.write("blob")
    111     let reservation = try #require(try f.coordinator.reserveMaintenance())
    112     for path in ["..", "../root", "/root", "root//child", "root/", "\0"] {
    113         #expect(throws: RadrootsAppleFileError.invalidRequest) { _ = try reservation.openDirectory(relativePath: path) }
    114     }
    115     try FileManager.default.createSymbolicLink(at: f.path("link"), withDestinationURL: f.base)
    116     #expect(throws: RadrootsAppleFileError.permanentFailure) { _ = try reservation.openDirectory(relativePath: "link") }
    117     let scan = try reservation.openDirectory()
    118     let blob = try #require(try scan.next().entries.first { $0.name == "blob" })
    119     try Data().write(to: f.path(RadrootsFileMaintenanceGate.name), options: .atomic)
    120     #expect(throws: RadrootsAppleFileError.permanentFailure) { try reservation.validate() }
    121     #expect(throws: RadrootsAppleFileError.permanentFailure) { _ = try scan.remove(blob) }
    122     #expect(FileManager.default.fileExists(atPath: f.path("blob").path))
    123     try FileManager.default.moveItem(at: f.root, to: f.base.appendingPathComponent("moved"))
    124     try FileManager.default.createDirectory(at: f.root, withIntermediateDirectories: false)
    125     #expect(throws: RadrootsAppleFileError.permanentFailure) { _ = try scan.next() }
    126 }
    127 
    128 @Test func fileMaintenanceSupportsBoundedSubdirectoryInspection() throws {
    129     let f = try FileMaintenanceFixture()
    130     defer { f.remove() }
    131     try FileManager.default.createDirectory(at: f.path("staging"), withIntermediateDirectories: false)
    132     try f.write("staging/blob")
    133     let reservation = try #require(try f.coordinator.reserveMaintenance())
    134     let scan = try reservation.openDirectory(relativePath: "staging")
    135     let blob = try #require(try scan.next().entries.first)
    136     #expect(blob.name == "blob")
    137     #expect(try scan.remove(blob))
    138     #expect(FileManager.default.fileExists(atPath: f.path(RadrootsFileMaintenanceGate.name).path))
    139 }
    140 
    141 @Test func fileMaintenanceCancellationDoesNotReleaseUndrainedUse() async throws {
    142     let f = try FileMaintenanceFixture()
    143     defer { f.remove() }
    144     let latch = FileMaintenanceLatch()
    145     let task = try holdMaintenanceUse(#require(try f.coordinator.reserveUse()), latch: latch)
    146     task.cancel()
    147     #expect(try f.coordinator.reserveMaintenance() == nil)
    148     await latch.open()
    149     try await task.value
    150     #expect(try f.coordinator.reserveMaintenance() != nil)
    151 }
    152 
    153 private func holdMaintenanceUse(_ lease: RadrootsFileUseReservation, latch: FileMaintenanceLatch) throws -> Task<Void, Error> {
    154     try lease.validate()
    155     return Task {
    156         defer { withExtendedLifetime(lease) {} }
    157         await latch.wait()
    158         try lease.validate()
    159     }
    160 }
    161 
    162 @Test func fileMaintenanceConcurrentReadersAndCollectorsNeverOverlap() async throws {
    163     let f = try FileMaintenanceFixture()
    164     defer { f.remove() }
    165     let probe = FileMaintenanceProbe()
    166     try await withThrowingTaskGroup(of: Void.self) { group in
    167         for owner in 0 ..< 8 {
    168             group.addTask {
    169                 for _ in 0 ..< 30 {
    170                     if owner < 6 {
    171                         if let lease = try f.coordinator.reserveUse() {
    172                             await probe.enter(exclusive: false)
    173                             await Task.yield()
    174                             await probe.leave(exclusive: false)
    175                             withExtendedLifetime(lease) {}
    176                         }
    177                     } else if let maintenance = try f.coordinator.reserveMaintenance() {
    178                         await probe.enter(exclusive: true)
    179                         await Task.yield()
    180                         await probe.leave(exclusive: true)
    181                         withExtendedLifetime(maintenance) {}
    182                     }
    183                     await Task.yield()
    184                 }
    185             }
    186         }
    187         try await group.waitForAll()
    188     }
    189     #expect(await probe.violations == 0)
    190     #expect(await probe.completed > 0)
    191     #expect(try f.coordinator.reserveMaintenance() != nil)
    192 }
    193 
    194 @Test func fileMaintenanceRejectsInvalidCoordinationAndReplacedParent() throws {
    195     for variant in ["nonempty", "symlink", "hardlink", "parent"] {
    196         let f = try FileMaintenanceFixture()
    197         defer { f.remove() }
    198         try f.write("blob")
    199         let gate = f.path(RadrootsFileMaintenanceGate.name)
    200         switch variant {
    201         case "nonempty": try Data("unknown".utf8).write(to: gate)
    202         case "symlink": try FileManager.default.createSymbolicLink(at: gate, withDestinationURL: f.path("blob"))
    203         case "hardlink":
    204             try Data().write(to: gate)
    205             try FileManager.default.linkItem(at: gate, to: f.path("alias"))
    206         default:
    207             let reservation = try #require(try f.coordinator.reserveMaintenance())
    208             let scan = try reservation.openDirectory()
    209             let entry = try #require(try scan.next().entries.first)
    210             try FileManager.default.moveItem(at: f.root, to: f.base.appendingPathComponent("moved"))
    211             try FileManager.default.createDirectory(at: f.root, withIntermediateDirectories: false)
    212             #expect(throws: RadrootsAppleFileError.permanentFailure) { _ = try scan.remove(entry) }
    213             #expect(FileManager.default.fileExists(atPath: f.base.appendingPathComponent("moved/blob").path))
    214             continue
    215         }
    216         #expect(throws: RadrootsAppleFileError.permanentFailure) { _ = try f.coordinator.reserveUse() }
    217         #expect(throws: RadrootsAppleFileError.permanentFailure) { _ = try f.coordinator.reserveMaintenance() }
    218     }
    219 }
    220 
    221 private actor FileMaintenanceProbe {
    222     private var readers = 0
    223     private var writers = 0
    224     private(set) var violations = 0
    225     private(set) var completed = 0
    226     func enter(exclusive: Bool) {
    227         if writers != 0 || exclusive && readers != 0 {
    228             violations += 1
    229         }
    230         if exclusive {
    231             writers += 1
    232         } else {
    233             readers += 1
    234         }
    235     }
    236 
    237     func leave(exclusive: Bool) {
    238         if exclusive {
    239             writers -= 1
    240         } else {
    241             readers -= 1
    242         }
    243         completed += 1
    244     }
    245 }
    246 
    247 @Test func fileMaintenanceFailedScanCannotResumeAfterParentRestoration() throws {
    248     let f = try FileMaintenanceFixture()
    249     defer { f.remove() }
    250     try f.write("blob")
    251     let reservation = try #require(try f.coordinator.reserveMaintenance())
    252     let scan = try reservation.openDirectory()
    253     _ = try scan.next(limit: 1)
    254     let moved = f.base.appendingPathComponent("moved")
    255     try FileManager.default.moveItem(at: f.root, to: moved)
    256     try FileManager.default.createDirectory(at: f.root, withIntermediateDirectories: false)
    257     #expect(throws: RadrootsAppleFileError.permanentFailure) { _ = try scan.next() }
    258     try FileManager.default.removeItem(at: f.root)
    259     try FileManager.default.moveItem(at: moved, to: f.root)
    260     try reservation.validate()
    261     #expect(throws: RadrootsAppleFileError.permanentFailure) { _ = try scan.next() }
    262     let fresh = try reservation.openDirectory()
    263     #expect(try fresh.next().entries.map(\.name) == ["blob"])
    264 }
    265 
    266 private actor FileMaintenanceLatch {
    267     private var continuation: CheckedContinuation<Void, Never>?
    268     private var opened = false
    269     func wait() async {
    270         if opened {
    271             return
    272         }
    273         await withCheckedContinuation { continuation = $0 }
    274     }
    275 
    276     func open() {
    277         opened = true
    278         continuation?.resume()
    279         continuation = nil
    280     }
    281 }
    282 
    283 private struct FileMaintenanceFixture: Sendable {
    284     let base: URL
    285     let root: URL
    286     var coordinator: RadrootsAppleFileMaintenance {
    287         RadrootsAppleFileMaintenance(root: root)
    288     }
    289 
    290     init() throws {
    291         let raw = FileManager.default.temporaryDirectory
    292             .appendingPathComponent("file-maintenance-\(UUID().uuidString)", isDirectory: true)
    293         try FileManager.default.createDirectory(at: raw, withIntermediateDirectories: true)
    294         let pointer = try #require(raw.path.withCString { Darwin.realpath($0, nil) })
    295         defer { Darwin.free(pointer) }
    296         base = URL(fileURLWithPath: String(cString: pointer), isDirectory: true)
    297         root = base.appendingPathComponent("root", isDirectory: true)
    298         try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
    299     }
    300 
    301     func path(_ name: String) -> URL {
    302         root.appendingPathComponent(name)
    303     }
    304 
    305     func write(_ name: String) throws {
    306         try Data("old".utf8).write(to: path(name))
    307     }
    308 
    309     func remove() {
    310         try? FileManager.default.removeItem(at: base)
    311     }
    312 }