Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions Modules/Sources/WordPressData/Swift/ContextManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,16 +249,14 @@ private extension ContextManager {
}
}

// MARK: - Initialise Core Data stack

private extension ContextManager {
public extension ContextManager {
/// The current version of the data model, loaded once per process.
///
/// Every loaded `NSManagedObjectModel` registers its entities in Core Data's global
/// class-to-entity table. Loading the model file more than once (which happens in unit
/// tests, where each test creates its own `ContextManager` instance) leaves multiple
/// entity descriptions claiming the same `NSManagedObject` subclasses, and
/// `+[NSManagedObject entity]` fails to resolve them.
/// Reuse this shared instance instead of loading the model again. Every loaded
/// `NSManagedObjectModel` registers its entities in Core Data's global class-to-entity table,
/// so loading the model file more than once (which happens in unit tests that each create their
/// own `ContextManager`) leaves multiple entity descriptions claiming the same `NSManagedObject`
/// subclasses, and `+[NSManagedObject entity]` fails to resolve them.
static let currentObjectModel: NSManagedObjectModel = {
guard let modelFileURL = Bundle.wordPressData.url(forResource: "WordPress", withExtension: "momd") else {
fatalError("Can't find WordPress.momd")
Expand All @@ -270,7 +268,11 @@ private extension ContextManager {

return objectModel
}()
}

// MARK: - Initialise Core Data stack

private extension ContextManager {
static func objectModel(named modelName: String) -> NSManagedObjectModel {
guard modelName != ContextManagerModelNameCurrent else {
return currentObjectModel
Expand Down
2 changes: 1 addition & 1 deletion Modules/Tests/WordPressDataTests/ContextManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ class ContextManagerTests: XCTestCase {
/// This function ensures created Core Data stack is cleaned up properly, so that the database file
/// is ready to be used by `ContextManager` to perform migration.
private func prepareForMigration(withModelName modelName: String, block: (NSManagedObjectContext) throws -> Void) throws {
let model = try XCTUnwrap(NSManagedObjectModel(contentsOf: XCTUnwrap(urlForModelName(modelName))))
let model = try XCTUnwrap(NSManagedObjectModel(contentsOf: XCTUnwrap(urlForModelName(modelName)))).neutralizingEntityClasses()
let container = NSPersistentContainer(name: "WordPress", managedObjectModel: model)
let storeDesc = NSPersistentStoreDescription(url: storeURL)
storeDesc.type = NSSQLiteStoreType
Expand Down
213 changes: 0 additions & 213 deletions Modules/Tests/WordPressDataTests/CoreDataMigrationTests.swift

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import CoreData

extension NSManagedObjectModel {
/// Resets every entity's managed object class to the generic `NSManagedObject`.
///
/// Historical schema versions all define the same `NSManagedObject` subclasses (`Blog`,
/// `Post`, ...). Core Data registers each loaded model's entities in a single process-global
/// class-to-entity table, and the unit tests all run in one process. Loading more than one
/// model that claims the same subclass leaves `+[NSManagedObject entity]` unable to resolve a
/// unique match, which crashes unrelated Core Data tests depending on execution order.
///
/// Migration tests only reach their data by entity name and key-value coding, so dropping the
/// concrete class association keeps them working while preventing the duplicate registration.
/// Call this right after loading a historical (non-current) model, before attaching it to a
/// coordinator.
func neutralizingEntityClasses() -> NSManagedObjectModel {
for entity in entities {
entity.managedObjectClassName = NSStringFromClass(NSManagedObject.self)
}
return self
}
}
21 changes: 19 additions & 2 deletions Tests/KeystoneTests/Tests/Jetpack/DataMigratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ private extension DataMigratorTests {
}

func createContext(
for model: NSManagedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle.wordPressData])!,
for model: NSManagedObjectModel = ContextManager.currentObjectModel,
type: String = NSInMemoryStoreType,
at location: URL? = nil
) throws -> NSManagedObjectContext {
Expand Down Expand Up @@ -440,7 +440,7 @@ private extension DataMigratorTests {
guard let url = urlForModel(name: modelName) else {
return nil
}
return NSManagedObjectModel(contentsOf: url)
return NSManagedObjectModel(contentsOf: url)?.neutralizingEntityClasses()
}

func urlForModel(name: String) -> URL? {
Expand Down Expand Up @@ -508,3 +508,20 @@ private extension DataMigratorTests {
return fileURL
}
}

private extension NSManagedObjectModel {
/// Resets every entity's managed object class to the generic `NSManagedObject`.
///
/// The unit tests all run in one process, and Core Data registers each loaded model's entities
/// in a single process-global class-to-entity table. Historical schema versions (and a second
/// copy of the current one) all claim the same `NSManagedObject` subclasses, which leaves
/// `+[NSManagedObject entity]` unable to resolve a unique match and crashes unrelated Core Data
/// tests depending on execution order. `DataMigrator` only copies the store at the file level,
/// so it never needs the concrete classes.
func neutralizingEntityClasses() -> NSManagedObjectModel {
for entity in entities {
entity.managedObjectClassName = NSStringFromClass(NSManagedObject.self)
}
return self
}
}