Skip to content

Commit aad3b24

Browse files
committed
Add tests for atomic batch operations and fix API inconsistencies
3.0-preview
1 parent e8861de commit aad3b24

2 files changed

Lines changed: 178 additions & 31 deletions

File tree

Sources/CoreDataRepository/CoreDataRepository+Batch.swift

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ extension CoreDataRepository {
6868
}
6969

7070
/// Create a batch of unmanaged models.
71-
public func createAtomiclly<Model: UnmanagedModel>(
71+
public func createAtomically<Model: UnmanagedModel>(
7272
_ items: [Model],
7373
transactionAuthor: String? = nil
7474
) async -> Result<[Model], CoreDataError> {
@@ -131,8 +131,8 @@ extension CoreDataRepository {
131131

132132
/// Read a batch of unmanaged models.
133133
public func readAtomically<Model: UnmanagedModel>(
134-
_ urls: [URL],
135-
of _: Model.Type
134+
urls: [URL],
135+
as _: Model.Type
136136
) async -> Result<[Model], CoreDataError> {
137137
await context.performInChild(schedule: .enqueued) { readContext in
138138
try urls.map { url in
@@ -160,33 +160,6 @@ extension CoreDataRepository {
160160
}
161161
}
162162

163-
/// Update the store with a batch of unmanaged models.
164-
public func updateAtomically<Model: UnmanagedModel>(
165-
_ items: [Model],
166-
transactionAuthor: String? = nil
167-
) async -> Result<[Model], CoreDataError> {
168-
await context.performInScratchPad(schedule: .enqueued) { [context] scratchPad in
169-
scratchPad.transactionAuthor = transactionAuthor
170-
let objects = try items.map { item in
171-
guard let url = item.managedIdUrl else {
172-
throw CoreDataError.noUrlOnItemToMapToObjectId
173-
}
174-
let id = try scratchPad.objectId(from: url).get()
175-
let object = try scratchPad.notDeletedObject(for: id)
176-
let managed: Model.ManagedModel = try object.asManagedModel()
177-
try item.updating(managed: managed)
178-
return managed
179-
}
180-
try scratchPad.save()
181-
try context.performAndWait {
182-
context.transactionAuthor = transactionAuthor
183-
try context.save()
184-
context.transactionAuthor = nil
185-
}
186-
return try objects.map(Model.init(managed:))
187-
}
188-
}
189-
190163
/// Update the store with a batch of unmanaged models.
191164
///
192165
/// This operation is non-atomic. Each instance may succeed or fail individually.
@@ -231,6 +204,33 @@ extension CoreDataRepository {
231204
return (success: successes, failed: failures)
232205
}
233206

207+
/// Update the store with a batch of unmanaged models.
208+
public func updateAtomically<Model: UnmanagedModel>(
209+
_ items: [Model],
210+
transactionAuthor: String? = nil
211+
) async -> Result<[Model], CoreDataError> {
212+
await context.performInScratchPad(schedule: .enqueued) { [context] scratchPad in
213+
scratchPad.transactionAuthor = transactionAuthor
214+
let objects = try items.map { item in
215+
guard let url = item.managedIdUrl else {
216+
throw CoreDataError.noUrlOnItemToMapToObjectId
217+
}
218+
let id = try scratchPad.objectId(from: url).get()
219+
let object = try scratchPad.notDeletedObject(for: id)
220+
let managed: Model.ManagedModel = try object.asManagedModel()
221+
try item.updating(managed: managed)
222+
return managed
223+
}
224+
try scratchPad.save()
225+
try context.performAndWait {
226+
context.transactionAuthor = transactionAuthor
227+
try context.save()
228+
context.transactionAuthor = nil
229+
}
230+
return try objects.map(Model.init(managed:))
231+
}
232+
}
233+
234234
/// Execute a NSBatchDeleteRequest against the store.
235235
public func delete(
236236
_ request: NSBatchDeleteRequest,
@@ -290,7 +290,7 @@ extension CoreDataRepository {
290290

291291
/// Delete from the store with a batch of unmanaged models.
292292
public func deleteAtomically(
293-
_ urls: [URL],
293+
urls: [URL],
294294
transactionAuthor: String? = nil
295295
) async -> Result<Void, CoreDataError> {
296296
await context.performInScratchPad(schedule: .enqueued) { [context] scratchPad in

Tests/CoreDataRepositoryTests/BatchRepositoryTests.swift

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,55 @@ final class BatchRepositoryTests: CoreDataXCTestCase {
141141
try verify(transactionAuthor: transactionAuthor, timeStamp: historyTimeStamp)
142142
}
143143

144+
func testCreateAtomicallySuccess() async throws {
145+
let fetchRequest = Movie.managedFetchRequest()
146+
try await repositoryContext().perform {
147+
let count = try self.repositoryContext().count(for: fetchRequest)
148+
XCTAssertEqual(count, 0, "Count of objects in CoreData should be zero at the start of each test.")
149+
}
150+
151+
let historyTimeStamp = Date()
152+
let transactionAuthor: String = #function
153+
154+
let newMovies = try movies.map(mapDictToMovie(_:))
155+
let createdMovies: [Movie]
156+
switch try await repository()
157+
.createAtomically(newMovies, transactionAuthor: transactionAuthor)
158+
{
159+
case let .success(_createdMovies):
160+
createdMovies = _createdMovies
161+
case let .failure(error):
162+
XCTFail("Not expecting failure: \(error.localizedDescription)")
163+
return
164+
}
165+
166+
XCTAssertEqual(createdMovies.count, newMovies.count)
167+
168+
for movie in createdMovies {
169+
try await verify(movie)
170+
}
171+
172+
let createdMoviesForEquality = createdMovies.map { movie in
173+
var movie = movie
174+
XCTAssertNotNil(movie.url)
175+
movie.url = nil
176+
return movie
177+
}
178+
179+
XCTAssertNoDifference(createdMoviesForEquality, newMovies)
180+
181+
try await repositoryContext().perform {
182+
let data = try self.repositoryContext().fetch(fetchRequest)
183+
XCTAssertEqual(
184+
data.map { $0.title ?? "" }.sorted(),
185+
["A", "B", "C", "D", "E"],
186+
"Inserted titles should match expectation"
187+
)
188+
}
189+
190+
try verify(transactionAuthor: transactionAuthor, timeStamp: historyTimeStamp)
191+
}
192+
144193
func testReadSuccess() async throws {
145194
let fetchRequest = Movie.managedFetchRequest()
146195
var movies = [Movie]()
@@ -162,6 +211,33 @@ final class BatchRepositoryTests: CoreDataXCTestCase {
162211
XCTAssertEqual(Set(movies), Set(result.success))
163212
}
164213

214+
func testReadAtomicallySuccess() async throws {
215+
let fetchRequest = Movie.managedFetchRequest()
216+
var movies = [Movie]()
217+
try await repositoryContext().perform {
218+
let count = try self.repositoryContext().count(for: fetchRequest)
219+
XCTAssertEqual(count, 0, "Count of objects in CoreData should be zero at the start of each test.")
220+
221+
let managedMovies = try self.movies
222+
.map(self.mapDictToManagedMovie(_:))
223+
try self.repositoryContext().save()
224+
movies = try managedMovies.map(Movie.init(managed:))
225+
}
226+
227+
let readMovies: [Movie]
228+
switch try await repository().readAtomically(urls: movies.compactMap(\.url), as: Movie.self) {
229+
case let .success(_readMovies):
230+
readMovies = _readMovies
231+
case let .failure(error):
232+
XCTFail("Not expecting failure: \(error.localizedDescription)")
233+
return
234+
}
235+
236+
XCTAssertEqual(readMovies.count, movies.count)
237+
238+
XCTAssertNoDifference(readMovies, movies)
239+
}
240+
165241
func testUpdateSuccess() async throws {
166242
let fetchRequest = Movie.managedFetchRequest()
167243
try await repositoryContext().perform {
@@ -226,6 +302,44 @@ final class BatchRepositoryTests: CoreDataXCTestCase {
226302
try verify(transactionAuthor: transactionAuthor, timeStamp: historyTimeStamp)
227303
}
228304

305+
func testAltUpdateAtomicallySuccess() async throws {
306+
let fetchRequest = Movie.managedFetchRequest()
307+
var movies = [Movie]()
308+
try await repositoryContext().perform {
309+
let count = try self.repositoryContext().count(for: fetchRequest)
310+
XCTAssertEqual(count, 0, "Count of objects in CoreData should be zero at the start of each test.")
311+
312+
let managedMovies = try self.movies
313+
.map(self.mapDictToManagedMovie(_:))
314+
try self.repositoryContext().save()
315+
movies = try managedMovies.map(Movie.init(managed:))
316+
}
317+
318+
var editedMovies = movies
319+
let newTitles = ["ZA", "ZB", "ZC", "ZD", "ZE"]
320+
newTitles.enumerated().forEach { index, title in editedMovies[index].title = title }
321+
322+
let historyTimeStamp = Date()
323+
let transactionAuthor: String = #function
324+
325+
let updatedMovies: [Movie]
326+
switch try await repository()
327+
.updateAtomically(editedMovies, transactionAuthor: transactionAuthor)
328+
{
329+
case let .success(_updatedMovies):
330+
updatedMovies = _updatedMovies
331+
case let .failure(error):
332+
XCTFail("Not expecting failure: \(error.localizedDescription)")
333+
return
334+
}
335+
336+
XCTAssertEqual(updatedMovies.count, movies.count)
337+
338+
XCTAssertNoDifference(updatedMovies, editedMovies)
339+
340+
try verify(transactionAuthor: transactionAuthor, timeStamp: historyTimeStamp)
341+
}
342+
229343
func testDeleteSuccess() async throws {
230344
let fetchRequest = Movie.managedFetchRequest()
231345
try await repositoryContext().perform {
@@ -284,4 +398,37 @@ final class BatchRepositoryTests: CoreDataXCTestCase {
284398
}
285399
try verify(transactionAuthor: transactionAuthor, timeStamp: historyTimeStamp)
286400
}
401+
402+
func testAltDeleteAtomicallySuccess() async throws {
403+
let fetchRequest = Movie.managedFetchRequest()
404+
var movies = [Movie]()
405+
try await repositoryContext().perform {
406+
let count = try self.repositoryContext().count(for: fetchRequest)
407+
XCTAssertEqual(count, 0, "Count of objects in CoreData should be zero at the start of each test.")
408+
409+
let managedMovies = try self.movies
410+
.map(self.mapDictToManagedMovie(_:))
411+
try self.repositoryContext().save()
412+
movies = try managedMovies.map(Movie.init(managed:))
413+
}
414+
415+
let historyTimeStamp = Date()
416+
let transactionAuthor: String = #function
417+
418+
switch try await repository()
419+
.deleteAtomically(urls: movies.compactMap(\.url), transactionAuthor: transactionAuthor)
420+
{
421+
case .success:
422+
break
423+
case let .failure(error):
424+
XCTFail("Not expecting failure: \(error.localizedDescription)")
425+
return
426+
}
427+
428+
try await repositoryContext().perform {
429+
let data = try self.repositoryContext().fetch(fetchRequest)
430+
XCTAssertEqual(data.map { $0.title ?? "" }.sorted(), [], "There should be no remaining values.")
431+
}
432+
try verify(transactionAuthor: transactionAuthor, timeStamp: historyTimeStamp)
433+
}
287434
}

0 commit comments

Comments
 (0)