Skip to content

Instantly share code, notes, and snippets.

@southerton81
Last active November 5, 2022 17:04
Show Gist options
  • Save southerton81/6ebf422ee63ff63e68fdea08a1decdc5 to your computer and use it in GitHub Desktop.
Save southerton81/6ebf422ee63ff63e68fdea08a1decdc5 to your computer and use it in GitHub Desktop.
import XCTest
import CoreData
@testable import iosApp
class iosAppTests: XCTestCase {
/*
ChartState - is a CoreData entity with two fields: chartLen and seed:
<entity name="ChartState" representedClassName="ChartState" syncable="YES" codeGenerationType="class">
<attribute name="chartLen" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="seed" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
*/
/*
CoreDataInventory - is a class defined in the app, it holds the NSPersistentContainer instance:
final class CoreDataInventory {
static let instance = CoreDataInventory()
let persistentContainer: NSPersistentContainer
private init() {
persistentContainer = NSPersistentContainer(name: "Data")
persistentContainer.loadPersistentStores { _, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
}
*/
func testCoreDataMergeConflict() {
let coreData = CoreDataInventory.instance.persistentContainer
let expectation = XCTestExpectation(description: "")
expectation.expectedFulfillmentCount = 2
// 1. Launch new background task to create a new ChartState entity and save it
coreData.performBackgroundTask { (context) in
let newChartState = ChartState(context: context)
newChartState.chartLen = 100
newChartState.seed = 1
do {
try context.save()
} catch {
let nsError = error as NSError
fatalError("saveContext() error: \(nsError), \(nsError.userInfo)")
}
}
// 2. Launch two simultaneous background tasks
// 2.1 This task sleeps for 1 second, reads the written ChartLen from CoreData and modifies it, then saves it
coreData.performBackgroundTask { (context) in
sleep(1)
self.modifyChartStateSeed(context, 2)
sleep(1) // Wait 1 seconds before save for the next background task to read the same version of entity
do {
try context.save()
} catch {
let nsError = error as NSError
fatalError("saveContext() error: \(nsError), \(nsError.userInfo)")
}
expectation.fulfill()
}
// 2.2 This task also modifies the same entity, but waits another second before saving it
coreData.performBackgroundTask { (context) in
// Uncomment to set one of the Merge Policies, and bypass an error by prefering saved or in-memory data version
//context.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType)
sleep(1)
self.modifyChartStateSeed(context, 3)
sleep(2) // Wait 2 seconds before save, so that the previous performBackgroundTask() would have already saved its ChartState version
do {
// This save would produce a merge conflict, because read entity was already modified and saved by the other context,
// so the entity saved version is already greater than the one that was read in this thread
try context.save()
} catch {
let nsError = error as NSError
// Would land here with the "Could not merge changes ... oldVersion = 1 and newVersion = 2 and
// old object snapshot = {chartLen = 100; seed = 1;} and new cached row = {chartLen = 100; seed = 2;}""
fatalError("saveContext() error: \(nsError), \(nsError.userInfo)")
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 6)
}
private func modifyChartStateSeed(_ context: NSManagedObjectContext, _ seed: Int32) {
let fetchRequest: NSFetchRequest<ChartState> = ChartState.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "chartLen = %i", 100)
let chartStateEntity = try? context.fetch(fetchRequest).first
chartStateEntity?.seed = seed
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment