From f71b5f5706d8a17e9701b10d2fa2a56ede7d5e32 Mon Sep 17 00:00:00 2001 From: Alessandro Date: Fri, 22 Nov 2024 10:13:43 +0100 Subject: [PATCH] Xcode 16 and Swift 6 (#50) Xcode 16 and Swift 6 --- .github/workflows/ci.yml | 38 +-- CHANGELOG.md | 5 + CoreDataPlus.xcodeproj/project.pbxproj | 24 +- .../xcschemes/Cleanup Whitespace.xcscheme | 2 +- .../xcschemes/CoreDataPlus.xcscheme | 2 +- .../xcshareddata/xcschemes/SwiftLint.xcscheme | 2 +- Package.swift | 8 +- Sources/FetchedResultsChange.swift | 34 +-- Sources/Migration/LegacyMigration.swift | 14 +- Sources/Migration/LegacyMigrationStep.swift | 2 +- Sources/Migration/Migrator.swift | 24 +- Sources/Migration/ModelVersion.swift | 34 +-- Sources/Migration/StagedMigration.swift | 45 +++- Sources/Migration/StagedMigrationStep.swift | 7 +- Sources/NSFetchRequestResult+CoreData.swift | 202 ++++++++-------- Sources/NSManagedObjectContext+History.swift | 2 +- Sources/NSPredicate+Utils.swift | 4 +- Tests/FetchedResultsChanges_Tests.swift | 79 ++++--- Tests/Migrations_Tests.swift | 60 ++--- Tests/NSEntityDescriptionUtils_Tests.swift | 12 +- Tests/NSFetchRequestResultUtils_Tests.swift | 80 +++---- .../NSManagedObjectContextHistory_Tests.swift | 216 +++++++++++++++--- ...agedObjectContextInvestigation_Tests.swift | 14 +- Tests/NSManagedObjectContextUtils_Tests.swift | 3 +- Tests/NSManagedObjectUtils_Tests.swift | 4 +- Tests/NSPredicateUtils_Tests.swift | 4 +- .../NotificationMerge_Tests.swift | 18 +- .../NotificationPayload_Tests.swift | 60 ++--- Tests/ProgrammaticMigration_Tests.swift | 4 +- .../ProgrammaticallyDefinedModel_Tests.swift | 5 +- .../Resources/SampleModel/Entities/Car.swift | 4 +- .../MappingModels/V2to3MakerPolicy.swift | 11 +- .../NSManagedObjectContext+SampleModel2.swift | 10 +- .../SampleModel2/SampleModel2+V1.swift | 2 +- .../SampleModel2/SampleModel2+V2.swift | 2 +- .../Resources/SampleModel3/SampleModel3.swift | 2 +- .../SampleModel3/SampleModelVersion3.swift | 32 +-- Tests/StagedMigrations_Tests.swift | 127 +++++----- Tests/TestPlans/CoreDataPlus iOS.xctestplan | 150 ------------ Tests/TestPlans/CoreDataPlus macOS.xctestplan | 154 ------------- Tests/TestPlans/CoreDataPlus tvOS.xctestplan | 123 ---------- .../TestPlans/CoreDataPlus watchOS.xctestplan | 150 ------------ Tests/TestPlans/CoreDataPlus.xctestplan | 7 +- Tests/Utils/InMemoryTestCase.swift | 2 +- Tests/Utils/OnDiskTestCase.swift | 2 +- ...iskWithProgrammaticallyModelTestCase.swift | 10 +- Tests/Utils/Utils.swift | 4 +- 47 files changed, 716 insertions(+), 1084 deletions(-) delete mode 100644 Tests/TestPlans/CoreDataPlus iOS.xctestplan delete mode 100644 Tests/TestPlans/CoreDataPlus macOS.xctestplan delete mode 100644 Tests/TestPlans/CoreDataPlus tvOS.xctestplan delete mode 100644 Tests/TestPlans/CoreDataPlus watchOS.xctestplan diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a82baaa3..4ff63ad1 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,9 +19,9 @@ concurrency: jobs: info: name: Show macOS and Xcode versions - runs-on: macos-14 + runs-on: macos-15 env: - DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.app/Contents/Developer steps: - name: Versions run: | @@ -32,9 +32,9 @@ jobs: xcrun simctl list macOS: name: Test macOS - runs-on: macos-14 + runs-on: macos-15 env: - DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.app/Contents/Developer steps: - uses: actions/checkout@v4 - name: macOS @@ -48,12 +48,12 @@ jobs: path: ~/Downloads/Report iOS: name: Test iOS - runs-on: macos-14 + runs-on: macos-15 env: - DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.app/Contents/Developer strategy: matrix: - destination: ["OS=17.0,name=iPhone 15 Pro"] + destination: ["OS=18.0,name=iPhone 16 Pro"] steps: - uses: actions/checkout@v4 - name: iOS - ${{ matrix.destination }} @@ -67,12 +67,12 @@ jobs: path: ~/Downloads/Report visionOS: name: Test visionOS - runs-on: macos-14 + runs-on: macos-15 env: - DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.app/Contents/Developer strategy: matrix: - destination: ["OS=1.0,name=Apple Vision Pro"] + destination: ["OS=2.0,name=Apple Vision Pro"] steps: - uses: actions/checkout@v4 - name: iOS - ${{ matrix.destination }} @@ -86,12 +86,12 @@ jobs: path: ~/Downloads/Report tvOS: name: Test tvOS - runs-on: macos-14 + runs-on: macos-15 env: - DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.app/Contents/Developer strategy: matrix: - destination: ["OS=17.0,name=Apple TV"] + destination: ["OS=18.0,name=Apple TV"] steps: - uses: actions/checkout@v4 - name: tvOS - ${{ matrix.destination }} @@ -105,12 +105,12 @@ jobs: path: ~/Downloads/Report watchOS: name: Test watchOS - runs-on: macos-14 + runs-on: macos-15 env: - DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.app/Contents/Developer strategy: matrix: - destination: ["OS=10.0,name=Apple Watch Series 9 (45mm)"] + destination: ["OS=11.0,name=Apple Watch Series 10 (46mm)"] steps: - uses: actions/checkout@v4 - name: watchOS - ${{ matrix.destination }} @@ -124,9 +124,9 @@ jobs: path: ~/Downloads/Report SPM: name: Test SPM Integration - runs-on: macos-14 + runs-on: macos-15 env: - DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.app/Contents/Developer steps: - uses: actions/checkout@v4 - name: SPM Test @@ -135,7 +135,7 @@ jobs: swift test # lint: # name: Swift Lint -# runs-on: macos-14 +# runs-on: macos-15 # steps: # - uses: actions/checkout@v4 # - name: Run SwiftLint diff --git a/CHANGELOG.md b/CHANGELOG.md index 626ad7d6..da256ec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### 6.2.0 + +- Xcode 16 +- Swift 6 (Strict concurrency checking) + ### 6.1.0 - Added utility methods for `FetchedResultsObjectChange` and `FetchedResultsSectionChange`. diff --git a/CoreDataPlus.xcodeproj/project.pbxproj b/CoreDataPlus.xcodeproj/project.pbxproj index c35d3033..eb1b3e4a 100644 --- a/CoreDataPlus.xcodeproj/project.pbxproj +++ b/CoreDataPlus.xcodeproj/project.pbxproj @@ -157,14 +157,10 @@ 063E100D264BC2F90050E84C /* Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deprecated.swift; sourceTree = ""; }; 065094052643E8BC00B3EE12 /* MigrationProgressReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationProgressReporter.swift; sourceTree = ""; }; 0693F915237C58A7008E4F31 /* workflows */ = {isa = PBXFileReference; lastKnownFileType = text; name = workflows; path = .github/workflows; sourceTree = SOURCE_ROOT; }; - 06968AD926454BA300088D76 /* CoreDataPlus watchOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CoreDataPlus watchOS.xctestplan"; sourceTree = ""; }; 06968B0C26454FBE00088D76 /* BookCoverToCoverMigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookCoverToCoverMigrationPolicy.swift; sourceTree = ""; }; 06968B112645685F00088D76 /* FeedbackMigrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackMigrationManager.swift; sourceTree = ""; }; 06A1D11E2BF34C6300F62CA1 /* LegacyMigrationStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyMigrationStep.swift; sourceTree = ""; }; - 06A3C1D3239FD36100E08D45 /* CoreDataPlus macOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CoreDataPlus macOS.xctestplan"; sourceTree = ""; }; - 06A3C1D4239FD3E900E08D45 /* CoreDataPlus tvOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CoreDataPlus tvOS.xctestplan"; sourceTree = ""; }; 06A3C1D5239FD83D00E08D45 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = SOURCE_ROOT; }; - 06A4584E239FD22C007BA7C9 /* CoreDataPlus iOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CoreDataPlus iOS.xctestplan"; sourceTree = ""; }; 06A6CD9F261DE0E7000563F0 /* Transformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transformer.swift; sourceTree = ""; }; 06A6CDC6261DFC8F000563F0 /* Transformer_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transformer_Tests.swift; sourceTree = ""; }; 06A6CE25261E0961000563F0 /* SampleModel_V1.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = SampleModel_V1.sqlite; sourceTree = ""; }; @@ -301,10 +297,6 @@ isa = PBXGroup; children = ( 06AF61B126F095070090A61B /* CoreDataPlus.xctestplan */, - 06968AD926454BA300088D76 /* CoreDataPlus watchOS.xctestplan */, - 06A4584E239FD22C007BA7C9 /* CoreDataPlus iOS.xctestplan */, - 06A3C1D3239FD36100E08D45 /* CoreDataPlus macOS.xctestplan */, - 06A3C1D4239FD3E900E08D45 /* CoreDataPlus tvOS.xctestplan */, ); path = TestPlans; sourceTree = ""; @@ -612,7 +604,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1300; - LastUpgradeCheck = 1540; + LastUpgradeCheck = 1600; ORGANIZATIONNAME = "Alessandro Marzoli"; TargetAttributes = { 06AF614726F091370090A61B = { @@ -869,7 +861,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.0.0; + MARKETING_VERSION = 6.2.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; @@ -881,7 +873,6 @@ SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3,4,5,6,7"; TVOS_DEPLOYMENT_TARGET = 16.0; WATCHOS_DEPLOYMENT_TARGET = 9.0; @@ -912,7 +903,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.0.0; + MARKETING_VERSION = 6.2.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_FAST_MATH = YES; @@ -923,7 +914,6 @@ SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3,4,5,6,7"; TVOS_DEPLOYMENT_TARGET = 16.0; WATCHOS_DEPLOYMENT_TARGET = 9.0; @@ -934,7 +924,6 @@ isa = XCBuildConfiguration; buildSettings = { ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_STYLE = Automatic; @@ -956,7 +945,6 @@ SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,4,6,7"; TVOS_DEPLOYMENT_TARGET = 16.0; WATCHOS_DEPLOYMENT_TARGET = 9.0; @@ -967,7 +955,6 @@ isa = XCBuildConfiguration; buildSettings = { ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_STYLE = Automatic; @@ -988,7 +975,6 @@ SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,4,6,7"; TVOS_DEPLOYMENT_TARGET = 16.0; WATCHOS_DEPLOYMENT_TARGET = 9.0; @@ -1108,7 +1094,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TVOS_DEPLOYMENT_TARGET = 16.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -1173,7 +1159,7 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TVOS_DEPLOYMENT_TARGET = 16.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; diff --git a/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/Cleanup Whitespace.xcscheme b/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/Cleanup Whitespace.xcscheme index b8fcc6db..b3058f83 100644 --- a/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/Cleanup Whitespace.xcscheme +++ b/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/Cleanup Whitespace.xcscheme @@ -1,6 +1,6 @@ (from: sectionInfo) switch type { @@ -222,7 +226,7 @@ extension FetchedResultsSectionChange { case .delete(let info, _): info } } - + /// Returns`true` if the change is an insertion. public var isInsertion: Bool { switch self { @@ -232,7 +236,7 @@ extension FetchedResultsSectionChange { return false } } - + /// Returns `true` if the change is a deletion. public var isDeletion: Bool { switch self { @@ -242,7 +246,7 @@ extension FetchedResultsSectionChange { return false } } - + /// The index of the change. public var index: Int { switch self { diff --git a/Sources/Migration/LegacyMigration.swift b/Sources/Migration/LegacyMigration.swift index be5bf282..2aef8936 100644 --- a/Sources/Migration/LegacyMigration.swift +++ b/Sources/Migration/LegacyMigration.swift @@ -2,7 +2,7 @@ import CoreData -/// Handles migrations with the old `NSMigrationManager`. +/// Handles migrations via `NSMigrationManager`. public protocol LegacyMigration { /// Returns a list of mapping models needed to migrate the current version of the database to the next one. func mappingModelsToNextModelVersion() -> [NSMappingModel]? @@ -67,8 +67,9 @@ extension ModelVersion where Self: LegacyMigration { return nil } - return try? NSMappingModel.inferredMappingModel(forSourceModel: managedObjectModel(), - destinationModel: nextVersion.managedObjectModel()) + return try? NSMappingModel.inferredMappingModel( + forSourceModel: managedObjectModel(), + destinationModel: nextVersion.managedObjectModel()) } /// - Returns: a list of `NSMappingModel` from a list of mapping model names. @@ -81,8 +82,9 @@ extension ModelVersion where Self: LegacyMigration { } guard - let allMappingModelsURLs = modelBundle.urls(forResourcesWithExtension: ModelVersionFileExtension.cdm, - subdirectory: nil), + let allMappingModelsURLs = modelBundle.urls( + forResourcesWithExtension: ModelVersionFileExtension.cdm, + subdirectory: nil), allMappingModelsURLs.count > 0 else { return results @@ -91,7 +93,7 @@ extension ModelVersion where Self: LegacyMigration { for name in mappingModelNames { let expectedFileName = "\(name).\(ModelVersionFileExtension.cdm)" if let url = allMappingModelsURLs.first(where: { $0.lastPathComponent == expectedFileName }), - let mappingModel = NSMappingModel(contentsOf: url) + let mappingModel = NSMappingModel(contentsOf: url) { results.append(mappingModel) } diff --git a/Sources/Migration/LegacyMigrationStep.swift b/Sources/Migration/LegacyMigrationStep.swift index 4b39b3e1..79f95a96 100644 --- a/Sources/Migration/LegacyMigrationStep.swift +++ b/Sources/Migration/LegacyMigrationStep.swift @@ -2,7 +2,7 @@ import CoreData -// Representation of a Core Data legacy migration step. +/// Representation of a Core Data legacy migration step. public final class LegacyMigrationStep { public let sourceVersion: Version public let sourceModel: NSManagedObjectModel diff --git a/Sources/Migration/Migrator.swift b/Sources/Migration/Migrator.swift index 687e95ba..a70fd7de 100644 --- a/Sources/Migration/Migrator.swift +++ b/Sources/Migration/Migrator.swift @@ -151,9 +151,10 @@ extension Migrator { // A dead lock can occur if a NSPersistentStore with a different journaling mode // is currently active and using the database file. // You need to remove it before performing a WAL checkpoint. - try performWALCheckpointForStore(at: sourceURL, - storeOptions: sourceOptions, - model: sourceVersion.managedObjectModel()) + try performWALCheckpointForStore( + at: sourceURL, + storeOptions: sourceOptions, + model: sourceVersion.managedObjectModel()) } let steps = sourceVersion.migrationSteps(to: targetVersion) @@ -263,9 +264,11 @@ extension Migrator { // MARK: - WAL Checkpoint /// Forces Core Data to perform a checkpoint operation, which merges the data in the `-wal` file to the store file. -private func performWALCheckpointForStore(at storeURL: URL, - storeOptions: PersistentStoreOptions? = nil, - model: NSManagedObjectModel) throws { +private func performWALCheckpointForStore( + at storeURL: URL, + storeOptions: PersistentStoreOptions? = nil, + model: NSManagedObjectModel +) throws { // "If the -wal file is not present, using this approach to add the store won't cause any exceptions, // but the transactions recorded in the missing -wal file will be lost." (from: https://developer.apple.com/library/archive/qa/qa1809/_index.html) // credits: @@ -285,10 +288,11 @@ private func performWALCheckpointForStore(at storeURL: URL, options[NSPersistentHistoryTrackingKey] = [NSPersistentHistoryTrackingKey: true as NSNumber] } - let store = try persistentStoreCoordinator.addPersistentStore(type: .sqlite, - configuration: nil, - at: storeURL, - options: options) + let store = try persistentStoreCoordinator.addPersistentStore( + type: .sqlite, + configuration: nil, + at: storeURL, + options: options) try persistentStoreCoordinator.remove(store) } diff --git a/Sources/Migration/ModelVersion.swift b/Sources/Migration/ModelVersion.swift index f08d58f1..aa224439 100644 --- a/Sources/Migration/ModelVersion.swift +++ b/Sources/Migration/ModelVersion.swift @@ -59,10 +59,6 @@ public protocol ModelVersion: Equatable, RawRepresentable, CustomDebugStringConv func managedObjectModel() -> NSManagedObjectModel } - - - - extension ModelVersion { /// Protocol `ModelVersion`. /// @@ -89,9 +85,10 @@ extension ModelVersion { /// - Throws: It throws an error if no store is found at `persistentStoreURL` or if there is a problem accessing its contents. public init?(persistentStoreURL: URL) throws { let metadata: [String: Any] - metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(type: .sqlite, - at: persistentStoreURL, - options: nil) + metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore( + type: .sqlite, + at: persistentStoreURL, + options: nil) let version = Self[metadata] guard let modelVersion = version else { @@ -107,12 +104,13 @@ extension ModelVersion { public func managedObjectModel() -> NSManagedObjectModel { _managedObjectModel() } - + // swiftlint:disable:next identifier_name internal func _managedObjectModel() -> NSManagedObjectModel { - let momURL = modelBundle.url(forResource: versionName, - withExtension: "\(ModelVersionFileExtension.mom)", - subdirectory: momd) + let momURL = modelBundle.url( + forResource: versionName, + withExtension: "\(ModelVersionFileExtension.mom)", + subdirectory: momd) // As of iOS 11, Apple is advising that opening the .omo file for a managed object model is not supported, since the file format can change from release to release // let omoURL = modelBundle.url(forResource: versionName, withExtension: "\(ModelVersionExtension.omo)", subdirectory: momd) @@ -165,9 +163,10 @@ extension ModelVersion { return false } - let mappingModel = try? NSMappingModel.inferredMappingModel(forSourceModel: managedObjectModel(), - destinationModel: nextVersion.managedObjectModel()) - + let mappingModel = try? NSMappingModel.inferredMappingModel( + forSourceModel: managedObjectModel(), + destinationModel: nextVersion.managedObjectModel()) + return mappingModel != nil } } @@ -184,9 +183,10 @@ public func isMigrationNecessary(for storeURL: URL, to ve // Before you initiate a migration process, you should first determine whether it is necessary. // If the target model configuration is compatible with the persistent store metadata, there is no need to migrate // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreDataVersioning/Articles/vmCustomizing.html#//apple_ref/doc/uid/TP40004399-CH8-SW2 - let metadata: [String: Any] = try NSPersistentStoreCoordinator.metadataForPersistentStore(type: .sqlite, - at: storeURL, - options: nil) + let metadata: [String: Any] = try NSPersistentStoreCoordinator.metadataForPersistentStore( + type: .sqlite, + at: storeURL, + options: nil) let targetModel = version.managedObjectModel() // https://vimeo.com/164904652 diff --git a/Sources/Migration/StagedMigration.swift b/Sources/Migration/StagedMigration.swift index 54f6e532..dca2d3a2 100644 --- a/Sources/Migration/StagedMigration.swift +++ b/Sources/Migration/StagedMigration.swift @@ -6,30 +6,45 @@ import CoreData /// - Note: `NSStagedMigrationManager` requires `NSMigratePersistentStoresAutomaticallyOption` and `NSInferMappingModelAutomaticallyOption` set to to *true*. public protocol StagedMigration { /// Returns the current `NSManagedObjectModelReference`. - @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + @available( + iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, + macCatalystApplicationExtension 17.0, * + ) func managedObjectModelReference() -> NSManagedObjectModelReference - + /// The Base64-encoded 128-bit model version hash. - @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + @available( + iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, + macCatalystApplicationExtension 17.0, * + ) var versionChecksum: String { get } - + /// - Returns a `NSMigrationStage` needed to migrate to the next `version` of the store. - @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + @available( + iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, + macCatalystApplicationExtension 17.0, * + ) func migrationStageToNextModelVersion() -> NSMigrationStage? } // MARK: - NSMigrationStage extension ModelVersion where Self: StagedMigration { - @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + @available( + iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, + macCatalystApplicationExtension 17.0, * + ) public var versionChecksum: String { managedObjectModel().versionChecksum } - + /// Protocol `StagedMigration`. /// /// Returns the `NSManagedObjectModelReference` for this `ModelVersion`. - @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + @available( + iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, + macCatalystApplicationExtension 17.0, * + ) public func managedObjectModelReference() -> NSManagedObjectModelReference { .init(model: managedObjectModel(), versionChecksum: versionChecksum) } @@ -37,17 +52,23 @@ extension ModelVersion where Self: StagedMigration { /// Protocol `StagedMigration`. /// /// Returns a `NSMigrationStage` needed to migrate to the next `version` of the store. - @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + @available( + iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, + macCatalystApplicationExtension 17.0, * + ) public func migrationStageToNextModelVersion() -> NSMigrationStage? { - return nil + nil } } // MARK: - StagedMigrationStep -extension ModelVersion where Self: StagedMigration { +extension ModelVersion where Self: StagedMigration { /// Returns a list of `StagedMigrationStep` needed to mirate to the next `version` of the store. - @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + @available( + iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, + macCatalystApplicationExtension 17.0, * + ) public func stagedMigrationSteps(to version: Self) -> [StagedMigrationStep] { guard self != version else { return [] diff --git a/Sources/Migration/StagedMigrationStep.swift b/Sources/Migration/StagedMigrationStep.swift index 7d84a598..a076e2a9 100644 --- a/Sources/Migration/StagedMigrationStep.swift +++ b/Sources/Migration/StagedMigrationStep.swift @@ -3,14 +3,17 @@ import CoreData // Representation of a Core Data staged migration step. -@available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) +@available( + iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, + macCatalystApplicationExtension 17.0, * +) public struct StagedMigrationStep { public let sourceVersion: Version public let sourceModelReference: NSManagedObjectModelReference public let destinationVersion: Version public let destinationModelReference: NSManagedObjectModelReference public let stage: NSMigrationStage - + init?(sourceVersion: Version, destinationVersion: Version) { guard let stage = sourceVersion.migrationStageToNextModelVersion() else { return nil diff --git a/Sources/NSFetchRequestResult+CoreData.swift b/Sources/NSFetchRequestResult+CoreData.swift index 34975af1..670dd7c9 100644 --- a/Sources/NSFetchRequestResult+CoreData.swift +++ b/Sources/NSFetchRequestResult+CoreData.swift @@ -11,17 +11,17 @@ extension NSManagedObject { if let name = entity().name { return name } - + // Fallback to string representation of Self - + // https://stackoverflow.com/questions/37909392/exc-bad-access-when-calling-new-entity-method-in-ios-10-macos-sierra-core-da // https://stackoverflow.com/questions/43231873/nspersistentcontainer-unittests-with-ios10/43286175 // https://www.jessesquires.com/blog/swift-coredata-and-testing/ // https://github.com/jessesquires/rdar-19368054 - + // Returning a string representation of Self metatype doesn't work if you have a NSManagedObject subclass // with a name different from the NSEntityDescription name (In that case is ovverride this property). - + return String(describing: Self.self) } } @@ -48,22 +48,22 @@ extension NSManagedObject { //} extension NSFetchRequestResult where Self: NSManagedObject { - + // MARK: - Fetch - + /// Returns a new fetch request initialized with the entity represented by this subclass (`self`). /// - Warning: This fetch request is created with a string name (`entityName`), and cannot respond to -entity until used by an NSManagedObjectContex. public static func newFetchRequest() -> NSFetchRequest { NSFetchRequest(entityName: entityName) } - + /// - Returns: an object for a specified `id` even if the object needs to be fetched. /// If the object is not registered in the context, it may be fetched or returned as a fault. /// If use existingObject(with:) if you don't want a faulted object. public static func object(with id: NSManagedObjectID, in context: NSManagedObjectContext) -> Self? { context.object(with: id) as? Self } - + /// - Returns: the object for the specified ID or nil if the object does not exist. /// If there is a managed object with the given ID already registered in the context, that object is returned directly; otherwise the corresponding object is faulted into the context. /// This method might perform I/O if the data is uncached. @@ -71,7 +71,7 @@ extension NSFetchRequestResult where Self: NSManagedObject { public static func existingObject(with id: NSManagedObjectID, in context: NSManagedObjectContext) throws -> Self? { try context.existingObject(with: id) as? Self } - + /// Performs a configurable fetch request in a context. /// - Note: It always accesses the underlying persistent stores to retrieve the latest results. /// - Attention: Core Data makes heavy use of Futures, especially for relationship values. @@ -83,8 +83,10 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - configuration: Configuration closure applied **only** before fetching. /// - Throws: It throws an error in cases of failure. /// - Returns: An array of objects meeting the criteria specified by request fetched from *the receiver* and from *the persistent stores* associated with the receiver’s persistent store coordinator. - public static func fetchObjects(in context: NSManagedObjectContext, - with configuration: (NSFetchRequest) -> Void = { _ in }) throws -> [Self] { + public static func fetchObjects( + in context: NSManagedObjectContext, + with configuration: (NSFetchRequest) -> Void = { _ in } + ) throws -> [Self] { // Check the Discussion paragraph for the fetch(_:) documentation: // https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext/1506672-fetch // When you execute an instance of NSFetchRequest, it always accesses the underlying persistent stores to retrieve the latest results. @@ -96,7 +98,7 @@ extension NSFetchRequestResult where Self: NSManagedObject { "This method requires a NSFetchRequest with resultType of .managedObjectResultType.") return try context.fetch(request) } - + /// Performs a configurable fetch request in a context. /// - Note: When fetching data from Core Data, you don’t always know how many values you’ll be getting back. /// Core Data solves this problem by using a subclass of `NSArray` that will dynamically pull in data from the underlying store on demand. @@ -113,15 +115,15 @@ extension NSFetchRequestResult where Self: NSManagedObject { ) throws -> NSArray { // Check the Discussion paragraph for the fetch(_:) documentation: // https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext/1506672-fetch - + // When you execute an instance of NSFetchRequest, it always accesses the underlying persistent stores to retrieve the latest results. // https://developer.apple.com/documentation/coredata/nsfetchrequest let request = NSFetchRequest(entityName: entityName) configuration(request) - + return try context.fetchNSArray(request) } - + /// Fetches all the `NSManagedObjectID` for a given predicate. /// - Note: it always accesses the underlying persistent stores to retrieve the latest results. /// @@ -132,10 +134,12 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - affectedStores: An array of persistent stores specified for the fetch request. /// - Returns: A list of `NSManagedObjectID`. /// - Throws: It throws an error in cases of failure. - public static func fetchObjectIDs(in context: NSManagedObjectContext, - includingSubentities: Bool = true, - where predicate: NSPredicate, - affectedStores: [NSPersistentStore]? = nil) throws -> [NSManagedObjectID] { + public static func fetchObjectIDs( + in context: NSManagedObjectContext, + includingSubentities: Bool = true, + where predicate: NSPredicate, + affectedStores: [NSPersistentStore]? = nil + ) throws -> [NSManagedObjectID] { let request = NSFetchRequest(entityName: entityName) // If includesPropertyValues is false, then Core Data fetches only the object ID information for the matching records—it does not populate the row cache. // @@ -152,12 +156,12 @@ extension NSFetchRequestResult where Self: NSManagedObject { request.includesSubentities = includingSubentities request.predicate = predicate request.affectedStores = affectedStores - + return try context.fetch(request) } - + // MARK: - First - + /// Fetches an object matching the given predicate. /// - Note: it always accesses the underlying persistent stores to retrieve the latest results. /// @@ -182,9 +186,9 @@ extension NSFetchRequestResult where Self: NSManagedObject { request.fetchLimit = 1 }.first } - + // MARK: - Unique - + /// Executes a fetch request where **at most** a single object is expected as result; if more than one object are fetched, a fatal error will occour. /// - Note: To guarantee uniqueness the fetch accesses the underlying persistent stores to retrieve the latest results and, also, matches against currently /// unsaved changes in the managed object context. @@ -194,16 +198,18 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - predicate: Matching predicate. /// - affectedStores: An array of persistent stores specified for the fetch request. /// - Returns: An unique object matching the given configuration (if any). - public static func fetchUniqueObject(in context: NSManagedObjectContext, - where predicate: NSPredicate, - affectedStores: [NSPersistentStore]? = nil) throws -> Self? { + public static func fetchUniqueObject( + in context: NSManagedObjectContext, + where predicate: NSPredicate, + affectedStores: [NSPersistentStore]? = nil + ) throws -> Self? { let result = try fetchObjects(in: context) { request in request.predicate = predicate request.includesPendingChanges = true // default, uniqueness should be guaranteed request.affectedStores = affectedStores request.fetchLimit = 2 } - + switch result.count { case 0: return nil @@ -213,9 +219,9 @@ extension NSFetchRequestResult where Self: NSManagedObject { fatalError("Returned multiple objects, expected only one.") } } - + // MARK: - Delete - + /// Specifies the objects (matching a given `predicate`) that should be removed from its persistent store when changes are committed. /// If objects have not yet been saved to a persistent store, they are simply removed from the context. /// If the dataset to delete is very large, use the `limit` value to decide the number of objects to be deleted otherwise the operation could last an unbounded amount time. @@ -223,11 +229,12 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// The delete request can be executed only to certain stores if `affectedStores` is not nil. /// - Note: `NSBatchDeleteRequest` would be more efficient but requires a context with an `NSPersistentStoreCoordinator` directly connected (no child context). /// - Throws: It throws an error in cases of failure. - public static func delete(in context: NSManagedObjectContext, - includingSubentities: Bool = true, - where predicate: NSPredicate = NSPredicate(value: true), - limit: Int? = nil, - affectedStores: [NSPersistentStore]? = nil + public static func delete( + in context: NSManagedObjectContext, + includingSubentities: Bool = true, + where predicate: NSPredicate = NSPredicate(value: true), + limit: Int? = nil, + affectedStores: [NSPersistentStore]? = nil ) throws { try autoreleasepool { try fetchObjects(in: context) { request in @@ -242,7 +249,7 @@ extension NSFetchRequestResult where Self: NSManagedObject { }.lazy.forEach(context.delete(_:)) } } - + /// Removes all entities from within the specified `NSManagedObjectContext` excluding a given list of entities. /// /// - Parameters: @@ -251,31 +258,35 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - affectedStores: An array of persistent stores specified for the fetch request. /// - Throws: It throws an error in cases of failure. /// - Note: `NSBatchDeleteRequest` would be more efficient but requires a context with an `NSPersistentStoreCoordinator` directly connected (no child context). - public static func delete(in context: NSManagedObjectContext, - except objects: [Self], - affectedStores: [NSPersistentStore]? = nil) throws { + public static func delete( + in context: NSManagedObjectContext, + except objects: [Self], + affectedStores: [NSPersistentStore]? = nil + ) throws { let predicate = NSPredicate(format: "NOT (self IN %@)", objects) try delete(in: context, includingSubentities: true, where: predicate, affectedStores: affectedStores) } - + // MARK: - Count - + /// Counts the results of a configurable fetch request in a context. /// - Throws: It throws an error in cases of failure. - public static func count(in context: NSManagedObjectContext, - for configuration: (NSFetchRequest) -> Void = { _ in }) throws -> Int { + public static func count( + in context: NSManagedObjectContext, + for configuration: (NSFetchRequest) -> Void = { _ in } + ) throws -> Int { let request = newFetchRequest() configuration(request) - + let result = try context.count(for: request) // result is equal to NSNotFound if an error occurs (an exception is expected to be thrown) guard result != NSNotFound else { return 0 } - + return result } - + // MARK: - Materialized Object - + /// Iterates over the context’s registeredObjects set (which contains all managed objects the context currently knows about) until it finds one that is not a fault matching for a given predicate. /// Faulted objects are not considered to prevent Core Data to make a round trip to the persistent store. /// @@ -286,12 +297,12 @@ extension NSFetchRequestResult where Self: NSManagedObject { public static func materializedObject(in context: NSManagedObjectContext, where predicate: NSPredicate) -> Self? { for object in context.registeredObjects where !object.isFault { guard let result = object as? Self, predicate.evaluate(with: result) else { continue } - + return result } return nil } - + /// Iterates over the context’s registeredObjects set (which contains all managed objects the context currently knows about) until it finds /// all the objects that aren't a fault matching for a given predicate. /// Faulted objects are not considered to prevent Core Data to make a round trip to the persistent store. @@ -319,14 +330,16 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - Throws: It throws an error in cases of failure. /// - Returns: The result returned when executing a batch update request. /// - Note: A batch delete can **only** be done on a SQLite store. - public static func batchUpdate(using context: NSManagedObjectContext, - configuration: (NSBatchUpdateRequest) -> Void) throws -> NSBatchUpdateResult { + public static func batchUpdate( + using context: NSManagedObjectContext, + configuration: (NSBatchUpdateRequest) -> Void + ) throws -> NSBatchUpdateResult { let batchRequest = NSBatchUpdateRequest(entityName: entityName) configuration(batchRequest) // swiftlint:disable:next force_cast return try context.execute(batchRequest) as! NSBatchUpdateResult } - + /// Executes a batch delete on the context's persistent store coordinator. /// - Parameters: /// - context: The context whose the persistent store coordinator will be used to execute the batch delete. @@ -338,11 +351,12 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - Returns: The result returned when executing a batch delete request. /// - Note: A batch delete can **only** be done on a SQLite store. @discardableResult - public static func batchDelete(using context: NSManagedObjectContext, - predicate: NSPredicate? = nil, - includesSubentities: Bool = true, - resultType: NSBatchDeleteRequestResultType = .resultTypeStatusOnly, - affectedStores: [NSPersistentStore]? = nil + public static func batchDelete( + using context: NSManagedObjectContext, + predicate: NSPredicate? = nil, + includesSubentities: Bool = true, + resultType: NSBatchDeleteRequestResultType = .resultTypeStatusOnly, + affectedStores: [NSPersistentStore]? = nil ) throws -> NSBatchDeleteResult { // Only a subset of NSFetchRequest properties are used by a NSBatchDeleteRequest // @@ -351,16 +365,16 @@ extension NSFetchRequestResult where Self: NSManagedObject { let request = NSFetchRequest(entityName: entityName) request.predicate = predicate request.includesSubentities = includesSubentities - + // swiftlint:disable:next force_cast let batchRequest = NSBatchDeleteRequest(fetchRequest: request as! NSFetchRequest) batchRequest.resultType = resultType batchRequest.affectedStores = affectedStores - + // swiftlint:disable:next force_cast return try context.execute(batchRequest) as! NSBatchDeleteResult } - + /// Executes a batch insert on the context's persistent store coordinator. /// - Parameters: /// - context: The context whose the persistent store coordinator will be used to execute the batch insert. @@ -369,19 +383,20 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - Throws: It throws an error in cases of failure /// - Returns: The result that Core Data returns when executing a batch-insertion request. /// - Note: A batch insert can **only** be done on a SQLite store. - public static func batchInsert(using context: NSManagedObjectContext, - resultType: NSBatchInsertRequestResultType = .statusOnly, - objects: [[String: Any]], - affectedStores: [NSPersistentStore]? = nil + public static func batchInsert( + using context: NSManagedObjectContext, + resultType: NSBatchInsertRequestResultType = .statusOnly, + objects: [[String: Any]], + affectedStores: [NSPersistentStore]? = nil ) throws -> NSBatchInsertResult { let batchRequest = NSBatchInsertRequest(entityName: entityName, objects: objects) batchRequest.resultType = resultType batchRequest.affectedStores = affectedStores - + // swiftlint:disable:next force_cast return try context.execute(batchRequest) as! NSBatchInsertResult } - + /// Executes a batch insert on the context's persistent store coordinator. /// Doing a batch insert with this method is more memory efficient than the standard batch insert where all the items are passed alltogether. /// - Parameters: @@ -398,11 +413,11 @@ extension NSFetchRequestResult where Self: NSManagedObject { ) throws -> NSBatchInsertResult { let batchRequest = NSBatchInsertRequest(entityName: entityName, dictionaryHandler: handler) batchRequest.resultType = resultType - + // swiftlint:disable:next force_cast return try context.execute(batchRequest) as! NSBatchInsertResult } - + /// Executes a batch insert on the context's persistent store coordinator. /// Doing a batch insert with this method is more memory efficient than the standard batch insert where all the items are passed alltogether. /// - Parameters: @@ -412,9 +427,10 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - Throws: It throws an error in cases of failure. /// - Returns: The result that Core Data returns when executing a batch-insertion request. /// - Note: A batch insert can **only** be done on a SQLite store. - public static func batchInsert(using context: NSManagedObjectContext, - resultType: NSBatchInsertRequestResultType = .statusOnly, - managedObjectHandler handler: @escaping (Self) -> Bool + public static func batchInsert( + using context: NSManagedObjectContext, + resultType: NSBatchInsertRequestResultType = .statusOnly, + managedObjectHandler handler: @escaping (Self) -> Bool ) throws -> NSBatchInsertResult { let batchRequest = NSBatchInsertRequest( entityName: entityName, @@ -423,7 +439,7 @@ extension NSFetchRequestResult where Self: NSManagedObject { handler(object as! Self) }) batchRequest.resultType = resultType - + // swiftlint:disable:next force_cast return try context.execute(batchRequest) as! NSBatchInsertResult } @@ -453,14 +469,15 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - Warning: If the ConcurrencyDebug is enabled, the fetch request will cause a thread violation error. /// ([more details here](https://stackoverflow.com/questions/31728425/coredata-asynchronous-fetch-causes-concurrency-debugger-error)). @discardableResult - public static func fetchObjects(in context: NSManagedObjectContext, - estimatedResultCount: Int = 0, - with configuration: (NSFetchRequest) -> Void = { _ in }, - completion: @escaping (Result<[Self], Error>) -> Void + public static func fetchObjects( + in context: NSManagedObjectContext, + estimatedResultCount: Int = 0, + with configuration: (NSFetchRequest) -> Void = { _ in }, + completion: @escaping (Result<[Self], Error>) -> Void ) throws -> NSAsynchronousFetchResult { let request = Self.newFetchRequest() configuration(request) - + let asynchronousRequest = NSAsynchronousFetchRequest(fetchRequest: request) { result in if let error = result.operationError { completion(.failure(error)) @@ -471,7 +488,7 @@ extension NSFetchRequestResult where Self: NSManagedObject { } } asynchronousRequest.estimatedResultCount = estimatedResultCount - + // swiftlint:disable:next force_cast return try context.execute(asynchronousRequest) as! NSAsynchronousFetchResult } @@ -480,27 +497,32 @@ extension NSFetchRequestResult where Self: NSManagedObject { extension NSFetchRequestResult where Self: NSManagedObject { /// Performs a configurable asynchronous fetch request in a context. /// - /// - Parameter context: Searched context. + /// - Parameter context: Searched context (used internally to execute the request). /// - Parameter estimatedResultCount: A parameter that assists Core Data with scheduling the asynchronous fetch request. /// - Parameter configuration: Configuration closure called when preparing the `NSFetchRequest`. /// - Returns: The results that were received from the fetch request. /// - Throws: It throws an error in cases of failure. /// - Warning: If the ConcurrencyDebug is enabled, the fetch request will cause a thread violation error, without it data races will be always detected by Xcode. - public static func fetchObjects(in context: NSManagedObjectContext, - estimatedResultCount: Int = 0, - with configuration: (NSFetchRequest) -> Void = { _ in }) async throws -> [Self] { + public static func fetchObjects( + in context: NSManagedObjectContext, + estimatedResultCount: Int = 0, + with configuration: (NSFetchRequest) -> Void = { _ in }, + progress: ((Progress) -> Void)? = nil + ) async throws -> [Self] { try await withCheckedThrowingContinuation { continuation in do { // TODO: Swift concurrency and NSProgress: https://github.com/apple/swift-evolution/blob/main/proposals/0297-concurrency-objc.md#nsprogress - // TODO: The associated test is disabled because of data races. - try fetchObjects(in: context, estimatedResultCount: estimatedResultCount, with: configuration) { result in - switch result { - case .success(let fetchResult): - continuation.resume(returning: fetchResult) - case .failure(let error): - continuation.resume(throwing: error) + try fetchObjects(in: context, estimatedResultCount: estimatedResultCount, with: configuration) { result in + //currentProgress.resignCurrent() + switch result { + case .success(let fetchResult): + // it's up to the caller to access the result (NSManagedObject objects) from the correct NSManagedContext to avoid thread issues + nonisolated(unsafe) let _fetchResult = fetchResult + continuation.resume(returning: _fetchResult) + case .failure(let error): + continuation.resume(throwing: error) + } } - } } catch let error { continuation.resume(throwing: error) } diff --git a/Sources/NSManagedObjectContext+History.swift b/Sources/NSManagedObjectContext+History.swift index 851207da..25249cf9 100644 --- a/Sources/NSManagedObjectContext+History.swift +++ b/Sources/NSManagedObjectContext+History.swift @@ -56,7 +56,7 @@ extension NSManagedObjectContext { /// /// - Important: The merging operation must be done inside a the context queue. /// - /// Returns the last merged transaction's token and timestamp. + /// - Returns: the last merged transaction's token and timestamp. public func mergeTransactions(_ transactions: [NSPersistentHistoryTransaction]) throws -> ( NSPersistentHistoryToken, Date )? { diff --git a/Sources/NSPredicate+Utils.swift b/Sources/NSPredicate+Utils.swift index 1d15d0bd..3bdbeceb 100644 --- a/Sources/NSPredicate+Utils.swift +++ b/Sources/NSPredicate+Utils.swift @@ -4,10 +4,10 @@ import Foundation extension NSPredicate { /// A `NSPredicate` that always evaluates to `true`. - public static let `true` = NSPredicate(value: true) + public final class func `true`() -> NSPredicate { NSPredicate(value: true) } /// A `NSPredicate` that always evaluates to `false`. - public static let `false` = NSPredicate(value: false) + public final class func `false`() -> NSPredicate { NSPredicate(value: false) } /// Returns a `new` compound NSPredicate formed by **AND**-ing `self` with `predicate`. /// - Parameter predicate: A `NSPredicate` object. diff --git a/Tests/FetchedResultsChanges_Tests.swift b/Tests/FetchedResultsChanges_Tests.swift index 6d6ba0d5..baa19b49 100644 --- a/Tests/FetchedResultsChanges_Tests.swift +++ b/Tests/FetchedResultsChanges_Tests.swift @@ -20,10 +20,11 @@ final class FetchedResultsChanges_Tests: InMemoryTestCase { delegate.didChangeExpectation.isInverted = true // When - let controller = NSFetchedResultsController(fetchRequest: request, - managedObjectContext: context, - sectionNameKeyPath: nil, - cacheName: nil) + let controller = NSFetchedResultsController( + fetchRequest: request, + managedObjectContext: context, + sectionNameKeyPath: nil, + cacheName: nil) controller.delegate = delegate try controller.performFetch() @@ -134,7 +135,7 @@ final class FetchedResultsChanges_Tests: InMemoryTestCase { XCTAssertEqual(deletes.count, 1) XCTAssertEqual(moves.count, 1) XCTAssertEqual(updates.count, 2) - + for insert in inserts { XCTAssertTrue(insert.isInsertion) XCTAssertFalse(insert.isMove) @@ -142,7 +143,7 @@ final class FetchedResultsChanges_Tests: InMemoryTestCase { XCTAssertFalse(insert.isDeletion) XCTAssert(insert.object is Person) } - + for delete in deletes { XCTAssertFalse(delete.isInsertion) XCTAssertFalse(delete.isMove) @@ -150,7 +151,7 @@ final class FetchedResultsChanges_Tests: InMemoryTestCase { XCTAssertTrue(delete.isDeletion) XCTAssert(delete.object is Person) } - + for udpate in updates { XCTAssertFalse(udpate.isInsertion) XCTAssertFalse(udpate.isMove) @@ -158,7 +159,7 @@ final class FetchedResultsChanges_Tests: InMemoryTestCase { XCTAssertFalse(udpate.isDeletion) XCTAssert(udpate.object is Person) } - + for move in moves { XCTAssertFalse(move.isInsertion) XCTAssertTrue(move.isMove) @@ -180,10 +181,11 @@ final class FetchedResultsChanges_Tests: InMemoryTestCase { let delegate = MockNSFetchedResultControllerDelegate() // When - let controller = NSFetchedResultsController(fetchRequest: request, - managedObjectContext: context, - sectionNameKeyPath: #keyPath(Person.lastName), - cacheName: nil) + let controller = NSFetchedResultsController( + fetchRequest: request, + managedObjectContext: context, + sectionNameKeyPath: #keyPath(Person.lastName), + cacheName: nil) controller.delegate = delegate try controller.performFetch() @@ -205,7 +207,7 @@ final class FetchedResultsChanges_Tests: InMemoryTestCase { // Then wait(for: [delegate.willChangeExpectation, delegate.didChangeExpectation], timeout: 10) XCTAssertEqual(delegate.sectionChanges.count, 3) - + let inserts = delegate.sectionChanges.filter { guard case FetchedResultsSectionChange.insert = $0 else { return false } return true @@ -214,16 +216,16 @@ final class FetchedResultsChanges_Tests: InMemoryTestCase { guard case FetchedResultsSectionChange.delete = $0 else { return false } return true } - + XCTAssertEqual(inserts.count, 1) XCTAssertEqual(deletes.count, 2) - + for insert in inserts { XCTAssertTrue(insert.isInsertion) XCTAssertFalse(insert.isDeletion) - XCTAssertEqual(insert.info.objects.count, count) // 2 peopble have been renamed + XCTAssertEqual(insert.info.objects.count, count) // 2 peopble have been renamed } - + for delete in deletes { XCTAssertFalse(delete.isInsertion) XCTAssertTrue(delete.isDeletion) @@ -245,10 +247,11 @@ final class FetchedResultsChanges_Tests: InMemoryTestCase { let delegate = MockNSFetchedResultControllerDelegate() // When - let controller = NSFetchedResultsController(fetchRequest: request, - managedObjectContext: context, - sectionNameKeyPath: nil, - cacheName: nil) + let controller = NSFetchedResultsController( + fetchRequest: request, + managedObjectContext: context, + sectionNameKeyPath: nil, + cacheName: nil) controller.delegate = delegate try controller.performFetch() @@ -282,26 +285,32 @@ private final class MockNSFetchedResultControllerDelegate: N var willChangeExpectation = XCTestExpectation(description: "\(#function)\(#line)") var didChangeExpectation = XCTestExpectation(description: "\(#function)\(#line)") - public func controller(_ controller: NSFetchedResultsController, - didChange anObject: Any, - at indexPath: IndexPath?, - for type: NSFetchedResultsChangeType, newIndexPath: IndexPath? + public func controller( + _ controller: NSFetchedResultsController, + didChange anObject: Any, + at indexPath: IndexPath?, + for type: NSFetchedResultsChangeType, newIndexPath: IndexPath? ) { - if let change = FetchedResultsObjectChange(object: anObject, - indexPath: indexPath, - changeType: type, - newIndexPath: newIndexPath) + if let change = FetchedResultsObjectChange( + object: anObject, + indexPath: indexPath, + changeType: type, + newIndexPath: newIndexPath) { changes.append(change) } } - public func controller(_ controller: NSFetchedResultsController, - didChange sectionInfo: NSFetchedResultsSectionInfo, - atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) { - if let change = FetchedResultsSectionChange(section: sectionInfo, - index: sectionIndex, - changeType: type) { + public func controller( + _ controller: NSFetchedResultsController, + didChange sectionInfo: NSFetchedResultsSectionInfo, + atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType + ) { + if let change = FetchedResultsSectionChange( + section: sectionInfo, + index: sectionIndex, + changeType: type) + { sectionChanges.append(change) } } diff --git a/Tests/Migrations_Tests.swift b/Tests/Migrations_Tests.swift index fcdc6b80..5804b1b7 100644 --- a/Tests/Migrations_Tests.swift +++ b/Tests/Migrations_Tests.swift @@ -61,8 +61,9 @@ final class Migrations_Tests: BaseTestCase { try migrator.migrate(enableWALCheckpoint: enableWALCheckpoint) // ⚠️ migration should be done before loading the NSPersistentContainer instance or you need to create a new one after the migration - let migratedContainer = NSPersistentContainer(name: name, - managedObjectModel: targetVersion.managedObjectModel()) + let migratedContainer = NSPersistentContainer( + name: name, + managedObjectModel: targetVersion.managedObjectModel()) let expectation2 = expectation(description: "\(#function)\(#line)") migratedContainer.loadPersistentStores { (store, error) in @@ -106,7 +107,7 @@ final class Migrations_Tests: BaseTestCase { destinationStoreDescription: destinationDescription, targetVersion: .version1) try migrator.migrate(enableWALCheckpoint: true) - + try NSPersistentStoreCoordinator.destroyStore(at: sourceURL) } @@ -122,8 +123,9 @@ final class Migrations_Tests: BaseTestCase { // When let targetDescription = NSPersistentStoreDescription(url: sourceURL) - let migrator = Migrator(targetStoreDescription: targetDescription, - targetVersion: targetVersion) + let migrator = Migrator( + targetStoreDescription: targetDescription, + targetVersion: targetVersion) migrator.enableLog = true migrator.enableLog = false @@ -134,8 +136,9 @@ final class Migrations_Tests: BaseTestCase { try migrator.migrate(enableWALCheckpoint: true) - let migratedContext = NSManagedObjectContext(model: targetVersion.managedObjectModel(), - storeURL: sourceURL) + let migratedContext = NSManagedObjectContext( + model: targetVersion.managedObjectModel(), + storeURL: sourceURL) let luxuryCars = try LuxuryCar.fetchObjects(in: migratedContext) XCTAssertEqual(luxuryCars.count, 5) @@ -163,7 +166,7 @@ final class Migrations_Tests: BaseTestCase { func test_MigrationFromV1ToV2UsingCustomMigratorProvider() throws { let sourceURL = try Self.createSQLiteSample1ForV1() - + let targetVersion = SampleModelVersion.version2 let steps = SampleModelVersion.version1.migrationSteps(to: .version2) XCTAssertEqual(steps.count, 1) @@ -225,13 +228,15 @@ final class Migrations_Tests: BaseTestCase { let sourceDescription = NSPersistentStoreDescription(url: sourceURL) let destinationDescription = NSPersistentStoreDescription(url: sourceURL) - let migrator = Migrator(sourceStoreDescription: sourceDescription, - destinationStoreDescription: destinationDescription, - targetVersion: .version3) + let migrator = Migrator( + sourceStoreDescription: sourceDescription, + destinationStoreDescription: destinationDescription, + targetVersion: .version3) try migrator.migrate(enableWALCheckpoint: true) - let migratedContext = NSManagedObjectContext(model: SampleModelVersion.version3.managedObjectModel(), - storeURL: targetURL) + let migratedContext = NSManagedObjectContext( + model: SampleModelVersion.version3.managedObjectModel(), + storeURL: targetURL) let cars = try migratedContext.fetch(NSFetchRequest(entityName: "Car")) let makers = try Maker.fetchObjects(in: migratedContext) XCTAssertEqual(makers.count, 10) @@ -267,9 +272,10 @@ final class Migrations_Tests: BaseTestCase { let sourceURL = try Self.createSQLiteSample1ForV2() let sourceDescription = NSPersistentStoreDescription(url: sourceURL) let destinationDescription = NSPersistentStoreDescription(url: sourceURL) - let migrator = Migrator(sourceStoreDescription: sourceDescription, - destinationStoreDescription: destinationDescription, - targetVersion: .version3) + let migrator = Migrator( + sourceStoreDescription: sourceDescription, + destinationStoreDescription: destinationDescription, + targetVersion: .version3) migrator.enableLog = true // When migrator.progress.cancel() @@ -300,7 +306,7 @@ final class Migrations_Tests: BaseTestCase { let sourceURL = try Self.createSQLiteSample1ForV1() let version = try SampleModelVersion(persistentStoreURL: sourceURL as URL) XCTAssertTrue(version == .version1) - + XCTAssertTrue(SampleModelVersion.version1.isLightWeightMigrationPossibleToNextModelVersion()) XCTAssertTrue(SampleModelVersion.version2.isLightWeightMigrationPossibleToNextModelVersion()) XCTAssertFalse(SampleModelVersion.version3.isLightWeightMigrationPossibleToNextModelVersion()) @@ -308,9 +314,10 @@ final class Migrations_Tests: BaseTestCase { let targetURL = URL.temporaryDirectory.appendingPathComponent("SampleModel").appendingPathExtension("sqlite") let sourceDescription = NSPersistentStoreDescription(url: sourceURL) let destinationDescription = NSPersistentStoreDescription(url: targetURL) - let migrator = Migrator(sourceStoreDescription: sourceDescription, - destinationStoreDescription: destinationDescription, - targetVersion: .version3) + let migrator = Migrator( + sourceStoreDescription: sourceDescription, + destinationStoreDescription: destinationDescription, + targetVersion: .version3) migrator.enableLog = true let completion = OSAllocatedUnfairLock(initialState: 0.0) let token = migrator.progress.observe(\.fractionCompleted, options: [.new]) { (progress, change) in @@ -319,8 +326,9 @@ final class Migrations_Tests: BaseTestCase { } try migrator.migrate(enableWALCheckpoint: true) - let migratedContext = NSManagedObjectContext(model: SampleModelVersion.version3.managedObjectModel(), - storeURL: targetURL) + let migratedContext = NSManagedObjectContext( + model: SampleModelVersion.version3.managedObjectModel(), + storeURL: targetURL) let makers = try migratedContext.fetch(NSFetchRequest(entityName: "Maker")) XCTAssertEqual(makers.count, 10) @@ -362,7 +370,7 @@ extension Migrations_Tests { let bundle = Bundle.tests // 125 cars, 5 sport cars let _sourceURL = try XCTUnwrap(bundle.url(forResource: "SampleModel_V1", withExtension: "sqlite")) - + // Being the test run multiple times, we create an unique copy for every test let uuid = UUID().uuidString let sourceURL = bundle.bundleURL.appendingPathComponent("SampleModel_V1_copy-\(uuid).sqlite") @@ -370,12 +378,12 @@ extension Migrations_Tests { XCTAssertTrue(FileManager.default.fileExists(atPath: sourceURL.path)) return sourceURL } - + static func createSQLiteSample1ForV2() throws -> URL { let bundle = Bundle.tests // 125 cars, 5 sport cars let _sourceURL = try XCTUnwrap(bundle.url(forResource: "SampleModel_V2", withExtension: "sqlite")) - + // Being the test run multiple times, we create an unique copy for every test let uuid = UUID().uuidString let sourceURL = bundle.bundleURL.appendingPathComponent("SampleModel_V2_copy-\(uuid).sqlite") @@ -383,7 +391,7 @@ extension Migrations_Tests { XCTAssertTrue(FileManager.default.fileExists(atPath: sourceURL.path)) return sourceURL } - + /// Creates a .sqlite with some data for the initial model (version 1) static func createSampleVersion1(completion: @escaping (Result) -> Void) { let containerSQLite = NSPersistentContainer(name: "SampleModel-\(UUID())", managedObjectModel: model1) diff --git a/Tests/NSEntityDescriptionUtils_Tests.swift b/Tests/NSEntityDescriptionUtils_Tests.swift index 55882026..a92895cc 100644 --- a/Tests/NSEntityDescriptionUtils_Tests.swift +++ b/Tests/NSEntityDescriptionUtils_Tests.swift @@ -14,9 +14,9 @@ extension NSManagedObject { } // This entity is not mapped in any model and it will trigger an error: -// "No NSEntityDescriptions in any model claim the NSManagedObject subclass 'CoreDataPlus_Tests.FakeEntity' +// "No NSEntityDescriptions in any model claim the NSManagedObject subclass 'CoreDataPlus_Tests.FakeEntity' // so +entity is confused. Have you loaded your NSManagedObjectModel yet ?" -private class FakeEntity: NSManagedObject { } +private class FakeEntity: NSManagedObject {} final class NSEntityDescriptionUtils_Tests: InMemoryTestCase { func test_EntityName() { @@ -24,7 +24,7 @@ final class NSEntityDescriptionUtils_Tests: InMemoryTestCase { XCTAssertEqual(FakeEntity.entityName, "FakeEntity") XCTAssertEqual(SportCar.entityName, "SportCar") } - + func test_Entity() { let context = container.viewContext let expensiveCar = ExpensiveSportCar(context: context) @@ -50,10 +50,10 @@ final class NSEntityDescriptionUtils_Tests: InMemoryTestCase { XCTFail("Car Entity not found; available entities: \(entities)") return } - + // Car.entity().name can be nil while running tests // To avoid some random failed tests, the entity is created by looking in a context. - guard + guard let carEntity = NSEntityDescription.entity(forEntityName: Car.entityName, in: container.viewContext) else { XCTFail("Car Entity Not Found.") @@ -156,7 +156,7 @@ final class NSEntityDescriptionUtils_Tests: InMemoryTestCase { do { let entities = [ - ExpensiveSportCar(context: context).entity, + ExpensiveSportCar(context: context).entity, ExpensiveSportCar(context: context).entity, SportCar(context: context).entity, SportCar(context: context).entity, diff --git a/Tests/NSFetchRequestResultUtils_Tests.swift b/Tests/NSFetchRequestResultUtils_Tests.swift index 7ca32c90..90244858 100644 --- a/Tests/NSFetchRequestResultUtils_Tests.swift +++ b/Tests/NSFetchRequestResultUtils_Tests.swift @@ -745,27 +745,27 @@ final class NSFetchRequestResultUtils_Tests: OnDiskTestCase { let count = try Car.count(in: context) XCTAssertEqual(count, total) } - - @MainActor + func test_BatchInserWithNSFetchedResultController() throws { // How to make FRC aware of batch insertions - + let context = container.viewContext context.automaticallyMergesChangesFromParent = true let backgroundContext = container.newBackgroundContext() - + let request = Car.fetchRequest() request.addSortDescriptors([]) let delegate = FetchedResultsControllerMockDelegate() - let frc = NSFetchedResultsController(fetchRequest: request, - managedObjectContext: context, - sectionNameKeyPath: nil, - cacheName: nil) + let frc = NSFetchedResultsController( + fetchRequest: request, + managedObjectContext: context, + sectionNameKeyPath: nil, + cacheName: nil) frc.delegate = delegate try frc.performFetch() - + XCTAssertTrue((frc.fetchedObjects ?? []).isEmpty) - + let objects = [ [ #keyPath(Car.maker): "FIAT", @@ -783,18 +783,19 @@ final class NSFetchRequestResultUtils_Tests: OnDiskTestCase { #keyPath(Car.model): "Panda", ], ] - + try backgroundContext.performAndWait { - let result: NSBatchInsertResult = try Car.batchInsert(using: $0, - resultType: .objectIDs, - objects: objects) - try $0.save() // not needed because batch insertions are done directly in the store - XCTAssertEqual(delegate.insertedObjects.count, 0) // FRC is not aware yet - + let result: NSBatchInsertResult = try Car.batchInsert( + using: $0, + resultType: .objectIDs, + objects: objects) + try $0.save() // not needed because batch insertions are done directly in the store + XCTAssertEqual(delegate.insertedObjects.count, 0) // FRC is not aware yet + let changes = try XCTUnwrap(result.changes) - NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context]) // make FRC aware of inserts + NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context]) // make FRC aware of inserts } - + XCTAssertEqual(delegate.insertedObjects.count, 3) XCTAssertEqual(delegate.deletedObjects.count, 0) XCTAssertEqual(delegate.updatedObjects.count, 0) @@ -947,7 +948,6 @@ final class NSFetchRequestResultUtils_Tests: OnDiskTestCase { // MARK: - Async Fetch - @MainActor func test_AsyncFetch() throws { // BUG: Async fetches can't be tested with the ConcurrencyDebug enabled, // https://stackoverflow.com/questions/31728425/coredata-asynchronous-fetch-causes-concurrency-debugger-error @@ -985,29 +985,31 @@ final class NSFetchRequestResultUtils_Tests: OnDiskTestCase { } } - waitForExpectations(timeout: 30, handler: nil) + wait(for: [expectation1, expectation2], timeout: 30) currentProgress.resignCurrent() currentToken?.invalidate() } - // func test_AsyncFetchUsingSwiftConcurrency() async throws { - // // In test_AsyncFetch() the standard implementation doesn't pass the test only if we enable ConcurrencyDebug. - // // The async/await version (that is, btw, used in WWDC 2021 videos on how to use continuations) always fails due to data races. - // // https://stackoverflow.com/questions/31728425/coredata-asynchronous-fetch-causes-concurrency-debugger-error - // try XCTSkipIf(UserDefaults.standard.integer(forKey: "com.apple.CoreData.ConcurrencyDebug") == 1) - // let mainContext = container.viewContext - // - // // https://forums.developer.apple.com/forums/thread/741461 - // - // (1...10_000).forEach { - // let car = Car(context: mainContext) - // car.numberPlate = "test\($0)" - // } - // try mainContext.save() - // - // let results = try await Car.fetchObjects(in: mainContext) { $0.predicate = .true } - // XCTAssertEqual(results.count, 10_000) - // } + func test_AsyncFetchUsingSwiftConcurrency() async throws { + // In test_AsyncFetch() the standard implementation doesn't pass the test only if we enable ConcurrencyDebug. + // The async/await version (that is, btw, used in WWDC 2021 videos on how to use continuations) always fails due to data races. + // https://stackoverflow.com/questions/31728425/coredata-asynchronous-fetch-causes-concurrency-debugger-error + try XCTSkipIf(UserDefaults.standard.integer(forKey: "com.apple.CoreData.ConcurrencyDebug") == 1) + let mainContext = container.newBackgroundContext() + + // https://forums.developer.apple.com/forums/thread/741461 + + try mainContext.performAndWait { context in + (1...10_000).forEach { + let car = Car(context: mainContext) + car.numberPlate = "test\($0)" + } + try mainContext.save() + } + + let results = try await Car.fetchObjects(in: mainContext) { $0.predicate = .true() } + XCTAssertEqual(results.count, 10_000) + } // MARK: - Subquery diff --git a/Tests/NSManagedObjectContextHistory_Tests.swift b/Tests/NSManagedObjectContextHistory_Tests.swift index 97bf4b73..11641a98 100644 --- a/Tests/NSManagedObjectContextHistory_Tests.swift +++ b/Tests/NSManagedObjectContextHistory_Tests.swift @@ -6,7 +6,7 @@ import XCTest @testable import CoreDataPlus final class NSManagedObjectContextHistory_Tests: BaseTestCase { - @MainActor + func test_MergeHistoryAfterDate() throws { // Given let id = UUID() @@ -42,7 +42,7 @@ final class NSManagedObjectContextHistory_Tests: BaseTestCase { XCTAssertNotNil(result) XCTAssertTrue(viewContext2.registeredObjects.isEmpty) - waitForExpectations(timeout: 5, handler: nil) + wait(for: [expectation1], timeout: 5) cancellable.cancel() let status = try viewContext2.deleteHistory(before: result!.0) XCTAssertTrue(status) @@ -61,7 +61,6 @@ final class NSManagedObjectContextHistory_Tests: BaseTestCase { try container1.destroy() } - @MainActor func test_MergeHistoryAfterDateWithMultipleTransactions() throws { // Given let id = UUID() @@ -141,7 +140,8 @@ final class NSManagedObjectContextHistory_Tests: BaseTestCase { let result = try viewContext2.mergeTransactions(transactionsFromDistantPast) XCTAssertNotNil(result) - waitForExpectations(timeout: 5, handler: nil) + + wait(for: [expectation1, expectation2, expectation3], timeout: 5) try viewContext2.save() //print(viewContext2.insertedObjects) @@ -189,7 +189,6 @@ final class NSManagedObjectContextHistory_Tests: BaseTestCase { try container1.destroy() } - @MainActor func test_PersistentStoreWithHistoryTrackingEnabledGeneratesHistoryTokens() throws { // Given let psc = NSPersistentStoreCoordinator(managedObjectModel: model1) @@ -214,7 +213,7 @@ final class NSManagedObjectContextHistory_Tests: BaseTestCase { person1.lastName = "Moreton" try context.save() - waitForExpectations(timeout: 5, handler: nil) + wait(for: [expectation1], timeout: 5) cancellable.cancel() let result = try context.deleteHistory() @@ -227,7 +226,6 @@ final class NSManagedObjectContextHistory_Tests: BaseTestCase { try NSPersistentStoreCoordinator.destroyStore(at: storeURL) } - @MainActor func test_PersistentStoreWithHistoryTrackingDisabledDoesntGenerateHistoryTokens() throws { // Given let psc = NSPersistentStoreCoordinator(managedObjectModel: model1) @@ -253,7 +251,7 @@ final class NSManagedObjectContextHistory_Tests: BaseTestCase { person1.lastName = "Moreton" try context.save() - waitForExpectations(timeout: 5, handler: nil) + wait(for: [expectation1], timeout: 5) cancellable.cancel() // cleaning avoiding SQLITE warnings @@ -291,8 +289,7 @@ final class NSManagedObjectContextHistory_Tests: BaseTestCase { let result1 = try XCTUnwrap(try viewContext.deleteHistory(before: firstTransaction1)) XCTAssertTrue(result1) - let transactions2 = try viewContext.historyTransactions( - using: NSPersistentHistoryChangeRequest.fetchHistory(after: .distantPast)) + let transactions2 = try viewContext.historyTransactions(using: .fetchHistory(after: .distantPast)) XCTAssertEqual(transactions2.count, 2) let lastTransaction2 = try XCTUnwrap(transactions2.last) // Removes all the transactions before the last one: 1 transaction gets deleted @@ -304,7 +301,7 @@ final class NSManagedObjectContextHistory_Tests: BaseTestCase { XCTAssertEqual(transactions3.count, 1) } - func test_FetchHistoryChangesUsingFetchRequest() throws { + func test_InvestigationHistoryTokens() throws { // Given let id = UUID() let container1 = OnDiskPersistentContainer.makeNew(id: id) @@ -313,60 +310,196 @@ final class NSManagedObjectContextHistory_Tests: BaseTestCase { let viewContext1 = container1.viewContext viewContext1.name = "viewContext1" viewContext1.transactionAuthor = "author1" + let viewContext2 = container2.viewContext viewContext2.name = "viewContext2" viewContext2.transactionAuthor = "author2" + let psc2 = container2.persistentStoreCoordinator + let token1 = try XCTUnwrap(psc2.currentPersistentHistoryToken(fromStores: psc2.persistentStores)) - let lastHistoryToken = try XCTUnwrap(psc2.currentPersistentHistoryToken(fromStores: psc2.persistentStores)) + do { + let predicate = NSPredicate(format: "%K = %@", #keyPath(NSPersistentHistoryTransaction.token), token1) + let request = try NSPersistentHistoryChangeRequest.makeTransactionFetchRequest( + predicate: predicate, with: viewContext2) + _ = try viewContext2.historyTransactions(using: request) + XCTFail("Behavior changed again! Investigate") + } catch let error as NSError { + XCTAssertEqual( + error.code, NSPersistentHistoryTokenExpiredError, + "Starting iOS 18/macOS 15, using the initial token in NSPredicate.init(format:_:) triggers a NSPersistentHistoryTokenExpiredError unless used with fetchHistory(after:)" + ) + } viewContext1.fillWithSampleData() + try viewContext1.save() + + let token2 = try XCTUnwrap(psc2.currentPersistentHistoryToken(fromStores: psc2.persistentStores)) + + do { + let predicate = NSPredicate(format: "%K = %@", #keyPath(NSPersistentHistoryTransaction.token), token2) + let request = try NSPersistentHistoryChangeRequest.makeTransactionFetchRequest( + predicate: predicate, with: viewContext2) + let allTransactions = try viewContext2.historyTransactions(using: request) + XCTAssertEqual(allTransactions.count, 1) + } + let person = Person(context: viewContext1) + person.firstName = "John" + person.lastName = "Doe" + try viewContext1.save() + + do { + let predicate = NSPredicate(format: "%K = %@", #keyPath(NSPersistentHistoryTransaction.token), token2) + let request = try NSPersistentHistoryChangeRequest.makeTransactionFetchRequest( + predicate: predicate, with: viewContext2) + let allTransactions = try viewContext2.historyTransactions(using: request) + XCTAssertEqual(allTransactions.count, 1) + } + + let token3 = try XCTUnwrap(psc2.currentPersistentHistoryToken(fromStores: psc2.persistentStores)) + + do { + let predicate = NSPredicate(format: "%K >= %@", #keyPath(NSPersistentHistoryTransaction.token), token2) + let request = try NSPersistentHistoryChangeRequest.makeTransactionFetchRequest( + predicate: predicate, with: viewContext2) + let allTransactions = try viewContext2.historyTransactions(using: request) + XCTAssertEqual(allTransactions.count, 2) + } + + do { + let predicate = NSPredicate(format: "%K <= %@", #keyPath(NSPersistentHistoryTransaction.token), token3) + let request = try NSPersistentHistoryChangeRequest.makeTransactionFetchRequest( + predicate: predicate, with: viewContext2) + let allTransactions = try viewContext2.historyTransactions(using: request) + XCTAssertEqual(allTransactions.count, 2) + } + + do { + let request = NSPersistentHistoryChangeRequest.fetchHistory(after: token1) + let allTransactions = try viewContext2.historyTransactions(using: request) + XCTAssertEqual(allTransactions.count, 2) + } + + // cleaning avoiding SQLITE warnings + let psc1 = viewContext1.persistentStoreCoordinator! + for store in psc1.persistentStores { + try psc1.remove(store) + } + + for store in psc2.persistentStores { + try psc2.remove(store) + } + + try container1.destroy() + } + + func test_FetchHistoryChangesUsingFetchRequest() throws { + // Given + let id = UUID() + let container1 = OnDiskPersistentContainer.makeNew(id: id) + let container2 = OnDiskPersistentContainer.makeNew(id: id) + + let viewContext1 = container1.viewContext + viewContext1.name = "viewContext1" + viewContext1.transactionAuthor = "author1" + + let viewContext2 = container2.viewContext + viewContext2.name = "viewContext2" + viewContext2.transactionAuthor = "author2" + + let psc2 = container2.persistentStoreCoordinator + let oldHistoryToken = try XCTUnwrap(psc2.currentPersistentHistoryToken(fromStores: psc2.persistentStores)) + + viewContext1.fillWithSampleData() try viewContext1.save() let newHistoryToken = try XCTUnwrap(psc2.currentPersistentHistoryToken(fromStores: psc2.persistentStores)) - let tokenGreaterThanLastHistoryTokenPredicate = NSPredicate(format: "%@ < token", lastHistoryToken) - let tokenGreaterThanNewHistoryTokenPredicate = NSPredicate(format: "%@ < token", newHistoryToken) - let notAuthor2Predicate = NSPredicate(format: "author != %@", "author2") - let notAuthor1Predicate = NSPredicate(format: "author != %@", "author1") + do { + let request = NSPersistentHistoryChangeRequest.fetchHistory(after: oldHistoryToken) + let allTransactions = try viewContext2.historyTransactions(using: request) + XCTAssertEqual(allTransactions.count, 1) + } - func makeTransactionFetchRequest(predicate: NSPredicate) throws -> NSPersistentHistoryChangeRequest { - let request = try XCTUnwrap(NSPersistentHistoryChangeRequest.makeTransactionFetchRequest(with: viewContext2)) - request.predicate = predicate - let transactionsRequest = NSPersistentHistoryChangeRequest.fetchHistory(withFetch: request) - transactionsRequest.resultType = .transactionsOnly - return transactionsRequest + do { + let predicate = NSPredicate(format: "%K > %@", #keyPath(NSPersistentHistoryTransaction.token), oldHistoryToken) + let request = try NSPersistentHistoryChangeRequest.makeTransactionFetchRequest( + predicate: predicate, with: viewContext2) + _ = try viewContext2.historyTransactions(using: request) + XCTFail("Behavior changed again! Investigate") + } catch let error as NSError { + XCTAssertEqual( + error.code, NSPersistentHistoryTokenExpiredError, + "Starting iOS 18/macOS 15, using the initial token in NSPredicate.init(format:_:) triggers a NSPersistentHistoryTokenExpiredError unless used with fetchHistory(after:)" + ) } do { - let predicate = NSCompoundPredicate( - type: .and, subpredicates: [tokenGreaterThanLastHistoryTokenPredicate, notAuthor1Predicate]) - let request = try makeTransactionFetchRequest(predicate: predicate) + let request = NSPersistentHistoryChangeRequest.fetchHistory(after: newHistoryToken) let allTransactions = try viewContext2.historyTransactions(using: request) - XCTAssertTrue(allTransactions.isEmpty) + XCTAssertEqual(allTransactions.count, 0) + } + + do { + let predicate = NSPredicate(format: "%K > %@", #keyPath(NSPersistentHistoryTransaction.token), newHistoryToken) + let request = try NSPersistentHistoryChangeRequest.makeTransactionFetchRequest( + predicate: predicate, with: viewContext2) + let allTransactions = try viewContext2.historyTransactions(using: request) + XCTAssertEqual(allTransactions.count, 0) + } + + do { + // fetch transactions where author is "author1" + let request = try NSPersistentHistoryChangeRequest.makeTransactionFetchRequest( + predicate: NSPredicate(format: "%K = %@", #keyPath(NSPersistentHistoryTransaction.author), "author1"), + with: viewContext2) + let allTransactions = try viewContext2.historyTransactions(using: request) + XCTAssertEqual(allTransactions.count, 1) let result = try viewContext2.mergeTransactions(allTransactions) - XCTAssertNil(result) + XCTAssertNotNil(result) } do { - let predicate = tokenGreaterThanNewHistoryTokenPredicate - let request = try makeTransactionFetchRequest(predicate: predicate) + // fetch transactions where author is "author1" and contextName is "viewContext1" + let predicate = NSPredicate( + format: "%K = %@ AND %K = %@", #keyPath(NSPersistentHistoryTransaction.author), "author1", + #keyPath(NSPersistentHistoryTransaction.contextName), "viewContext1") + let request = try NSPersistentHistoryChangeRequest.makeTransactionFetchRequest( + predicate: predicate, with: viewContext2) let allTransactions = try viewContext2.historyTransactions(using: request) - XCTAssertTrue(allTransactions.isEmpty) - XCTAssertTrue(allTransactions.isEmpty) + XCTAssertEqual(allTransactions.count, 1) let result = try viewContext2.mergeTransactions(allTransactions) - XCTAssertNil(result) + XCTAssertNotNil(result) + let lastMergedToken = result!.0 + XCTAssertEqual(newHistoryToken, lastMergedToken) } do { + // fetch transactions where author is "author2" and contextName is "viewContext1" + let predicate = NSPredicate( + format: "%K = %@ AND %K = %@", #keyPath(NSPersistentHistoryTransaction.author), "author2", + #keyPath(NSPersistentHistoryTransaction.contextName), "viewContext1") + let request = try NSPersistentHistoryChangeRequest.makeTransactionFetchRequest( + predicate: predicate, with: viewContext2) + let allTransactions = try viewContext2.historyTransactions(using: request) + XCTAssertTrue(allTransactions.isEmpty) + } + + do { + // fetch transactions where author is "author1" and token is newHistoryToken let predicate = NSCompoundPredicate( - type: .and, subpredicates: [tokenGreaterThanLastHistoryTokenPredicate, notAuthor2Predicate]) - let request = try makeTransactionFetchRequest(predicate: predicate) + type: .and, + subpredicates: [ + NSPredicate(format: "%K = %@", #keyPath(NSPersistentHistoryTransaction.token), newHistoryToken), + NSPredicate(format: "%K = %@", #keyPath(NSPersistentHistoryTransaction.author), "author1"), + ]) + let request = try NSPersistentHistoryChangeRequest.makeTransactionFetchRequest( + predicate: predicate, with: viewContext2) let allTransactions = try viewContext2.historyTransactions(using: request) - XCTAssertFalse(allTransactions.isEmpty) + XCTAssertEqual(allTransactions.count, 1) let result = try viewContext2.mergeTransactions(allTransactions) XCTAssertNotNil(result) @@ -560,8 +693,7 @@ final class NSManagedObjectContextHistory_Tests: BaseTestCase { let changes = try viewContext1.historyChanges(using: historyFetchRequest) XCTAssertEqual(changes.count, 1) // Person - // ⚠️ fetching history changes with a changedObjectID in the predicate doesn't work on a context associated with a different container - // (even if the underlying store is the same) + // ⚠️ fetching history changes with a changedObjectID in the predicate doesn't work on a context associated with a different container (even if the underlying store is the same) // If we remove from the predicate the "changedObjectID" clause, we get both the Updated Person and the Deleted Car. // // At the moment querying for changes using changedObjectID seems useful only in bulk updates (many contexts for the same container) @@ -618,6 +750,16 @@ final class NSManagedObjectContextHistory_Tests: BaseTestCase { } extension NSPersistentHistoryChangeRequest { + fileprivate final class func makeTransactionFetchRequest(predicate: NSPredicate, with context: NSManagedObjectContext) + throws -> NSPersistentHistoryChangeRequest + { + let request = try XCTUnwrap(Self.makeTransactionFetchRequest(with: context)) + request.predicate = predicate + let transactionsRequest = NSPersistentHistoryChangeRequest.fetchHistory(withFetch: request) + transactionsRequest.resultType = .transactionsOnly + return transactionsRequest + } + /// Creates a NSFetchRequest for NSPersistentHistoryTransaction. /// - Note: context is used as hint to discover the Transaction entity. /// diff --git a/Tests/NSManagedObjectContextInvestigation_Tests.swift b/Tests/NSManagedObjectContextInvestigation_Tests.swift index eb6409ca..1b7ccbb4 100644 --- a/Tests/NSManagedObjectContextInvestigation_Tests.swift +++ b/Tests/NSManagedObjectContextInvestigation_Tests.swift @@ -6,7 +6,6 @@ import os.lock final class NSManagedObjectContextInvestigation_Tests: InMemoryTestCase { /// Investigation test: calling refreshAllObjects calls refreshObject:mergeChanges on all objects in the context. - @MainActor func test_InvestigationRefreshAllObjects() throws { let viewContext = container.viewContext let car1 = Car(context: viewContext) @@ -25,7 +24,6 @@ final class NSManagedObjectContextInvestigation_Tests: InMemoryTestCase { } /// Investigation test: KVO is fired whenever a property changes (even if the object is not saved in the context). - @MainActor func test_InvestigationKVO() throws { let context = container.viewContext let expectation = self.expectation(description: "\(#function)\(#line)") @@ -46,7 +44,8 @@ final class NSManagedObjectContextInvestigation_Tests: InMemoryTestCase { sportCar1.maker = "McLaren 2" try context.save() - waitForExpectations(timeout: 10) + + wait(for: [expectation], timeout: 5) token.invalidate() } @@ -56,10 +55,11 @@ final class NSManagedObjectContextInvestigation_Tests: InMemoryTestCase { do { let psc = NSPersistentStoreCoordinator(managedObjectModel: model1) let storeURL = URL.newDatabaseURL(withID: UUID()) - try psc.addPersistentStore(ofType: NSSQLiteStoreType, - configurationName: nil, - at: storeURL, - options: nil) + try psc.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: storeURL, + options: nil) let parentContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) parentContext.persistentStoreCoordinator = psc diff --git a/Tests/NSManagedObjectContextUtils_Tests.swift b/Tests/NSManagedObjectContextUtils_Tests.swift index 35a0719d..913b1cbf 100644 --- a/Tests/NSManagedObjectContextUtils_Tests.swift +++ b/Tests/NSManagedObjectContextUtils_Tests.swift @@ -130,7 +130,6 @@ final class NSManagedObjectContextUtils_Tests: InMemoryTestCase { XCTAssertFalse(cars.isEmpty) } - @MainActor func test_PerformAndWaitWithThrow() { let expectation1 = expectation(description: "\(#function)\(#line)") @@ -154,7 +153,7 @@ final class NSManagedObjectContextUtils_Tests: InMemoryTestCase { expectation1.fulfill() } - waitForExpectations(timeout: 2) + wait(for: [expectation1], timeout: 2) } func test_SaveIfNeededOrRollback() { diff --git a/Tests/NSManagedObjectUtils_Tests.swift b/Tests/NSManagedObjectUtils_Tests.swift index ac27124d..37e6d77c 100644 --- a/Tests/NSManagedObjectUtils_Tests.swift +++ b/Tests/NSManagedObjectUtils_Tests.swift @@ -179,10 +179,10 @@ final class NSManagedObjectUtils_Tests: InMemoryTestCase { func test_EvaluatePredicate() { let context = container.viewContext do { - let predicate = NSPredicate.true let car = Car(context: context) - XCTAssertTrue(car.evaluate(with: predicate)) + XCTAssertTrue(car.evaluate(with: .init(value: true))) } + do { let predicate = NSPredicate(format: "%K == %@", #keyPath(Car.maker), "FIAT") let car = Car(context: context) diff --git a/Tests/NSPredicateUtils_Tests.swift b/Tests/NSPredicateUtils_Tests.swift index 2ae69f03..b784a609 100644 --- a/Tests/NSPredicateUtils_Tests.swift +++ b/Tests/NSPredicateUtils_Tests.swift @@ -4,8 +4,8 @@ import XCTest final class NSPredicateUtils_Tests: XCTestCase { func test_AlwaysTrueAndFalsePredicates() { - XCTAssertEqual(NSPredicate.true.predicateFormat, "TRUEPREDICATE") - XCTAssertEqual(NSPredicate.false.predicateFormat, "FALSEPREDICATE") + XCTAssertEqual(NSPredicate.true().predicateFormat, "TRUEPREDICATE") + XCTAssertEqual(NSPredicate.false().predicateFormat, "FALSEPREDICATE") } func test_PredicateComposition() { diff --git a/Tests/Notifications/NotificationMerge_Tests.swift b/Tests/Notifications/NotificationMerge_Tests.swift index 426964b2..491295fa 100644 --- a/Tests/Notifications/NotificationMerge_Tests.swift +++ b/Tests/Notifications/NotificationMerge_Tests.swift @@ -58,7 +58,6 @@ final class NotificationMerge_Tests: InMemoryTestCase { XCTAssertEqual(viewContext.registeredObjects.count, 2) } - @MainActor func test_InvestigationMergeChanges() throws { // see: testInvesigationRegisteredObjects let expectation1 = expectation(description: "\(#function)\(#line)") @@ -131,12 +130,10 @@ final class NotificationMerge_Tests: InMemoryTestCase { try backgroundContext.save() } - self.waitForExpectations(timeout: 2) + wait(for: [expectation1], timeout: 5) cancellable.cancel() - } - @MainActor func test_Merge() throws { let viewContext = container.viewContext let backgroundContext = container.viewContext.newBackgroundContext(asChildContext: false) @@ -257,7 +254,7 @@ final class NotificationMerge_Tests: InMemoryTestCase { try! backgroundContext.save() // fires [0], [4] and then [1] } - waitForExpectations(timeout: 20) + wait(for: [expectation1, expectation2, expectation3, expectation4, expectation5, expectation6], timeout: 20) XCTAssertFalse(viewContext.hasChanges) try backgroundContext.performAndWait { _ in @@ -266,7 +263,6 @@ final class NotificationMerge_Tests: InMemoryTestCase { } } - @MainActor func test_AsyncMerge() throws { let context = container.viewContext let anotherContext = container.viewContext.newBackgroundContext(asChildContext: false) @@ -322,7 +318,8 @@ final class NotificationMerge_Tests: InMemoryTestCase { try anotherContext.save() } - waitForExpectations(timeout: 2) + wait(for: [expectation1, expectation2, expectation3], timeout: 5) + XCTAssertFalse(context.hasChanges) try anotherContext.performAndWait { _ in XCTAssertFalse(anotherContext.hasChanges) @@ -334,7 +331,6 @@ final class NotificationMerge_Tests: InMemoryTestCase { cancellable3.cancel() } - @MainActor func test_NSFetchedResultController() throws { let context = container.viewContext @@ -401,7 +397,8 @@ final class NotificationMerge_Tests: InMemoryTestCase { person3ObjectId = person3.objectID } - waitForExpectations(timeout: 5) + + wait(for: [expectation1], timeout: 5) XCTAssertEqual(delegate.updatedObjects.count + delegate.movedObjects.count, 1) XCTAssertEqual(delegate.insertedObjects.count, 1) // the FRC monitors only for Person objects @@ -415,7 +412,6 @@ final class NotificationMerge_Tests: InMemoryTestCase { cancellable1.cancel() } - @MainActor func test_NSFetchedResultControllerWithContextReset() throws { let context = container.viewContext @@ -486,7 +482,7 @@ final class NotificationMerge_Tests: InMemoryTestCase { context.reset() try context.save() // the command will do nothing, the FRC delegate is exepcted to have 0 changed objects - waitForExpectations(timeout: 5) + wait(for: [expectation1, expectation2], timeout: 5) XCTAssertEqual(delegate.updatedObjects.count, 0) XCTAssertEqual(delegate.deletedObjects.count, 0) diff --git a/Tests/Notifications/NotificationPayload_Tests.swift b/Tests/Notifications/NotificationPayload_Tests.swift index ae28dae7..1d6d51df 100644 --- a/Tests/Notifications/NotificationPayload_Tests.swift +++ b/Tests/Notifications/NotificationPayload_Tests.swift @@ -44,7 +44,6 @@ final class NotificationPayload_Tests: InMemoryTestCase { // MARK: - NSManagedObjectContextObjectsDidChange - @MainActor func test_ObserveInsertionsAndInvalidationsOnDidChangeNotification() { // Invalidation causes: // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/TroubleshootingCoreData.html @@ -97,11 +96,10 @@ final class NotificationPayload_Tests: InMemoryTestCase { context.reset() } - waitForExpectations(timeout: 2) + wait(for: [expectation, expectation2], timeout: 2) cancellable.cancel() } - @MainActor func test_ObserveInsertionsOnDidChangeNotificationOnBackgroundContext() { let expectation = self.expectation(description: "\(#function)\(#line)") let backgroundContext = container.newBackgroundContext() @@ -131,11 +129,10 @@ final class NotificationPayload_Tests: InMemoryTestCase { car.maker = "123!" backgroundContext.processPendingChanges() } - waitForExpectations(timeout: 5) + wait(for: [expectation], timeout: 5) cancellable.cancel() } - @MainActor func test_ObserveAsyncInsertionsOnDidChangeNotificationOnBackgroundContext() { let expectation = self.expectation(description: "\(#function)\(#line)") let backgroundContext = container.newBackgroundContext() @@ -165,11 +162,11 @@ final class NotificationPayload_Tests: InMemoryTestCase { car.numberPlate = "1" car.maker = "123!" } - waitForExpectations(timeout: 5) + + wait(for: [expectation], timeout: 5) cancellable.cancel() } - @MainActor func test_ObserveAsyncInsertionsOnDidChangeNotificationOnBackgroundContextAndDispatchQueue() { let expectation = self.expectation(description: "\(#function)\(#line)") let backgroundContext = container.newBackgroundContext() @@ -211,11 +208,10 @@ final class NotificationPayload_Tests: InMemoryTestCase { } } - waitForExpectations(timeout: 5) + wait(for: [expectation], timeout: 5) cancellable.cancel() } - @MainActor func test_ObserveInsertionsOnDidChangeNotificationOnPrivateContext() throws { let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) privateContext.persistentStoreCoordinator = container.persistentStoreCoordinator @@ -244,11 +240,11 @@ final class NotificationPayload_Tests: InMemoryTestCase { car.maker = "123!" privateContext.processPendingChanges() } - waitForExpectations(timeout: 5) + + wait(for: [expectation], timeout: 5) cancellable.cancel() } - @MainActor func test_ObserveRefreshedObjectsOnDidChangeNotification() throws { let context = container.viewContext context.fillWithSampleData() @@ -273,12 +269,11 @@ final class NotificationPayload_Tests: InMemoryTestCase { context.refreshAllObjects() - waitForExpectations(timeout: 5) + wait(for: [expectation], timeout: 2) cancellable.cancel() } // probably it's not a valid test - @MainActor func test_ObserveOnlyInsertionsOnDidChangeUsingBackgroundContextsAndAutomaticallyMergesChangesFromParent() throws { let backgroundContext1 = container.newBackgroundContext() let backgroundContext2 = container.newBackgroundContext() @@ -323,7 +318,7 @@ final class NotificationPayload_Tests: InMemoryTestCase { try backgroundContext1.save() } - waitForExpectations(timeout: 2) + wait(for: [expectation], timeout: 2) cancellable.cancel() } @@ -435,7 +430,6 @@ final class NotificationPayload_Tests: InMemoryTestCase { cancellable.cancel() } - @MainActor func test_ObserveRefreshesOnMaterializedObjects() throws { let backgroundContext1 = container.newBackgroundContext() let backgroundContext2 = container.newBackgroundContext() @@ -523,13 +517,12 @@ final class NotificationPayload_Tests: InMemoryTestCase { try backgroundContext2.save() } - waitForExpectations(timeout: 5) + wait(for: [expectation1], timeout: 5) cancellable.cancel() } // MARK: - NSManagedObjectContextWillSave and NSManagedObjectContextDidSave - @MainActor func test_ObserveInsertionsOnWillSaveNotification() throws { let context = container.viewContext let expectation = self.expectation(description: "\(#function)\(#line)") @@ -548,11 +541,10 @@ final class NotificationPayload_Tests: InMemoryTestCase { car.maker = "123!" try context.save() - waitForExpectations(timeout: 2) + wait(for: [expectation], timeout: 2) cancellable.cancel() } - @MainActor func test_ObserveInsertionsOnDidSaveNotification() throws { let context = container.viewContext var cancellables = [AnyCancellable]() @@ -596,14 +588,13 @@ final class NotificationPayload_Tests: InMemoryTestCase { car2.numberPlate = "2" try context.save() - waitForExpectations(timeout: 2) + wait(for: [expectation, expectation2], timeout: 2) for cancellable in cancellables { cancellable.cancel() } } - @MainActor func test_ObserveInsertionsUpdatesAndDeletesOnDidSaveNotification() throws { let context = container.viewContext var cancellables = [AnyCancellable]() @@ -668,14 +659,13 @@ final class NotificationPayload_Tests: InMemoryTestCase { car2.delete() try context.save() - waitForExpectations(timeout: 2) + wait(for: [expectation, expectation2], timeout: 2) for cancellable in cancellables { cancellable.cancel() } } - @MainActor func test_ObserveMultipleChangesUsingPersistentStoreCoordinatorWithChildAndParentContexts() throws { // Given let psc = NSPersistentStoreCoordinator(managedObjectModel: model1) @@ -826,7 +816,7 @@ final class NotificationPayload_Tests: InMemoryTestCase { try parentContext.save() // triggers the didSave event } - waitForExpectations(timeout: 10) + wait(for: [expectation, expectation2], timeout: 10) // cleaning stuff let store = psc.persistentStores.first! @@ -838,7 +828,6 @@ final class NotificationPayload_Tests: InMemoryTestCase { // MARK: - Entity Observer Example - @MainActor func test_ObserveInsertedOnDidChangeEventForSpecificEntities() { let context = container.viewContext let expectation1 = expectation(description: "\(#function)\(#line)") @@ -897,13 +886,12 @@ final class NotificationPayload_Tests: InMemoryTestCase { person1.firstName = "Edythe" person1.lastName = "Moreton" - waitForExpectations(timeout: 2) + wait(for: [expectation1], timeout: 2) cancellable.cancel() } // MARK: - NSPersistentStoreRemoteChange - @MainActor func test_InvestigationPersistentStoreRemoteChangeAndSave() throws { // Cross coordinators change notifications: @@ -952,12 +940,11 @@ final class NotificationPayload_Tests: InMemoryTestCase { car.model = "Panda" try viewContext1.save() - waitForExpectations(timeout: 5, handler: nil) + wait(for: [expectation1, expectation2], timeout: 5) cancellable1.cancel() cancellable2.cancel() } - @MainActor func test_InvestigationPersistentStoreRemoteChangeAndBatchOperations() throws { // Cross coordinators change notifications: // This notification notifies when history has been made even when batch operations are done. @@ -973,7 +960,8 @@ final class NotificationPayload_Tests: InMemoryTestCase { let expectation1 = expectation(description: "NSPersistentStoreRemoteChange Notification sent by container1") let cancellable1 = NotificationCenter.default.publisher( - for: .NSPersistentStoreRemoteChange, object: container1.persistentStoreCoordinator + for: .NSPersistentStoreRemoteChange, + object: container1.persistentStoreCoordinator ) .map { PersistentStoreRemoteChange(notification: $0) } .sink { payload in @@ -988,7 +976,8 @@ final class NotificationPayload_Tests: InMemoryTestCase { let expectation2 = expectation(description: "NSPersistentStoreRemoteChange Notification sent by container2") let cancellable2 = NotificationCenter.default.publisher( - for: .NSPersistentStoreRemoteChange, object: container2.persistentStoreCoordinator + for: .NSPersistentStoreRemoteChange, + object: container2.persistentStoreCoordinator ) .map { PersistentStoreRemoteChange(notification: $0) } .sink { payload in @@ -1010,14 +999,14 @@ final class NotificationPayload_Tests: InMemoryTestCase { let result = try Car.batchInsert(using: viewContext1, resultType: .count, objects: [object]) XCTAssertEqual(result.count!, 1) - waitForExpectations(timeout: 5, handler: nil) + wait(for: [expectation1, expectation2]) cancellable1.cancel() cancellable2.cancel() } } final class NotificationPayloadOnDiskTests: OnDiskTestCase { - @MainActor + func test_ObserveInsertionsOnDidSaveNotification() throws { let context = container.viewContext try context.setQueryGenerationFrom(.current) @@ -1066,14 +1055,13 @@ final class NotificationPayloadOnDiskTests: OnDiskTestCase { //print(car.objectID, car2.objectID) try context.save() - waitForExpectations(timeout: 2) + wait(for: [expectation, expectation2]) for cancellable in cancellables { cancellable.cancel() } } - @MainActor func test_InvestigationInsertionsInChildContextOnDidSaveNotification() throws { // the scope of this test is to verify wheter or not a NSManagedObjectContextDidSaveObjectIDs notification // fired in a child context will have insertedObjectIDs with temporary IDs (expected) @@ -1108,7 +1096,7 @@ final class NotificationPayloadOnDiskTests: OnDiskTestCase { car2.numberPlate = "2" try childViewContext.save() - waitForExpectations(timeout: 5) + wait(for: [expectation2]) for cancellable in cancellables { cancellable.cancel() diff --git a/Tests/ProgrammaticMigration_Tests.swift b/Tests/ProgrammaticMigration_Tests.swift index 525b942e..3ca77f62 100644 --- a/Tests/ProgrammaticMigration_Tests.swift +++ b/Tests/ProgrammaticMigration_Tests.swift @@ -254,7 +254,7 @@ final class ProgrammaticMigration_Tests: XCTestCase { func test_MigrationFromV1toV3() throws { let url = URL.newDatabaseURL(withID: UUID()) - + // the migration works fine even if NSMigratePersistentStoresAutomaticallyOption is set to true, // but it should be false let options = [ @@ -285,7 +285,7 @@ final class ProgrammaticMigration_Tests: XCTestCase { }) // Migration - + XCTAssertTrue(SampleModel2.SampleModelVersion2.version1.isLightWeightMigrationPossibleToNextModelVersion()) XCTAssertTrue(SampleModel2.SampleModelVersion2.version2.isLightWeightMigrationPossibleToNextModelVersion()) XCTAssertFalse(SampleModel2.SampleModelVersion2.version3.isLightWeightMigrationPossibleToNextModelVersion()) diff --git a/Tests/ProgrammaticallyDefinedModel_Tests.swift b/Tests/ProgrammaticallyDefinedModel_Tests.swift index a422d28d..ee80c0be 100644 --- a/Tests/ProgrammaticallyDefinedModel_Tests.swift +++ b/Tests/ProgrammaticallyDefinedModel_Tests.swift @@ -2,6 +2,7 @@ import CoreData import XCTest + @testable import CoreDataPlus // MARK: - V1 @@ -12,7 +13,7 @@ final class ProgrammaticallyDefinedModel_Tests: OnDiskWithProgrammaticallyModelT context.fillWithSampleData2() try context.save() context.reset() - + let books = try Book.fetchObjects(in: context) XCTAssertEqual(books.count, 52) @@ -77,7 +78,7 @@ final class ProgrammaticallyDefinedModelV2Tests: XCTestCase { try context.save() context.reset() - + let fetchedAuthor = try AuthorV2.fetchObjects(in: context) { $0.predicate = NSPredicate(format: "%K == %@", #keyPath(Author.alias), "Alessandro") }.first diff --git a/Tests/Resources/SampleModel/Entities/Car.swift b/Tests/Resources/SampleModel/Entities/Car.swift index b9111a5d..903719c8 100644 --- a/Tests/Resources/SampleModel/Entities/Car.swift +++ b/Tests/Resources/SampleModel/Entities/Car.swift @@ -43,7 +43,7 @@ public class Car: BaseEntity { @NSManaged public var owner: Person? @NSManaged public var currentDrivingSpeed: Int // transient property @NSManaged public var color: Color? // transformable property - + // Additional notes // - color: in the "Data Model Inspector", the "class name" field is set to Color; this is an optional requirement but quite useful because CoreData will validate the type when assigned to the color property. } @@ -63,6 +63,6 @@ final public class LuxuryCar: SportCar { // Some tests fallback to the string representation of LuxuryCar (fallback of entityName instead of entity().name because // entity() is not accessible. If that happens in production, it's better to override the property like so: //public override class var entityName: String { "LuxuryCar" } - + @NSManaged public var isLimitedEdition: Bool } diff --git a/Tests/Resources/SampleModel/MappingModels/V2to3MakerPolicy.swift b/Tests/Resources/SampleModel/MappingModels/V2to3MakerPolicy.swift index 8378d3bb..ff6a0fa3 100644 --- a/Tests/Resources/SampleModel/MappingModels/V2to3MakerPolicy.swift +++ b/Tests/Resources/SampleModel/MappingModels/V2to3MakerPolicy.swift @@ -3,19 +3,20 @@ import CoreData final class V2to3MakerPolicy: NSEntityMigrationPolicy { - override func createDestinationInstances(forSource sInstance: NSManagedObject, - in mapping: NSEntityMapping, - manager: NSMigrationManager + override func createDestinationInstances( + forSource sInstance: NSManagedObject, + in mapping: NSEntityMapping, + manager: NSMigrationManager ) throws { try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager) - guard + guard let makerName = sInstance.value(forKey: makerKey) as? String else { return } - guard + guard let car = manager.destinationInstances(forEntityMappingName: mapping.name, sourceInstances: [sInstance]).first else { fatalError("must return car") diff --git a/Tests/Resources/SampleModel2/NSManagedObjectContext+SampleModel2.swift b/Tests/Resources/SampleModel2/NSManagedObjectContext+SampleModel2.swift index f9c46732..2393e088 100644 --- a/Tests/Resources/SampleModel2/NSManagedObjectContext+SampleModel2.swift +++ b/Tests/Resources/SampleModel2/NSManagedObjectContext+SampleModel2.swift @@ -5,14 +5,14 @@ import Foundation extension NSManagedObjectContext { func fillWithSampleData2() { - fillWithAuthor1() // Author with 2 books and 1 Graphic Novel - fillWithAuthor2() // Author with 49 books + fillWithAuthor1() // Author with 2 books and 1 Graphic Novel + fillWithAuthor2() // Author with 49 books } - + func fillWithSampleData2UsingModelV2() { fillWithAuthor1UsingModelV2() } - + func fillWithSampleData2UsingModelV3() { fillWithAuthor1UsingModelV3() } @@ -133,7 +133,7 @@ extension NSManagedObjectContext { } } } - + func fillWithAuthor1UsingModelV2() { let author = AuthorV2(context: self) author.alias = "Alessandro" diff --git a/Tests/Resources/SampleModel2/SampleModel2+V1.swift b/Tests/Resources/SampleModel2/SampleModel2+V1.swift index e9804405..ef681066 100644 --- a/Tests/Resources/SampleModel2/SampleModel2+V1.swift +++ b/Tests/Resources/SampleModel2/SampleModel2+V1.swift @@ -105,7 +105,7 @@ extension SampleModel2.V1 { managedObjectModel.entities = entities managedObjectModel.setEntities(entities, forConfigurationName: Configurations.one) SampleModel2.modelCache.withLock { $0["V1"] = managedObjectModel } - + return managedObjectModel } diff --git a/Tests/Resources/SampleModel2/SampleModel2+V2.swift b/Tests/Resources/SampleModel2/SampleModel2+V2.swift index e26c4e60..669a51ae 100644 --- a/Tests/Resources/SampleModel2/SampleModel2+V2.swift +++ b/Tests/Resources/SampleModel2/SampleModel2+V2.swift @@ -114,7 +114,7 @@ extension V2 { static func makeWriterEntity() -> NSEntityDescription { let entity = NSEntityDescription(for: WriterV2.self, withName: String(describing: Writer.self)) entity.isAbstract = true - + let age = NSAttributeDescription.int16(name: #keyPath(WriterV2.age)) age.isOptional = false diff --git a/Tests/Resources/SampleModel3/SampleModel3.swift b/Tests/Resources/SampleModel3/SampleModel3.swift index 5b6ae722..38b8d78d 100644 --- a/Tests/Resources/SampleModel3/SampleModel3.swift +++ b/Tests/Resources/SampleModel3/SampleModel3.swift @@ -6,7 +6,7 @@ import CoreData @objc(User) public class User: NSManagedObject { - @NSManaged public var name: String! // unique + @NSManaged public var name: String! // unique @NSManaged public var petName: String? } diff --git a/Tests/Resources/SampleModel3/SampleModelVersion3.swift b/Tests/Resources/SampleModel3/SampleModelVersion3.swift index 3e5ee5b8..e53743d3 100644 --- a/Tests/Resources/SampleModel3/SampleModelVersion3.swift +++ b/Tests/Resources/SampleModel3/SampleModelVersion3.swift @@ -21,7 +21,7 @@ extension SampleModelVersion3: ModelVersion { public static var allVersions: [SampleModelVersion3] { SampleModelVersion3.allCases } public static var currentVersion: SampleModelVersion3 { .version1 } public var modelName: String { "SampleModel3" } - + public var next: SampleModelVersion3? { switch self { case .version1: return .version2 @@ -29,10 +29,10 @@ extension SampleModelVersion3: ModelVersion { default: return nil } } - + public var versionName: String { rawValue } public var modelBundle: Bundle { Bundle.tests } - + public func managedObjectModel() -> NSManagedObjectModel { switch self { case .version1: @@ -46,23 +46,27 @@ extension SampleModelVersion3: ModelVersion { } extension SampleModelVersion3 { - @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + @available( + iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, + macCatalystApplicationExtension 17.0, * + ) public func migrationStageToNextModelVersion() -> NSMigrationStage? { switch self { - // There can't be stages with the same versionCheckSum (you can't have a NSLightweightMigrationStage and a - // NSCustomMigrationStage referencing the same target versionCheckSum) + // There can't be stages with the same versionCheckSum (you can't have a NSLightweightMigrationStage and a + // NSCustomMigrationStage referencing the same target versionCheckSum) case .version1: - let stage = NSCustomMigrationStage(migratingFrom: self.managedObjectModelReference(), // v1 - to: self.next!.managedObjectModelReference()) // v2 + let stage = NSCustomMigrationStage( + migratingFrom: self.managedObjectModelReference(), // v1 + to: self.next!.managedObjectModelReference()) // v2 stage.label = "V1 to V2 (Add Pet entity and denormalize User entity)" - + stage.willMigrateHandler = { migrationManager, stage in // in willMigrateHandler Pet is not yet defined } - + stage.didMigrateHandler = { migrationManager, stage in guard let container = migrationManager.container else { return } - + let context = container.newBackgroundContext() try context.performAndWait { let fetchRequest = NSFetchRequest(entityName: "User") @@ -77,10 +81,10 @@ extension SampleModelVersion3 { try context.save() } } - + return stage case .version2: - let stage = NSLightweightMigrationStage([self.next!.versionChecksum]) // v3 + let stage = NSLightweightMigrationStage([self.next!.versionChecksum]) // v3 stage.label = "V2 to V3 (remove petName from User entity)" return stage default: @@ -95,5 +99,3 @@ extension SampleModelVersion3 { // it tries to use that instead. // // SampleModel2 is defined programmatically and I didn't find a way to make it work - - diff --git a/Tests/StagedMigrations_Tests.swift b/Tests/StagedMigrations_Tests.swift index 585e6940..689d8d8c 100644 --- a/Tests/StagedMigrations_Tests.swift +++ b/Tests/StagedMigrations_Tests.swift @@ -2,37 +2,42 @@ import CoreData import XCTest + @testable import CoreDataPlus -@available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) +@available( + iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, + macCatalystApplicationExtension 17.0, * +) final class StagedMigrations_Tests: XCTestCase { - + func test_MigrationFromV1ToV2() throws { let sourceURL = try Self.createSQLiteSample3ForV1() let steps = SampleModelVersion3.version1.stagedMigrationSteps(to: .version2) XCTAssertEqual(steps.count, 1) - + XCTAssertFalse(try isMigrationNecessary(for: sourceURL, to: SampleModelVersion3.version1)) XCTAssertTrue(try isMigrationNecessary(for: sourceURL, to: SampleModelVersion3.version2)) - + let version = try SampleModelVersion3(persistentStoreURL: sourceURL) XCTAssertTrue(version == .version1) - + let stages = steps.compactMap { $0.stage } let migrator = NSStagedMigrationManager(stages) - let container = NSPersistentContainer(name: "SampleModel3", - managedObjectModel: SampleModelVersion3.version2.managedObjectModel()) - + let container = NSPersistentContainer( + name: "SampleModel3", + managedObjectModel: SampleModelVersion3.version2.managedObjectModel()) + let storeDescription = try XCTUnwrap(container.persistentStoreDescriptions.first) storeDescription.url = sourceURL storeDescription.setOption(migrator, forKey: NSPersistentStoreStagedMigrationManagerOptionKey) - + container.loadPersistentStores { storeDescription, error in if let error = error { XCTFail(error.localizedDescription) } } - + let migratedContext = container.viewContext let users = try migratedContext.fetch(NSFetchRequest(entityName: "User")) for user in users { @@ -42,32 +47,33 @@ final class StagedMigrations_Tests: XCTestCase { XCTAssertNotNil(petName) XCTAssertEqual(pet?.value(forKey: "name") as? String, petName) } - + migratedContext._fix_sqlite_warning_when_destroying_a_store() try NSPersistentStoreCoordinator.destroyStore(at: sourceURL) } - + func test_MigrationFromV1ToV3() throws { let sourceURL = try Self.createSQLiteSample3ForV1() let steps = SampleModelVersion3.version1.stagedMigrationSteps(to: .version3) XCTAssertEqual(steps.count, 2) - + XCTAssertFalse(try isMigrationNecessary(for: sourceURL, to: SampleModelVersion3.version1)) XCTAssertTrue(try isMigrationNecessary(for: sourceURL, to: SampleModelVersion3.version2)) XCTAssertTrue(try isMigrationNecessary(for: sourceURL, to: SampleModelVersion3.version3)) - + XCTAssertTrue(SampleModelVersion3.version1.isLightWeightMigrationPossibleToNextModelVersion()) XCTAssertTrue(SampleModelVersion3.version2.isLightWeightMigrationPossibleToNextModelVersion()) - XCTAssertFalse(SampleModelVersion3.version3.isLightWeightMigrationPossibleToNextModelVersion()) // no V4 - + XCTAssertFalse(SampleModelVersion3.version3.isLightWeightMigrationPossibleToNextModelVersion()) // no V4 + let version = try SampleModelVersion3(persistentStoreURL: sourceURL) XCTAssertTrue(version == .version1) - + let stages = steps.compactMap { $0.stage } let migrator = NSStagedMigrationManager(stages) - let container = NSPersistentContainer(name: "SampleModel3", - managedObjectModel: SampleModelVersion3.version3.managedObjectModel()) - + let container = NSPersistentContainer( + name: "SampleModel3", + managedObjectModel: SampleModelVersion3.version3.managedObjectModel()) + let storeDescription = try XCTUnwrap(container.persistentStoreDescriptions.first) storeDescription.url = sourceURL storeDescription.setOption(migrator, forKey: NSPersistentStoreStagedMigrationManagerOptionKey) @@ -81,9 +87,9 @@ final class StagedMigrations_Tests: XCTestCase { expectation.fulfill() } } - + wait(for: [expectation]) - + let migratedContext = container.viewContext let users = try migratedContext.fetch(NSFetchRequest(entityName: "User")) for user in users { @@ -91,48 +97,51 @@ final class StagedMigrations_Tests: XCTestCase { XCTAssertNotNil(pet, "Pet should not be nil after migration to V3") XCTAssertNotNil(pet?.value(forKey: "name"), "Pet name is not optional after migration to V3") } - + migratedContext._fix_sqlite_warning_when_destroying_a_store() try NSPersistentStoreCoordinator.destroyStore(at: sourceURL) } - -// func test_generateSample() throws { -// let container = NSPersistentContainer(name: "SampleModel3", -// managedObjectModel: SampleModelVersion3.version1.managedObjectModel()) -// -// let url = URL.newDatabaseURL(withID: UUID()) -// -// let description = NSPersistentStoreDescription() -// description.url = url -// container.persistentStoreDescriptions = [description] -// -// container.loadPersistentStores { description, error in -// XCTAssertNil(error) -// } -// -// let context = container.viewContext -// -// for x in 0..<3 { -// for i in 0..<4 { -// let user = User(context: context) -// user.name = "User_\(x)" -// user.petName = "Dog_\(x)_\(i)" -// } -// } -// -// try context.save() -// -// print(url.path()) -// } - + + // func test_generateSample() throws { + // let container = NSPersistentContainer(name: "SampleModel3", + // managedObjectModel: SampleModelVersion3.version1.managedObjectModel()) + // + // let url = URL.newDatabaseURL(withID: UUID()) + // + // let description = NSPersistentStoreDescription() + // description.url = url + // container.persistentStoreDescriptions = [description] + // + // container.loadPersistentStores { description, error in + // XCTAssertNil(error) + // } + // + // let context = container.viewContext + // + // for x in 0..<3 { + // for i in 0..<4 { + // let user = User(context: context) + // user.name = "User_\(x)" + // user.petName = "Dog_\(x)_\(i)" + // } + // } + // + // try context.save() + // + // print(url.path()) + // } + } -@available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) +@available( + iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, + macCatalystApplicationExtension 17.0, * +) extension StagedMigrations_Tests { static func createSQLiteSample2ForV1() throws -> URL { let bundle = Bundle.tests let _sourceURL = try XCTUnwrap(bundle.url(forResource: "SampleModel2_V1", withExtension: "sqlite")) - + // Being the test run multiple times, we create an unique copy for every test let uuid = UUID().uuidString let sourceURL = bundle.bundleURL.appendingPathComponent("SampleModel2_V1_copy-\(uuid).sqlite") @@ -140,11 +149,11 @@ extension StagedMigrations_Tests { XCTAssertTrue(FileManager.default.fileExists(atPath: sourceURL.path)) return sourceURL } - + static func createSQLiteSample2ForV2() throws -> URL { let bundle = Bundle.tests let _sourceURL = try XCTUnwrap(bundle.url(forResource: "SampleModel2_V2", withExtension: "sqlite")) - + // Being the test run multiple times, we create an unique copy for every test let uuid = UUID().uuidString let sourceURL = bundle.bundleURL.appendingPathComponent("SampleModel2_V2_copy-\(uuid).sqlite") @@ -152,11 +161,11 @@ extension StagedMigrations_Tests { XCTAssertTrue(FileManager.default.fileExists(atPath: sourceURL.path)) return sourceURL } - + static func createSQLiteSample3ForV1() throws -> URL { let bundle = Bundle.tests let _sourceURL = try XCTUnwrap(bundle.url(forResource: "SampleModel3_V1", withExtension: "sqlite")) - + // Being the test run multiple times, we create an unique copy for every test let uuid = UUID().uuidString let sourceURL = bundle.bundleURL.appendingPathComponent("SampleModel3_V1_copy-\(uuid).sqlite") diff --git a/Tests/TestPlans/CoreDataPlus iOS.xctestplan b/Tests/TestPlans/CoreDataPlus iOS.xctestplan deleted file mode 100644 index a6779b55..00000000 --- a/Tests/TestPlans/CoreDataPlus iOS.xctestplan +++ /dev/null @@ -1,150 +0,0 @@ -{ - "configurations" : [ - { - "id" : "D0CA40FF-44F6-4EF6-B693-27878EB5C4B4", - "name" : "Memory Checking", - "options" : { - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - }, - { - "argument" : "com.apple.CoreData.MigrationDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.ConcurrencyDebug 1" - }, - { - "argument" : "-com.apple.CoreData.SQLDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 1", - "enabled" : false - }, - { - "argument" : "zombieObjectsEnabled" - } - ], - "nsZombieEnabled" : true - } - }, - { - "id" : "58D73E0F-4350-4E4D-B3C6-D326B53A2FE5", - "name" : "Concurrency", - "options" : { - "addressSanitizer" : { - "enabled" : false - }, - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - }, - { - "argument" : "com.apple.CoreData.MigrationDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.ConcurrencyDebug 1" - }, - { - "argument" : "-com.apple.CoreData.SQLDebug 3", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 1", - "enabled" : false - } - ], - "threadSanitizerEnabled" : true, - "undefinedBehaviorSanitizerEnabled" : true - } - }, - { - "id" : "992DEAB2-7713-4D8B-9501-2AE8248FA08F", - "name" : "Concurrency without ConcurrencyDebug flag", - "options" : { - "addressSanitizer" : { - "enabled" : false - }, - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - }, - { - "argument" : "com.apple.CoreData.MigrationDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.SQLDebug 3", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 1", - "enabled" : false - } - ], - "threadSanitizerEnabled" : true, - "undefinedBehaviorSanitizerEnabled" : true - } - } - ], - "defaultOptions" : { - "addressSanitizer" : { - "detectStackUseAfterReturn" : true, - "enabled" : true - }, - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - }, - { - "argument" : "com.apple.CoreData.MigrationDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.SQLDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 1", - "enabled" : false - } - ], - "environmentVariableEntries" : [ - { - "key" : "XCODE_TESTS", - "value" : "1" - }, - { - "key" : "SQLITE_ENABLE_THREAD_ASSERTIONS", - "value" : "1" - }, - { - "key" : "SQLITE_AUTO_TRACE", - "value" : "0" - }, - { - "key" : "SQLITE_ENABLE_FILE_ASSERTIONS", - "value" : "1" - } - ], - "targetForVariableExpansion" : { - "containerPath" : "container:CoreDataPlus.xcodeproj", - "identifier" : "23B580691F94FEDF00A365C0", - "name" : "CoreDataPlus iOS" - }, - "testTimeoutsEnabled" : true - }, - "testTargets" : [ - { - "target" : { - "containerPath" : "container:CoreDataPlus.xcodeproj", - "identifier" : "23EFDE8C1F95FE730038BE75", - "name" : "CoreDataPlus Tests iOS" - } - } - ], - "version" : 1 -} diff --git a/Tests/TestPlans/CoreDataPlus macOS.xctestplan b/Tests/TestPlans/CoreDataPlus macOS.xctestplan deleted file mode 100644 index 61e6729a..00000000 --- a/Tests/TestPlans/CoreDataPlus macOS.xctestplan +++ /dev/null @@ -1,154 +0,0 @@ -{ - "configurations" : [ - { - "id" : "0E9BED93-2A4E-4895-AB3C-92500CDB9EDB", - "name" : "Memory Checking", - "options" : { - "addressSanitizer" : { - "enabled" : true - }, - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.ConcurrencyDebug 1" - }, - { - "argument" : "-com.apple.CoreData.MigrationDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.SQLDebug 3", - "enabled" : false - }, - { - "argument" : "zombieObjectsEnabled" - } - ], - "nsZombieEnabled" : true - } - }, - { - "id" : "7275C325-C6F9-40EB-8CFF-698B3BDF4762", - "name" : "Concurrency", - "options" : { - "addressSanitizer" : { - "enabled" : false - }, - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.ConcurrencyDebug 1" - }, - { - "argument" : "-com.apple.CoreData.MigrationDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - }, - { - "argument" : "-com.apple.CoreData.SQLDebug 3", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 1", - "enabled" : false - } - ], - "threadSanitizerEnabled" : true, - "undefinedBehaviorSanitizerEnabled" : true - } - }, - { - "id" : "BCDCF027-BD58-4FED-99A3-EC98A382117F", - "name" : "Concurrency without ConcurrencyDebug flag", - "options" : { - "addressSanitizer" : { - "enabled" : false - }, - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - }, - { - "argument" : "-com.apple.CoreData.MigrationDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.SQLDebug 3", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 1", - "enabled" : false - } - ], - "threadSanitizerEnabled" : true - } - } - ], - "defaultOptions" : { - "addressSanitizer" : { - "detectStackUseAfterReturn" : true, - "enabled" : true - }, - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - }, - { - "argument" : "-com.apple.CoreData.MigrationDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.SQLDebug 3", - "enabled" : false - } - ], - "environmentVariableEntries" : [ - { - "key" : "XCODE_TESTS", - "value" : "1" - }, - { - "key" : "SQLITE_ENABLE_THREAD_ASSERTIONS", - "value" : "1" - }, - { - "key" : "SQLITE_AUTO_TRACE", - "value" : "0" - }, - { - "key" : "SQLITE_ENABLE_FILE_ASSERTIONS", - "value" : "1" - } - ], - "targetForVariableExpansion" : { - "containerPath" : "container:CoreDataPlus.xcodeproj", - "identifier" : "2363509C1F95EC5600B3A16A", - "name" : "CoreDataPlus macOS" - }, - "testTimeoutsEnabled" : true, - "undefinedBehaviorSanitizerEnabled" : true - }, - "testTargets" : [ - { - "parallelizable" : true, - "target" : { - "containerPath" : "container:CoreDataPlus.xcodeproj", - "identifier" : "23EFDEAA1F95FEB40038BE75", - "name" : "CoreDataPlus Tests macOS" - } - } - ], - "version" : 1 -} diff --git a/Tests/TestPlans/CoreDataPlus tvOS.xctestplan b/Tests/TestPlans/CoreDataPlus tvOS.xctestplan deleted file mode 100644 index 42e39fa6..00000000 --- a/Tests/TestPlans/CoreDataPlus tvOS.xctestplan +++ /dev/null @@ -1,123 +0,0 @@ -{ - "configurations" : [ - { - "id" : "AA9B294E-98E8-4854-AD82-B43EFC2DA232", - "name" : "Memory Checking", - "options" : { - "addressSanitizer" : { - "enabled" : true - }, - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.SQLDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.ConcurrencyDebug 1" - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - }, - { - "argument" : "zombieObjectsEnabled" - } - ], - "nsZombieEnabled" : true - } - }, - { - "id" : "485EA5DF-DA8A-4E8B-B6E1-97654C637F2F", - "name" : "Concurrency", - "options" : { - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.ConcurrencyDebug 1" - }, - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - }, - { - "argument" : "-com.apple.CoreData.SQLDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 1", - "enabled" : false - } - ], - "threadSanitizerEnabled" : true, - "undefinedBehaviorSanitizerEnabled" : true - } - }, - { - "id" : "9E39D84D-739A-411B-B183-741C5F76202E", - "name" : "Concurrency without ConcurrencyDebug flag", - "options" : { - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - }, - { - "argument" : "-com.apple.CoreData.SQLDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 1", - "enabled" : false - } - ], - "threadSanitizerEnabled" : true, - "undefinedBehaviorSanitizerEnabled" : true - } - } - ], - "defaultOptions" : { - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.SQLDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - } - ], - "environmentVariableEntries" : [ - { - "key" : "XCODE_TESTS", - "value" : "1" - }, - { - "key" : "SQLITE_ENABLE_THREAD_ASSERTIONS", - "value" : "1" - }, - { - "key" : "SQLITE_AUTO_TRACE", - "value" : "0" - }, - { - "key" : "SQLITE_ENABLE_FILE_ASSERTIONS", - "value" : "1" - } - ], - "targetForVariableExpansion" : { - "containerPath" : "container:CoreDataPlus.xcodeproj", - "identifier" : "236350801F95EC3000B3A16A", - "name" : "CoreDataPlus tvOS" - }, - "testTimeoutsEnabled" : true - }, - "testTargets" : [ - { - "target" : { - "containerPath" : "container:CoreDataPlus.xcodeproj", - "identifier" : "23EFDE9B1F95FE990038BE75", - "name" : "CoreDataPlus Tests tvOS" - } - } - ], - "version" : 1 -} diff --git a/Tests/TestPlans/CoreDataPlus watchOS.xctestplan b/Tests/TestPlans/CoreDataPlus watchOS.xctestplan deleted file mode 100644 index e81b1ff4..00000000 --- a/Tests/TestPlans/CoreDataPlus watchOS.xctestplan +++ /dev/null @@ -1,150 +0,0 @@ -{ - "configurations" : [ - { - "id" : "D0CA40FF-44F6-4EF6-B693-27878EB5C4B4", - "name" : "Memory Checking", - "options" : { - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - }, - { - "argument" : "com.apple.CoreData.MigrationDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.ConcurrencyDebug 1" - }, - { - "argument" : "-com.apple.CoreData.SQLDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 1", - "enabled" : false - }, - { - "argument" : "zombieObjectsEnabled" - } - ], - "nsZombieEnabled" : true - } - }, - { - "id" : "58D73E0F-4350-4E4D-B3C6-D326B53A2FE5", - "name" : "Concurrency", - "options" : { - "addressSanitizer" : { - "enabled" : false - }, - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - }, - { - "argument" : "com.apple.CoreData.MigrationDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.ConcurrencyDebug 1" - }, - { - "argument" : "-com.apple.CoreData.SQLDebug 3", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 1", - "enabled" : false - } - ], - "threadSanitizerEnabled" : true, - "undefinedBehaviorSanitizerEnabled" : true - } - }, - { - "id" : "992DEAB2-7713-4D8B-9501-2AE8248FA08F", - "name" : "Concurrency without ConcurrencyDebug flag", - "options" : { - "addressSanitizer" : { - "enabled" : false - }, - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - }, - { - "argument" : "com.apple.CoreData.MigrationDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.SQLDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 1", - "enabled" : false - } - ], - "threadSanitizerEnabled" : true, - "undefinedBehaviorSanitizerEnabled" : true - } - } - ], - "defaultOptions" : { - "addressSanitizer" : { - "detectStackUseAfterReturn" : true, - "enabled" : true - }, - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.ThreadingDebug 3" - }, - { - "argument" : "com.apple.CoreData.MigrationDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.SQLDebug 1", - "enabled" : false - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 1", - "enabled" : false - } - ], - "environmentVariableEntries" : [ - { - "key" : "XCODE_TESTS", - "value" : "1" - }, - { - "key" : "SQLITE_ENABLE_THREAD_ASSERTIONS", - "value" : "1" - }, - { - "key" : "SQLITE_AUTO_TRACE", - "value" : "0" - }, - { - "key" : "SQLITE_ENABLE_FILE_ASSERTIONS", - "value" : "1" - } - ], - "targetForVariableExpansion" : { - "containerPath" : "container:CoreDataPlus.xcodeproj", - "identifier" : "23B580691F94FEDF00A365C0", - "name" : "CoreDataPlus iOS" - }, - "testTimeoutsEnabled" : true - }, - "testTargets" : [ - { - "target" : { - "containerPath" : "container:CoreDataPlus.xcodeproj", - "identifier" : "06968ACD26454AFF00088D76", - "name" : "CoreDataPlus Tests watchOS" - } - } - ], - "version" : 1 -} diff --git a/Tests/TestPlans/CoreDataPlus.xctestplan b/Tests/TestPlans/CoreDataPlus.xctestplan index 675df886..a218baef 100644 --- a/Tests/TestPlans/CoreDataPlus.xctestplan +++ b/Tests/TestPlans/CoreDataPlus.xctestplan @@ -13,7 +13,8 @@ "enabled" : false }, { - "argument" : "-com.apple.CoreData.ConcurrencyDebug 1" + "argument" : "-com.apple.CoreData.ConcurrencyDebug 1", + "enabled" : false }, { "argument" : "-com.apple.CoreData.SQLDebug 1", @@ -99,6 +100,10 @@ { "argument" : "-com.apple.CoreData.ThreadingDebug 3" }, + { + "argument" : "-com. apple.CoreData.CloudKitDebug 1", + "enabled" : false + }, { "argument" : "com.apple.CoreData.MigrationDebug 1", "enabled" : false diff --git a/Tests/Utils/InMemoryTestCase.swift b/Tests/Utils/InMemoryTestCase.swift index c307be9b..a09d434e 100644 --- a/Tests/Utils/InMemoryTestCase.swift +++ b/Tests/Utils/InMemoryTestCase.swift @@ -23,7 +23,7 @@ class InMemoryTestCase: BaseTestCase { // MARK: - In Memory NSPersistentContainer -final class InMemoryPersistentContainer: NSPersistentContainer { +final class InMemoryPersistentContainer: NSPersistentContainer, @unchecked Sendable { static func makeNew(named: String? = nil) -> InMemoryPersistentContainer { // WWDC https://developer.apple.com/videos/play/wwdc2019/230 // A simple in memory store can't be shared between coordinators (remote change notifications won't work) diff --git a/Tests/Utils/OnDiskTestCase.swift b/Tests/Utils/OnDiskTestCase.swift index b2ba5471..a38c9de3 100644 --- a/Tests/Utils/OnDiskTestCase.swift +++ b/Tests/Utils/OnDiskTestCase.swift @@ -30,7 +30,7 @@ class OnDiskTestCase: BaseTestCase { // MARK: - On Disk NSPersistentContainer -final class OnDiskPersistentContainer: NSPersistentContainer { +final class OnDiskPersistentContainer: NSPersistentContainer, @unchecked Sendable { static func makeNew() -> OnDiskPersistentContainer { Self.makeNew(id: UUID()) diff --git a/Tests/Utils/OnDiskWithProgrammaticallyModelTestCase.swift b/Tests/Utils/OnDiskWithProgrammaticallyModelTestCase.swift index cd74653d..4c73228b 100644 --- a/Tests/Utils/OnDiskWithProgrammaticallyModelTestCase.swift +++ b/Tests/Utils/OnDiskWithProgrammaticallyModelTestCase.swift @@ -28,14 +28,16 @@ class OnDiskWithProgrammaticallyModelTestCase: XCTestCase { // MARK: - On Disk NSPersistentContainer with Programmatically Model -final class OnDiskWithProgrammaticallyModelPersistentContainer: NSPersistentContainer { +final class OnDiskWithProgrammaticallyModelPersistentContainer: NSPersistentContainer, @unchecked Sendable { static func makeNew() -> OnDiskWithProgrammaticallyModelPersistentContainer { Self.makeNew(id: UUID().uuidString) } - static func makeNew(id: String, - forStagedMigration enableStagedMigration: Bool = false, - model: NSManagedObjectModel = V1.makeManagedObjectModel()) -> OnDiskWithProgrammaticallyModelPersistentContainer { + static func makeNew( + id: String, + forStagedMigration enableStagedMigration: Bool = false, + model: NSManagedObjectModel = V1.makeManagedObjectModel() + ) -> OnDiskWithProgrammaticallyModelPersistentContainer { let url = URL.newDatabaseURL(withName: id) let container = OnDiskWithProgrammaticallyModelPersistentContainer(name: "SampleModel2", managedObjectModel: model) let description = NSPersistentStoreDescription() diff --git a/Tests/Utils/Utils.swift b/Tests/Utils/Utils.swift index f9341d7d..abda2797 100644 --- a/Tests/Utils/Utils.swift +++ b/Tests/Utils/Utils.swift @@ -6,8 +6,8 @@ import CoreData // It should be fine to mark these as Sendable because they can be shared between different threads. // https://duckrowing.com/2010/03/11/using-core-data-on-multiple-threads/ -extension NSManagedObjectContext: @unchecked Sendable {} -extension NSManagedObjectModel: @unchecked Sendable {} +extension NSManagedObjectContext: @unchecked @retroactive Sendable {} +extension NSManagedObjectModel: @unchecked @retroactive Sendable {} // MARK: - URL