From aee4f96e69fd3d209fadc3a50a8e9e7d2dfbc596 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Mon, 1 May 2023 09:55:04 -0700 Subject: [PATCH] `Integration Tests`: add `MainThreadMonitor` to ensure main thread is not blocked We've had a few reports of deadlocks (#2412, #2375). This might have not detected them, but it would detect future issues, as well as busy operations running on the main thread. --- RevenueCat.xcodeproj/project.pbxproj | 4 ++ .../BaseBackendIntegrationTests.swift | 11 ++++ .../MainThreadMonitor.swift | 52 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 Tests/BackendIntegrationTests/MainThreadMonitor.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 9a50326714..865eae6647 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -199,6 +199,7 @@ 37E3578711F5FDD5DC6458A8 /* AttributionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E3521731D8DC16873F55F3 /* AttributionFetcher.swift */; }; 37E35C8515C5E2D01B0AF5C1 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E3507939634ED5A9280544 /* Strings.swift */; }; 42F1DF385E3C1F9903A07FBF /* ProductsFetcherSK1.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFB3CBAA73855779FE828CE2 /* ProductsFetcherSK1.swift */; }; + 4FA696BD2A0020A000D228B1 /* MainThreadMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA696BC2A0020A000D228B1 /* MainThreadMonitor.swift */; }; 57032ABF28C13CE4004FF47A /* StoreKit2SettingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57032ABE28C13CE4004FF47A /* StoreKit2SettingTests.swift */; }; 57045B3829C514A8001A5417 /* ProductEntitlementMappingDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57045B3729C514A8001A5417 /* ProductEntitlementMappingDecodingTests.swift */; }; 57045B3A29C51751001A5417 /* GetProductEntitlementMappingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57045B3929C51751001A5417 /* GetProductEntitlementMappingOperation.swift */; }; @@ -857,6 +858,7 @@ 37E35F783903362B65FB7AF3 /* MockProductsRequestFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockProductsRequestFactory.swift; sourceTree = ""; }; 37E35FDA0A44EA03EA12DAA2 /* DateFormatter+ExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DateFormatter+ExtensionsTests.swift"; sourceTree = ""; }; 4FA696A329FC43C600D228B1 /* ReceiptParserTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ReceiptParserTests-Info.plist"; sourceTree = ""; }; + 4FA696BC2A0020A000D228B1 /* MainThreadMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainThreadMonitor.swift; sourceTree = ""; }; 57032ABE28C13CE4004FF47A /* StoreKit2SettingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2SettingTests.swift; sourceTree = ""; }; 57045B3729C514A8001A5417 /* ProductEntitlementMappingDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductEntitlementMappingDecodingTests.swift; sourceTree = ""; }; 57045B3929C51751001A5417 /* GetProductEntitlementMappingOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetProductEntitlementMappingOperation.swift; sourceTree = ""; }; @@ -1680,6 +1682,7 @@ 579189FC28F4966500BF4963 /* OtherIntegrationTests.swift */, 57488AFE29CA58050000EE7E /* LoadShedderIntegrationTests.swift */, 579234E427F779FE00B39C68 /* SubscriberAttributesManagerIntegrationTests.swift */, + 4FA696BC2A0020A000D228B1 /* MainThreadMonitor.swift */, 2CD2C541278CE0E0005D1CC2 /* RevenueCat_IntegrationPurchaseTesterConfiguration.storekit */, 2DE61A83264190830021CEA0 /* Constants.swift */, 2DE20B70264087FB004C597D /* Info.plist */, @@ -3335,6 +3338,7 @@ 2D3BFAD426DEA49200370B11 /* SKProductSubscriptionDurationExtensions.swift in Sources */, 579234E327F7788900B39C68 /* BaseBackendIntegrationTests.swift in Sources */, 2DE20B6F264087FB004C597D /* StoreKitIntegrationTests.swift in Sources */, + 4FA696BD2A0020A000D228B1 /* MainThreadMonitor.swift in Sources */, 2D3BFAD126DEA45C00370B11 /* MockSK1Product.swift in Sources */, 57DD426E2926B9A50026DF09 /* StoreKitTestHelpers.swift in Sources */, 57C2932A28BFF89D0054EDFC /* ErrorMatcher.swift in Sources */, diff --git a/Tests/BackendIntegrationTests/BaseBackendIntegrationTests.swift b/Tests/BackendIntegrationTests/BaseBackendIntegrationTests.swift index 17f761c81c..396e10740b 100644 --- a/Tests/BackendIntegrationTests/BaseBackendIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/BaseBackendIntegrationTests.swift @@ -34,6 +34,8 @@ class BaseBackendIntegrationTests: XCTestCase { // swiftlint:disable:next weak_delegate private(set) var purchasesDelegate: TestPurchaseDelegate! + private var mainThreadMonitor: MainThreadMonitor! + class var storeKit2Setting: StoreKit2Setting { return .default } class var observerMode: Bool { return false } class var responseVerificationMode: Signing.ResponseVerificationMode { @@ -56,6 +58,9 @@ class BaseBackendIntegrationTests: XCTestCase { throw ErrorUtils.configurationError(message: "Must set configuration in `Constants.swift`") } + self.mainThreadMonitor = .init() + self.mainThreadMonitor.run() + if #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) { // Despite calling `SKTestSession.clearTransactions` tests sometimes // begin with leftover transactions. This ensures that we remove them @@ -75,6 +80,12 @@ class BaseBackendIntegrationTests: XCTestCase { await self.waitForAnonymousUser() } + override func tearDown() { + super.tearDown() + + self.mainThreadMonitor = nil + } + // MARK: - Configuration var apiKey: String { return Constants.apiKey } diff --git a/Tests/BackendIntegrationTests/MainThreadMonitor.swift b/Tests/BackendIntegrationTests/MainThreadMonitor.swift new file mode 100644 index 0000000000..7e057d789f --- /dev/null +++ b/Tests/BackendIntegrationTests/MainThreadMonitor.swift @@ -0,0 +1,52 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// MainThreadMonitor.swift +// +// Created by Nacho Soto on 5/1/23. + +import Foundation +@testable import RevenueCat +import XCTest + +final class MainThreadMonitor { + + private let queue: DispatchQueue + + init() { + self.queue = .init(label: "com.revenuecat.MainThreadMonitor") + Logger.verbose("Initializing \(type(of: self)) with a threshold of \(Self.threshold.seconds) seconds") + } + + deinit { + Logger.verbose("Stopping \(type(of: self))") + } + + func run() { + self.queue.async { [weak self] in + while self != nil { + let semaphore = DispatchSemaphore(value: 0) + DispatchQueue.main.async { + semaphore.signal() + } + + let deadline = DispatchTime.now() + Self.threshold + let result = semaphore.wait(timeout: deadline) + + precondition( + result != .timedOut, + "Main thread was blocked for more than \(Self.threshold.seconds) seconds" + ) + } + } + } + + private static let threshold: DispatchTimeInterval = .seconds(1) + +}