RadrootsFileCapacityTests.swift (8448B)
1 import Darwin 2 import Foundation 3 @testable import RadrootsKit 4 import Testing 5 6 @Test func fileCapacityClassificationIsBoundedTypedAndRedacted() throws { 7 for code in [ENOSPC, EDQUOT] { 8 let raw = NSError(domain: NSPOSIXErrorDomain, code: Int(code), userInfo: [NSFilePathErrorKey: "/private/value"]) 9 #expect(RadrootsAppleFileError.posix(code) == .spaceInsufficient) 10 #expect(RadrootsAppleFileError.classified(raw) == .spaceInsufficient) 11 let wrapped = NSError(domain: NSCocoaErrorDomain, code: CocoaError.Code.fileWriteUnknown.rawValue, 12 userInfo: [NSUnderlyingErrorKey: raw]) 13 #expect(RadrootsAppleFileError.classified(wrapped) == .spaceInsufficient) 14 #expect(RadrootsBackgroundTransferError.persistence(wrapped) == .spaceInsufficient) 15 } 16 #expect(RadrootsAppleFileError.classified(CocoaError(.fileWriteOutOfSpace)) == .spaceInsufficient) 17 #expect(RadrootsAppleFileError.classified(RadrootsAppleFileError.notFound) == .notFound) 18 #expect(RadrootsAppleFileError.posix(EIO) == .permanentFailure) 19 #expect(RadrootsAppleFileError.classified(NSError(domain: "unrelated", code: Int(ENOSPC))) == .permanentFailure) 20 var nested = NSError(domain: NSPOSIXErrorDomain, code: Int(ENOSPC)) 21 for _ in 0 ..< 8 { nested = NSError(domain: "wrapper", code: 0, userInfo: [NSUnderlyingErrorKey: nested]) } 22 #expect(RadrootsAppleFileError.classified(nested) == .permanentFailure) 23 #expect(RadrootsAppleFileError.spaceInsufficient.errorDescription == "There is not enough storage space to complete the file operation.") 24 #expect(String(describing: RadrootsAppleFileError.spaceInsufficient) == "spaceInsufficient") 25 #expect(RadrootsAppleMediaPicker.adapt(fileError: .spaceInsufficient) == .spaceInsufficient) 26 #expect(RadrootsAppleMediaPicker.adapt(error: RadrootsAppleFileError.spaceInsufficient) == .spaceInsufficient) 27 #expect(RadrootsCaptureIntakeError.spaceInsufficient.errorDescription == "There is not enough storage space to save the capture.") 28 #expect(RadrootsBackgroundTransferError.persistence(.receiptCapacityExceeded as RadrootsBackgroundTransferError) == .receiptCapacityExceeded) 29 #expect(RadrootsBackgroundTransferError.persistence(RadrootsBackgroundTransferError.invalidRequest) == .persistenceFailure) 30 } 31 32 @Test func fileCapacityReachesActualFoundationAndLockCallers() throws { 33 let fixture = try CapacityFixture() 34 defer { fixture.remove() } 35 let access = RadrootsAppleFileAccess(roots: fixture.roots) 36 #expect(throws: RadrootsAppleFileError.spaceInsufficient) { 37 try access.classifiedFileSystemOperation { 38 throw NSError(domain: NSPOSIXErrorDomain, code: Int(ENOSPC)) 39 } 40 } 41 for code in [ENOSPC, EDQUOT] { 42 #expect(throws: RadrootsAppleFileError.spaceInsufficient) { 43 _ = try RadrootsAtomicFile.acquireExclusiveLock(at: fixture.base.appendingPathComponent("lock"), 44 injectedOpenError: { _ in code }) 45 } 46 } 47 #expect(!FileManager.default.fileExists(atPath: fixture.base.appendingPathComponent("lock").path)) 48 let directory = fixture.base.appendingPathComponent("directory") 49 try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false) 50 errno = ENOSPC 51 #expect(throws: RadrootsAppleFileError.permanentFailure) { 52 try RadrootsAtomicFile.synchronizeExisting(at: directory) 53 } 54 } 55 56 @Test func fileCapacityPreservesOldOrAmbiguousBytesAtEveryInstallBoundary() throws { 57 for phase in RadrootsAtomicFile.Phase.allCases { 58 let fixture = try CapacityFixture() 59 defer { fixture.remove() } 60 let url = fixture.base.appendingPathComponent("value") 61 let old = Data("acknowledged".utf8), pending = Data(repeating: 42, count: 100_000) 62 try RadrootsAtomicFile.install(old, at: url) 63 #expect(throws: RadrootsAppleFileError.spaceInsufficient) { 64 try RadrootsAtomicFile.installForTesting(pending, at: url) { current in 65 if current == phase { throw RadrootsAppleFileError.posix(ENOSPC) } 66 } 67 } 68 #expect(try Data(contentsOf: url) == (phase == .beforeDirectorySync ? pending : old)) 69 try RadrootsAtomicFile.install(pending, at: url) 70 #expect(try Data(contentsOf: url) == pending) 71 } 72 } 73 74 @Test func fileCapacityPreservesOriginalFailureAndSourceWhenLeaseIsAbsent() throws { 75 let fixture = try CapacityFixture() 76 defer { fixture.remove() } 77 let live = RadrootsAppleFileAccess(roots: fixture.roots) 78 let bytes = Data("pending upload".utf8), digest = RadrootsAppleFileDigest.sha256(bytes) 79 let blob = try live.stageBlob(bytes) 80 let access = RadrootsAppleFileAccess(roots: fixture.roots, persistence: capacityPersistence(.beforeInstall)) 81 #expect(throws: RadrootsAppleFileError.spaceInsufficient) { 82 _ = try access.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "pending") 83 } 84 #expect(throws: RadrootsAppleFileError.spaceInsufficient) { 85 _ = try access.leaseUploadBytes(bytes, identifier: "upload") 86 } 87 #expect(try live.readStagedBlob(blob) == bytes) 88 let lease = try live.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "pending") 89 #expect(try Data(contentsOf: lease.fileURL) == bytes) 90 let prior = try live.leaseUploadBytes(Data("other".utf8), identifier: "conflict") 91 #expect(throws: RadrootsAppleFileError.permanentFailure) { 92 _ = try access.leaseUploadBytes(bytes, identifier: "conflict") 93 } 94 #expect(try Data(contentsOf: prior.fileURL) == Data("other".utf8)) 95 } 96 97 @Test func fileCapacityDoesNotSwallowMatchingStagedBlobOrLeaseSyncFailure() throws { 98 let fixture = try CapacityFixture() 99 defer { fixture.remove() } 100 let bytes = Data("same exact bytes".utf8), digest = RadrootsAppleFileDigest.sha256(bytes) 101 let blob = try RadrootsStagedBlobReference(blobID: "opaque", sizeBytes: bytes.count) 102 let faulted = RadrootsAppleFileAccess(roots: fixture.roots, 103 persistence: capacityPersistence(.beforeDirectorySync, failSync: true)) 104 #expect(throws: RadrootsAppleFileError.spaceInsufficient) { try faulted.installStagedBlob(bytes, reference: blob) } 105 let live = RadrootsAppleFileAccess(roots: fixture.roots) 106 #expect(try live.readStagedBlob(blob) == bytes) 107 #expect(throws: RadrootsAppleFileError.spaceInsufficient) { try faulted.installStagedBlob(bytes, reference: blob) } 108 #expect(throws: RadrootsAppleFileError.spaceInsufficient) { 109 _ = try faulted.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "ambiguous") 110 } 111 #expect(throws: RadrootsAppleFileError.spaceInsufficient) { 112 _ = try faulted.leaseUploadBytes(bytes, identifier: "ambiguous_upload") 113 } 114 let lease = try live.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "ambiguous") 115 #expect(try Data(contentsOf: lease.fileURL) == bytes) 116 #expect(throws: RadrootsAppleFileError.spaceInsufficient) { 117 _ = try faulted.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "ambiguous") 118 } 119 try live.installStagedBlob(bytes, reference: blob) 120 let recovered = RadrootsAppleFileAccess(roots: fixture.roots, persistence: capacityPersistence(.beforeDirectorySync)) 121 let exact = try recovered.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "reflushed") 122 #expect(try Data(contentsOf: exact.fileURL) == bytes) 123 let attributes = try FileManager.default.attributesOfItem(atPath: exact.fileURL.path) 124 #expect((attributes[.posixPermissions] as? NSNumber)?.intValue == 0o400) 125 } 126 127 func capacityPersistence(_ phase: RadrootsAtomicFile.Phase, failSync: Bool = false) -> RadrootsFilePersistence { 128 RadrootsFilePersistence(install: { bytes, url, mode, readOnly in 129 try RadrootsAtomicFile.installForTesting(bytes, at: url, mode: mode, readOnly: readOnly) { current in 130 if current == phase { throw RadrootsAppleFileError.posix(ENOSPC) } 131 } 132 }, synchronize: { url in 133 if failSync { throw RadrootsAppleFileError.posix(EDQUOT) } 134 try RadrootsAtomicFile.synchronizeExisting(at: url) 135 }) 136 } 137 138 struct CapacityFixture { 139 let roots: RadrootsAppleFileRoots 140 var base: URL { roots.dataRoot.deletingLastPathComponent() } 141 init() throws { roots = try appleTransferRoots() } 142 func remove() { try? FileManager.default.removeItem(at: base) } 143 }