diff --git a/WordPress/Classes/Utility/ContainerContextFactory.swift b/WordPress/Classes/Utility/ContainerContextFactory.swift index 2fe68f7b5f89..350b8e543a18 100644 --- a/WordPress/Classes/Utility/ContainerContextFactory.swift +++ b/WordPress/Classes/Utility/ContainerContextFactory.swift @@ -27,6 +27,13 @@ class ContainerContextFactory: NSObject, ManagedObjectContextFactory { completionBlock?() } } + + /// Ensure that the `context`'s concurrency type is not `confinementConcurrencyType`, since it will crash if `perform` or `performAndWait` is called. + guard context.concurrencyType == .mainQueueConcurrencyType || context.concurrencyType == .privateQueueConcurrencyType else { + block() + return + } + if wait { context.performAndWait(block) } else { diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 1a38767d3420..e3822752076c 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -5209,6 +5209,7 @@ FE25C235271F23000084E1DB /* ReaderCommentsNotificationSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE25C234271F23000084E1DB /* ReaderCommentsNotificationSheetViewController.swift */; }; FE25C236271F23000084E1DB /* ReaderCommentsNotificationSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE25C234271F23000084E1DB /* ReaderCommentsNotificationSheetViewController.swift */; }; FE2E3729281C839C00A1E82A /* BloggingPromptsServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2E3728281C839C00A1E82A /* BloggingPromptsServiceTests.swift */; }; + FE320CC5294705990046899B /* ContainerContextFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE320CC4294705990046899B /* ContainerContextFactoryTests.swift */; }; FE32E7F12844971000744D80 /* ReminderScheduleCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE32E7F02844971000744D80 /* ReminderScheduleCoordinatorTests.swift */; }; FE32EFFF275914390040BE67 /* MenuSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE32EFFE275914390040BE67 /* MenuSheetViewController.swift */; }; FE32F000275914390040BE67 /* MenuSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE32EFFE275914390040BE67 /* MenuSheetViewController.swift */; }; @@ -8810,6 +8811,7 @@ FE23EB4826E7C91F005A1698 /* richCommentStyle.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = richCommentStyle.css; path = Resources/HTML/richCommentStyle.css; sourceTree = ""; }; FE25C234271F23000084E1DB /* ReaderCommentsNotificationSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderCommentsNotificationSheetViewController.swift; sourceTree = ""; }; FE2E3728281C839C00A1E82A /* BloggingPromptsServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloggingPromptsServiceTests.swift; sourceTree = ""; }; + FE320CC4294705990046899B /* ContainerContextFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerContextFactoryTests.swift; sourceTree = ""; }; FE32E7F02844971000744D80 /* ReminderScheduleCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderScheduleCoordinatorTests.swift; sourceTree = ""; }; FE32E7F32846A68800744D80 /* WordPress 142.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 142.xcdatamodel"; sourceTree = ""; }; FE32EFFE275914390040BE67 /* MenuSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSheetViewController.swift; sourceTree = ""; }; @@ -9877,7 +9879,7 @@ path = Classes; sourceTree = ""; }; - 29B97314FDCFA39411CA2CEA = { + 29B97314FDCFA39411CA2CEA /* CustomTemplate */ = { isa = PBXGroup; children = ( 3F20FDF3276BF21000DA3CAD /* Packages */, @@ -14564,6 +14566,7 @@ 93E9050319E6F242005513C9 /* ContextManagerTests.swift */, B5ECA6CC1DBAAD510062D7E0 /* CoreDataHelperTests.swift */, 931D26FF19EDAE8600114F17 /* CoreDataMigrationTests.m */, + FE320CC4294705990046899B /* ContainerContextFactoryTests.swift */, ); name = "Core Data"; sourceTree = ""; @@ -17869,14 +17872,14 @@ bg, sk, ); - mainGroup = 29B97314FDCFA39411CA2CEA; + mainGroup = 29B97314FDCFA39411CA2CEA /* CustomTemplate */; packageReferences = ( 3FF1442E266F3C2400138163 /* XCRemoteSwiftPackageReference "ScreenObject" */, 3FC2C33B26C4CF0A00C6D98F /* XCRemoteSwiftPackageReference "XCUITestHelpers" */, 17A8858B2757B97F0071FCA3 /* XCRemoteSwiftPackageReference "AutomatticAbout-swift" */, 3F2B62DA284F4E0B0008CD59 /* XCRemoteSwiftPackageReference "Charts" */, 3F3B23C02858A1B300CACE60 /* XCRemoteSwiftPackageReference "test-collector-swift" */, - 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios.git" */, + 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios" */, 3F338B6F289BD3040014ADC5 /* XCRemoteSwiftPackageReference "Nimble" */, ); productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; @@ -22318,6 +22321,7 @@ C738CB1128626606001BE107 /* QRLoginVerifyCoordinatorTests.swift in Sources */, FF0B2567237A023C004E255F /* GutenbergVideoUploadProcessorTests.swift in Sources */, FF1B11E7238FE27A0038B93E /* GutenbergGalleryUploadProcessorTests.swift in Sources */, + FE320CC5294705990046899B /* ContainerContextFactoryTests.swift in Sources */, F4D9AF51288AE23500803D40 /* SuggestionTableViewTests.swift in Sources */, 8BC12F72231FEBA1004DDA72 /* PostCoordinatorTests.swift in Sources */, C3C2F84628AC8BC700937E45 /* JetpackBannerScrollVisibilityTests.swift in Sources */, @@ -29112,7 +29116,7 @@ minimumVersion = 0.3.0; }; }; - 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios.git" */ = { + 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/airbnb/lottie-ios.git"; requirement = { @@ -29185,12 +29189,12 @@ }; 3F411B6E28987E3F002513AE /* Lottie */ = { isa = XCSwiftPackageProductDependency; - package = 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios.git" */; + package = 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios" */; productName = Lottie; }; 3F44DD57289C379C006334CD /* Lottie */ = { isa = XCSwiftPackageProductDependency; - package = 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios.git" */; + package = 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios" */; productName = Lottie; }; 3FC2C33C26C4CF0A00C6D98F /* XCUITestHelpers */ = { diff --git a/WordPress/WordPressTest/ContainerContextFactoryTests.swift b/WordPress/WordPressTest/ContainerContextFactoryTests.swift new file mode 100644 index 000000000000..d7abb99219b1 --- /dev/null +++ b/WordPress/WordPressTest/ContainerContextFactoryTests.swift @@ -0,0 +1,112 @@ +import XCTest + +@testable import WordPress + +final class ContainerContextFactoryTests: XCTestCase { + + private var persistentContainer: NSPersistentContainer! + private var factory: ContainerContextFactory! + + override func setUp() { + super.setUp() + + persistentContainer = makeInMemoryContainer() + factory = ContainerContextFactory(persistentContainer: persistentContainer) + } + + override func tearDown() { + persistentContainer = nil + factory = nil + + super.tearDown() + } + + // MARK: - `save` Tests + + func test_save_givenMainQueueConcurrencyType_shouldCallPerform() { + let context = makeMockContext(concurrencyType: .mainQueueConcurrencyType) + + factory.save(context, andWait: false, withCompletionBlock: nil) + + XCTAssertTrue(context.performCalled) + XCTAssertFalse(context.performAndWaitCalled) + } + + func test_save_givenMainQueueConcurrencyType_andWaitIsSetToTrue_shouldCallPerformAndWait() { + let context = makeMockContext(concurrencyType: .mainQueueConcurrencyType) + + factory.save(context, andWait: true, withCompletionBlock: nil) + + XCTAssertFalse(context.performCalled) + XCTAssertTrue(context.performAndWaitCalled) + } + + func test_save_givenPrivateQueueConcurrencyType_shouldCallPerform() { + let context = makeMockContext(concurrencyType: .privateQueueConcurrencyType) + + factory.save(context, andWait: false, withCompletionBlock: nil) + + XCTAssertTrue(context.performCalled) + XCTAssertFalse(context.performAndWaitCalled) + } + + func test_save_givenPrivateQueueConcurrencyType_andWaitIsSetToTrue_shouldCallPerformAndWait() { + let context = makeMockContext(concurrencyType: .privateQueueConcurrencyType) + + factory.save(context, andWait: true, withCompletionBlock: nil) + + XCTAssertFalse(context.performCalled) + XCTAssertTrue(context.performAndWaitCalled) + } + + func test_save_givenDeprecatedConcurrencyType_shouldNotCallPerform() { + /// creates a context with `.confinementConcurrencyType`. The enum is created from its raw value + /// to prevent Xcode from complaining since the enum value is deprecated. + let context = makeMockContext(concurrencyType: NSManagedObjectContextConcurrencyType(rawValue: 0)!) + + factory.save(context, andWait: false, withCompletionBlock: nil) + + XCTAssertFalse(context.performCalled) + XCTAssertFalse(context.performAndWaitCalled) + } +} + +// MARK: - Private Helpers + +private extension ContainerContextFactoryTests { + + class MockManagedObjectContext: NSManagedObjectContext { + + var performCalled = false + var performAndWaitCalled = false + + override func perform(_ block: @escaping () -> Void) { + performCalled = true + } + + override func performAndWait(_ block: () -> Void) { + performAndWaitCalled = true + } + + override func save() throws { + // do nothing. let's make sure nothing gets saved. + } + } + + /// Creates an "in-memory" NSPersistentContainer. + /// This follows the approach used in WWDC'18: https://developer.apple.com/videos/play/wwdc2018/224/ + /// + /// - Returns: An instance of NSPersistentContainer that stores data in memory. + func makeInMemoryContainer() -> NSPersistentContainer { + let container = NSPersistentContainer(name: "WordPress") + let description = NSPersistentStoreDescription(url: .init(fileURLWithPath: "/dev/null")) + container.persistentStoreDescriptions = [description] + + return container + } + + func makeMockContext(concurrencyType: NSManagedObjectContextConcurrencyType) -> MockManagedObjectContext { + return MockManagedObjectContext(concurrencyType: concurrencyType) + } + +}