Skip to content

Commit

Permalink
Add integration test
Browse files Browse the repository at this point in the history
  • Loading branch information
lawmicha committed Feb 14, 2024
1 parent 450895f commit 9fab7e9
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation
import Combine
import XCTest

import Amplify
import AWSPluginsCore
import AWSDataStorePlugin

extension AWSDataStoreLazyLoadPostComment4V2Tests {

static let loggingContext = "multiSaveWithInterruptions"

/// Test performing save's and stop/start concurrently.
///
/// This test was validated prior to [PR 3492](https://github.com/aws-amplify/amplify-swift/pull/3492)
/// and will fail. The failure will show up when the test asserts that a queried comment from AppSync should contain the associated
/// post, but comment's post is `nil`. See the PR for changes in adding transactional support for commiting the two writes (saving the model and
/// mutation event). and MutationEvent dequeuing logic.
///
/// - Given: A set of models (post and comment) created and saved to DataStore.
/// - When: A detached task will interrupt DataStore by calling `DataStore.stop()`,
/// followed by restarting it (`DataStore.start()`), while saving comment and posts.
/// - Then:
/// - DataStore should sync data in the correct order of what was saved/submitted to it
/// - the post should be synced before the comment
/// - it should not skip over an item, ie. a comment saved but post is missing.
/// - The remote store should contain all items synced
/// - comments and post should exist.
/// - the comment should also have the post reference.
///
func testMultiSaveWithInterruptions() async throws {
await setup(withModels: PostComment4V2Models())
let amplify = AmplifyTestExecutor()

Amplify.Logging.info("[\(AWSDataStoreLazyLoadPostComment4V2Tests.loggingContext)] Begin saving data with interruptions")
let savesSyncedExpectation = expectation(description: "Outbox is empty after saving (with interruptions)")
savesSyncedExpectation.assertForOverFulfill = false
try await amplify.multipleSavesWithInterruptions(savesSyncedExpectation)
await fulfillment(of: [savesSyncedExpectation], timeout: 120)

Amplify.Logging.info("[\(AWSDataStoreLazyLoadPostComment4V2Tests.loggingContext)] Outbox is empty, begin asserting data")
let savedModels = await amplify.savedModels
for savedModel in savedModels {
let savedComment = savedModel.0
let savedPost = savedModel.1

try await assertQueryComment(savedComment, post: savedPost)
try await assertQueryPost(savedPost)
}
Amplify.Logging.info("[\(AWSDataStoreLazyLoadPostComment4V2Tests.loggingContext)] All models match remote store, begin clean up.")
try await cleanUp(savedModels)
}

actor AmplifyTestExecutor {
var savedModels = [(Comment, Post)]()

/// The minimum number of iterations, through trial and error, found to reproduce the bug.
private let count = 15

/// `isOutboxEmpty` is used to return the flow back to the caller via fulfilling the `savesSyncedExpectation`.
/// By listening to the OutboxEvent after performing the operations, the last outboxEvent to be `true` while `index`
/// is the last index, will be when `savesSyncedExpectation` is fulfilled and returned execution back to the caller.
private var isOutboxEmpty = false

private var index = 0
private var subscribeToOutboxEventTask: Task<Void, Never>?
private var outboxEventsCount = 0

/// Perform saving the comment/post in one detached task while another detached task will
/// perform the interruption (stop/start). Repeat with a bit of delay to allow DataStore some
/// time to kick off its start sequence- this will always be the case since the last operation of
/// each detached task is a `save` (implicit `start`) or a `start`.
func multipleSavesWithInterruptions(_ savesSyncedExpectation: XCTestExpectation) async throws {
subscribeToOutboxEvent(savesSyncedExpectation)
while isOutboxEmpty == false {
try await Task.sleep(seconds: 1)
}
for i in 0..<count {
let post = Post(title: "title")
let comment = Comment(content: "content", post: post)
savedModels.append((comment,post))

Task.detached {
Amplify.Logging.info("[\(AWSDataStoreLazyLoadPostComment4V2Tests.loggingContext)] Saving comment and post, index: \(i)")
do {
_ = try await Amplify.DataStore.save(post)
_ = try await Amplify.DataStore.save(comment)
} catch {
// This is expected to happen when DataStore is interrupted and did not save the MutationEvent.
Amplify.Logging.info("[\(AWSDataStoreLazyLoadPostComment4V2Tests.loggingContext)] Failed to save post and/or comment, post: \(post.id). comment: \(comment.id). error \(error)")
}
}
Task.detached {
Amplify.Logging.info("[\(AWSDataStoreLazyLoadPostComment4V2Tests.loggingContext)] Stop/Start, index: \(i)")
try await Amplify.DataStore.stop()
try await Amplify.DataStore.start()
}
self.index = i
try await Task.sleep(seconds: 0.01)
}
}

/// Subscribe to DataStore Hub events, and handle `OutboxStatusEvent`'s.
/// Maintain the latest state of whether the outbox is empty or not in `isOutboxEmpty` variable.
/// Fulfill `savesSyncedExpectation` after all tasks have been created and the outbox is empty.
private func subscribeToOutboxEvent(_ savesSyncedExpectation: XCTestExpectation) {
self.subscribeToOutboxEventTask = Task {
for await event in Amplify.Hub.publisher(for: .dataStore).values {
switch event.eventName {
case HubPayload.EventName.DataStore.outboxStatus:
guard let outboxEvent = event.data as? OutboxStatusEvent else {
return
}
isOutboxEmpty = outboxEvent.isEmpty
outboxEventsCount += 1
Amplify.Logging.info("[\(AWSDataStoreLazyLoadPostComment4V2Tests.loggingContext)] \(outboxEventsCount) isOutboxEmpty: \(isOutboxEmpty), index: \(index)")
if index == (count - 1) && isOutboxEmpty {
XCTAssertEqual(savedModels.count, count)
savesSyncedExpectation.fulfill()
}
default:
break
}
}
}
}
}

func assertQueryComment(_ savedComment: Comment, post: Post) async throws {
guard let persistedComment = try await Amplify.DataStore.query(
Comment.self,
byIdentifier: savedComment.identifier) else {
Amplify.Logging.info("[\(AWSDataStoreLazyLoadPostComment4V2Tests.loggingContext)] Skipping comment \(savedComment.id) since it is not persisted in local DB")
return
}

let result = try await Amplify.API.query(
request: .get(
Comment.self,
byIdentifier: savedComment.id))
switch result {
case .success(let comment):
guard let comment else {
XCTFail("Missing comment, should contain \(savedComment)")
return
}
assertLazyReference(
comment._post,
state: .notLoaded(
identifiers: [.init(
name: "id",
value: post.identifier)]))
case .failure(let error):
XCTFail("Failed to query, error \(error)")
}
}

func assertQueryPost(_ savedPost: Post) async throws {
guard let persistedPost = try await Amplify.DataStore.query(
Post.self,
byIdentifier: savedPost.identifier) else {
Amplify.Logging.info("[\(AWSDataStoreLazyLoadPostComment4V2Tests.loggingContext)] Skipping post \(savedPost.id) since it is not persisted in local DB")
return
}
let result = try await Amplify.API.query(
request: .get(
Post.self,
byIdentifier: savedPost.id))
switch result {
case .success(let post):
guard post != nil else {
XCTFail("Missing post, should contain \(savedPost)")
return
}
case .failure(let error):
XCTFail("Failed to query, error \(error)")
}
}

func cleanUp(_ savedModels: [(Comment, Post)]) async throws {
for savedModel in savedModels {
let savedComment = savedModel.0
let savedPost = savedModel.1

do {
_ = try await Amplify.API.mutate(
request: .deleteMutation(
of: savedComment,
modelSchema: Comment.schema,
version: 1))

_ = try await Amplify.API.mutate(
request: .deleteMutation(
of: savedPost,
modelSchema: Post.schema,
version: 1))
} catch {
// Some models that fail to save don't need to be deleted,
// swallowing the error to continue deleting others
}
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,18 @@ class AWSDataStoreLazyLoadBaseTest: XCTestCase {
Amplify.Logging.logLevel = logLevel

#if os(watchOS)
try Amplify.add(plugin: AWSDataStorePlugin(modelRegistration: models,
configuration: .custom(syncMaxRecords: 100, disableSubscriptions: { false })))
try Amplify.add(plugin: AWSDataStorePlugin(
modelRegistration: models,
configuration: .custom(
errorHandler: { error in Amplify.Logging.error("DataStore ErrorHandler error: \(error)")},
syncMaxRecords: 100,
disableSubscriptions: { false })))
#else
try Amplify.add(plugin: AWSDataStorePlugin(modelRegistration: models,
configuration: .custom(syncMaxRecords: 100)))
try Amplify.add(plugin: AWSDataStorePlugin(
modelRegistration: models,
configuration: .custom(
errorHandler: { error in Amplify.Logging.error("DataStore ErrorHandler error: \(error)")},
syncMaxRecords: 100)))
#endif
try Amplify.add(plugin: AWSAPIPlugin(sessionFactory: AmplifyURLSessionFactory()))
try Amplify.configure(amplifyConfig)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@
21C3C4C1293FD9BD009194A0 /* AWSDataStoreLazyLoadDefaultPKTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21C3C4C0293FD9BD009194A0 /* AWSDataStoreLazyLoadDefaultPKTests.swift */; };
21C3C4C3293FD9FF009194A0 /* AWSDataStoreLazyLoadHasOneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21C3C4C2293FD9FF009194A0 /* AWSDataStoreLazyLoadHasOneTests.swift */; };
21C3C4C5293FDA12009194A0 /* AWSDataStoreLazyLoadCompositePKTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21C3C4C4293FDA12009194A0 /* AWSDataStoreLazyLoadCompositePKTests.swift */; };
21D8E2D22B7BF89900F945F8 /* AWSDataStoreLazyLoadPostComment4V2StressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21D8E2D12B7BF89900F945F8 /* AWSDataStoreLazyLoadPostComment4V2StressTests.swift */; };
21DFAE9F295F4E8600B4A883 /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DFAE99295F4E8500B4A883 /* Person.swift */; };
21DFAEA0295F4E8600B4A883 /* PhoneCall+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DFAE9A295F4E8500B4A883 /* PhoneCall+Schema.swift */; };
21DFAEA1295F4E8600B4A883 /* Transcript+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DFAE9B295F4E8500B4A883 /* Transcript+Schema.swift */; };
Expand Down Expand Up @@ -2112,6 +2113,7 @@
21C3C4C0293FD9BD009194A0 /* AWSDataStoreLazyLoadDefaultPKTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSDataStoreLazyLoadDefaultPKTests.swift; sourceTree = "<group>"; };
21C3C4C2293FD9FF009194A0 /* AWSDataStoreLazyLoadHasOneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSDataStoreLazyLoadHasOneTests.swift; sourceTree = "<group>"; };
21C3C4C4293FDA12009194A0 /* AWSDataStoreLazyLoadCompositePKTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSDataStoreLazyLoadCompositePKTests.swift; sourceTree = "<group>"; };
21D8E2D12B7BF89900F945F8 /* AWSDataStoreLazyLoadPostComment4V2StressTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSDataStoreLazyLoadPostComment4V2StressTests.swift; sourceTree = "<group>"; };
21DFAE99295F4E8500B4A883 /* Person.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Person.swift; sourceTree = "<group>"; };
21DFAE9A295F4E8500B4A883 /* PhoneCall+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PhoneCall+Schema.swift"; sourceTree = "<group>"; };
21DFAE9B295F4E8500B4A883 /* Transcript+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Transcript+Schema.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2643,6 +2645,7 @@
children = (
21801D0528F9AE9400FFA37E /* AWSDataStoreLazyLoadPostComment4V2Tests.swift */,
210EB5EB294796C40060C185 /* AWSDataStoreLazyLoadPostComment4V2SnapshotTests.swift */,
21D8E2D12B7BF89900F945F8 /* AWSDataStoreLazyLoadPostComment4V2StressTests.swift */,
21801CB728F9A86700FFA37E /* Comment4V2.swift */,
21801CCB28F9A86800FFA37E /* Comment4V2+Schema.swift */,
21801CBA28F9A86700FFA37E /* Post4V2.swift */,
Expand Down Expand Up @@ -5167,6 +5170,7 @@
21801D6629097D5800FFA37E /* DefaultPKChild+Schema.swift in Sources */,
21801D0728F9B11800FFA37E /* AsyncTesting.swift in Sources */,
21DFAEA0295F4E8600B4A883 /* PhoneCall+Schema.swift in Sources */,
21D8E2D22B7BF89900F945F8 /* AWSDataStoreLazyLoadPostComment4V2StressTests.swift in Sources */,
21801D2A29006DA300FFA37E /* Project1.swift in Sources */,
21AB5C5529819BF100CCA482 /* Nested.swift in Sources */,
2138482A2947ACCF00BBA647 /* AWSDataStoreLazyLoadBlogPostComment8V2Tests.swift in Sources */,
Expand Down

0 comments on commit 9fab7e9

Please sign in to comment.