From 66399224941141941a2cc5847202b2f167764e57 Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 31 Jul 2024 15:21:47 -0700 Subject: [PATCH 1/9] [tests] add OneSignalOSCoreMocks module * Add new framework called `OneSignalOSCoreMocks ` to access the Operation Repo and reset it between tests. * Part of the cause for flaky tests is state carrying over in the Operation Repo singleton. For example, Deltas that have not been flushed yet from the preceding test are flushed in the next one. --- .../OneSignal.xcodeproj/project.pbxproj | 315 ++++++++++++++++++ .../Source/OSOperationRepo.swift | 2 +- .../OneSignalOSCoreMocks/OSCoreMocks.swift | 50 +++ .../OneSignalOSCoreMocks.h | 38 +++ .../OneSignalUserMocks.swift | 4 +- 5 files changed, 406 insertions(+), 3 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/OSCoreMocks.swift create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/OneSignalOSCoreMocks.h diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 56c2f8466..ea3a1245c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -96,6 +96,12 @@ 3C7A39C12B7BED900082665E /* OneSignalCoreMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */; }; 3C7A39C22B7BED900082665E /* OneSignalCoreMocks.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3C7A39DC2B7C1C580082665E /* UNUserNotificationCenterOverrider.m in Sources */ = {isa = PBXBuildFile; fileRef = 4529DEE61FA82CDC00CEAB1D /* UNUserNotificationCenterOverrider.m */; }; + 3C8544B92C5AEFF700F542A9 /* OneSignalOSCoreMocks.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C8544B82C5AEFF700F542A9 /* OneSignalOSCoreMocks.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3C8544BC2C5AEFF700F542A9 /* OneSignalOSCoreMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C8544B62C5AEFF600F542A9 /* OneSignalOSCoreMocks.framework */; }; + 3C8544BD2C5AEFF700F542A9 /* OneSignalOSCoreMocks.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3C8544B62C5AEFF600F542A9 /* OneSignalOSCoreMocks.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3C8544C32C5AF18B00F542A9 /* OSCoreMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C8544C22C5AF18B00F542A9 /* OSCoreMocks.swift */; }; + 3C8544C42C5AF2E900F542A9 /* OneSignalOSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C115161289A259500565C41 /* OneSignalOSCore.framework */; }; + 3C8544C52C5AF2E900F542A9 /* OneSignalOSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3C115161289A259500565C41 /* OneSignalOSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3C87066D2BDE05B8000D8CD2 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C7A39D42B7C18EE0082665E /* XCTest.framework */; }; 3C87066E2BDE05B8000D8CD2 /* XCTest.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3C7A39D42B7C18EE0082665E /* XCTest.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3C8706702BDE0957000D8CD2 /* MockUserRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C87066F2BDE0957000D8CD2 /* MockUserRequests.swift */; }; @@ -638,6 +644,20 @@ remoteGlobalIDString = DEF5CCF02539321A0003E9CC; remoteInfo = UnitTestApp; }; + 3C8544BA2C5AEFF700F542A9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 37747F8B19147D6400558FAD /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3C8544B52C5AEFF600F542A9; + remoteInfo = OneSignalOSCoreMocks; + }; + 3C8544C62C5AF2E900F542A9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 37747F8B19147D6400558FAD /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3C115160289A259500565C41; + remoteInfo = OneSignalOSCore; + }; 3C9AD6D82B22A8DF00BC1540 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 37747F8B19147D6400558FAD /* Project object */; @@ -1063,6 +1083,17 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + 3C8544C82C5AF2E900F542A9 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 3C8544C52C5AF2E900F542A9 /* OneSignalOSCore.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; 3C9AD6DA2B22A8DF00BC1540 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -1141,6 +1172,7 @@ DEA4B4662888C59E00E9FE12 /* OneSignalExtension.framework in Embed Frameworks */, DEBAAE2F2A4211DA00BF2C1C /* OneSignalInAppMessages.framework in Embed Frameworks */, 475F47252B8E398E00EC05B3 /* OneSignalLiveActivities.framework in Embed Frameworks */, + 3C8544BD2C5AEFF700F542A9 /* OneSignalOSCoreMocks.framework in Embed Frameworks */, DEA4B4632888C4DC00E9FE12 /* OneSignalOutcomes.framework in Embed Frameworks */, 3CEE934B2B7C73B6008440BD /* OneSignalUserMocks.framework in Embed Frameworks */, DEA4B45D2888C1D000E9FE12 /* OneSignalCore.framework in Embed Frameworks */, @@ -1206,6 +1238,9 @@ 3C6299AA2BEEA4C000649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchUserIntegrationTests.swift; sourceTree = ""; }; 3C7A39D42B7C18EE0082665E /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 3C8544B62C5AEFF600F542A9 /* OneSignalOSCoreMocks.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OneSignalOSCoreMocks.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3C8544B82C5AEFF700F542A9 /* OneSignalOSCoreMocks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalOSCoreMocks.h; sourceTree = ""; }; + 3C8544C22C5AF18B00F542A9 /* OSCoreMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCoreMocks.swift; sourceTree = ""; }; 3C87066F2BDE0957000D8CD2 /* MockUserRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserRequests.swift; sourceTree = ""; }; 3C8706712BDEE076000D8CD2 /* MockUserDefines.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserDefines.swift; sourceTree = ""; }; 3C8706752BDEED75000D8CD2 /* NSDictionary+UnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSDictionary+UnitTests.swift"; sourceTree = ""; }; @@ -1647,6 +1682,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 3C8544B32C5AEFF600F542A9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C8544C42C5AF2E900F542A9 /* OneSignalOSCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3CC063972B6D7A8C002BB07F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1838,6 +1881,7 @@ DEA4B4652888C59100E9FE12 /* OneSignalExtension.framework in Frameworks */, DEF7842F2912DEBA00A1F3A5 /* OneSignalNotifications.framework in Frameworks */, 3CEE93462B7C73AB008440BD /* OneSignalCoreMocks.framework in Frameworks */, + 3C8544BC2C5AEFF700F542A9 /* OneSignalOSCoreMocks.framework in Frameworks */, DEA4B4622888C4D500E9FE12 /* OneSignalOutcomes.framework in Frameworks */, DEA4B45C2888C1D000E9FE12 /* OneSignalCore.framework in Frameworks */, 475F47242B8E398E00EC05B3 /* OneSignalLiveActivities.framework in Frameworks */, @@ -1912,6 +1956,7 @@ DEBAAE292A4211DA00BF2C1C /* OneSignalInAppMessages */, 475F471F2B8E398E00EC05B3 /* OneSignalLiveActivities */, 3CC0639B2B6D7A8D002BB07F /* OneSignalCoreMocks */, + 3C8544B72C5AEFF700F542A9 /* OneSignalOSCoreMocks */, 3CC063DE2B6D7F2A002BB07F /* OneSignalUserMocks */, 3CC063A52B6D7A8E002BB07F /* OneSignalCoreTests */, 3CC063EC2B6D7FE8002BB07F /* OneSignalUserTests */, @@ -1944,6 +1989,7 @@ 4735424A2B8F93330016DB4C /* OneSignalLiveActivitiesTests.xctest */, DEBA2A1A2C20E35E00E234DB /* OneSignalNotificationsTests.xctest */, 3C01518E2C2E298E0079E076 /* OneSignalInAppMessagesTests.xctest */, + 3C8544B62C5AEFF600F542A9 /* OneSignalOSCoreMocks.framework */, ); name = Products; sourceTree = ""; @@ -2015,6 +2061,15 @@ path = Source; sourceTree = ""; }; + 3C8544B72C5AEFF700F542A9 /* OneSignalOSCoreMocks */ = { + isa = PBXGroup; + children = ( + 3C8544B82C5AEFF700F542A9 /* OneSignalOSCoreMocks.h */, + 3C8544C22C5AF18B00F542A9 /* OSCoreMocks.swift */, + ); + path = OneSignalOSCoreMocks; + sourceTree = ""; + }; 3C8706742BDEED53000D8CD2 /* Extensions */ = { isa = PBXGroup; children = ( @@ -2899,6 +2954,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 3C8544B12C5AEFF600F542A9 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C8544B92C5AEFF700F542A9 /* OneSignalOSCoreMocks.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3CC063952B6D7A8C002BB07F /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -3152,6 +3215,26 @@ productReference = 3C115161289A259500565C41 /* OneSignalOSCore.framework */; productType = "com.apple.product-type.framework"; }; + 3C8544B52C5AEFF600F542A9 /* OneSignalOSCoreMocks */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3C8544C12C5AEFF800F542A9 /* Build configuration list for PBXNativeTarget "OneSignalOSCoreMocks" */; + buildPhases = ( + 3C8544B12C5AEFF600F542A9 /* Headers */, + 3C8544B22C5AEFF600F542A9 /* Sources */, + 3C8544B32C5AEFF600F542A9 /* Frameworks */, + 3C8544B42C5AEFF600F542A9 /* Resources */, + 3C8544C82C5AF2E900F542A9 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 3C8544C72C5AF2E900F542A9 /* PBXTargetDependency */, + ); + name = OneSignalOSCoreMocks; + productName = OneSignalOSCoreMocks; + productReference = 3C8544B62C5AEFF600F542A9 /* OneSignalOSCoreMocks.framework */; + productType = "com.apple.product-type.framework"; + }; 3CC063992B6D7A8C002BB07F /* OneSignalCoreMocks */ = { isa = PBXNativeTarget; buildConfigurationList = 3CC063AF2B6D7A8E002BB07F /* Build configuration list for PBXNativeTarget "OneSignalCoreMocks" */; @@ -3493,6 +3576,7 @@ 3CEE93492B7C73AB008440BD /* PBXTargetDependency */, 3CEE934D2B7C73B6008440BD /* PBXTargetDependency */, 475F47232B8E398E00EC05B3 /* PBXTargetDependency */, + 3C8544BB2C5AEFF700F542A9 /* PBXTargetDependency */, ); name = UnitTestApp; productName = UnitTestApp; @@ -3543,6 +3627,10 @@ DevelopmentTeam = 99SW8E36CT; ProvisioningStyle = Automatic; }; + 3C8544B52C5AEFF600F542A9 = { + CreatedOnToolsVersion = 15.2; + LastSwiftMigration = 1520; + }; 3CC063992B6D7A8C002BB07F = { CreatedOnToolsVersion = 15.2; DevelopmentTeam = 99SW8E36CT; @@ -3686,6 +3774,7 @@ DEBAAE272A4211D900BF2C1C /* OneSignalInAppMessages */, 475F471D2B8E398D00EC05B3 /* OneSignalLiveActivities */, 3CC063992B6D7A8C002BB07F /* OneSignalCoreMocks */, + 3C8544B52C5AEFF600F542A9 /* OneSignalOSCoreMocks */, 3CC063DC2B6D7F2A002BB07F /* OneSignalUserMocks */, 3CC063A02B6D7A8D002BB07F /* OneSignalCoreTests */, 3CC063EA2B6D7FE8002BB07F /* OneSignalUserTests */, @@ -3712,6 +3801,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 3C8544B42C5AEFF600F542A9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3CC063982B6D7A8C002BB07F /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -3946,6 +4042,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 3C8544B22C5AEFF600F542A9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C8544C32C5AF18B00F542A9 /* OSCoreMocks.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3CC063962B6D7A8C002BB07F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -4347,6 +4451,16 @@ target = DEF5CCF02539321A0003E9CC /* UnitTestApp */; targetProxy = 3C7A39DF2B7C1C8B0082665E /* PBXContainerItemProxy */; }; + 3C8544BB2C5AEFF700F542A9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3C8544B52C5AEFF600F542A9 /* OneSignalOSCoreMocks */; + targetProxy = 3C8544BA2C5AEFF700F542A9 /* PBXContainerItemProxy */; + }; + 3C8544C72C5AF2E900F542A9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3C115160289A259500565C41 /* OneSignalOSCore */; + targetProxy = 3C8544C62C5AF2E900F542A9 /* PBXContainerItemProxy */; + }; 3C9AD6D92B22A8DF00BC1540 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DEBAADF82A420A3700BF2C1C /* OneSignalLocation */; @@ -4928,6 +5042,197 @@ }; name = Debug; }; + 3C8544BE2C5AEFF800F542A9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 99SW8E36CT; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Hiptic. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.OneSignalOSCoreMocks; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 3C8544BF2C5AEFF800F542A9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 99SW8E36CT; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Hiptic. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.OneSignalOSCoreMocks; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 3C8544C02C5AEFF800F542A9 /* Test */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 99SW8E36CT; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Hiptic. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.OneSignalOSCoreMocks; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Test; + }; 3CC063A92B6D7A8E002BB07F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -8260,6 +8565,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 3C8544C12C5AEFF800F542A9 /* Build configuration list for PBXNativeTarget "OneSignalOSCoreMocks" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3C8544BE2C5AEFF800F542A9 /* Release */, + 3C8544BF2C5AEFF800F542A9 /* Debug */, + 3C8544C02C5AEFF800F542A9 /* Test */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 3CC063AF2B6D7A8E002BB07F /* Build configuration list for PBXNativeTarget "OneSignalCoreMocks" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift index dd81de171..338fadd50 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift @@ -42,7 +42,7 @@ public class OSOperationRepo: NSObject { // Maps delta names to the interfaces for the operation executors var deltasToExecutorMap: [String: OSOperationExecutor] = [:] var executors: [OSOperationExecutor] = [] - private var deltaQueue: [OSDelta] = [] + var deltaQueue: [OSDelta] = [] // non-private for unit test access // TODO: This could come from a config, plist, method, remote params var pollIntervalMilliseconds = Int(POLL_INTERVAL_MS) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/OSCoreMocks.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/OSCoreMocks.swift new file mode 100644 index 000000000..6a144eef7 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/OSCoreMocks.swift @@ -0,0 +1,50 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import Foundation +import OneSignalCore +@testable import OneSignalOSCore + +@objc +public class OSCoreMocks: NSObject { + public static func resetOperationRepo() { + OSOperationRepo.sharedInstance.reset() + } +} + +extension OSOperationRepo { + /** + The Operation Repo needs to reset between tests until we dependency inject the Operation Repo, + to prevent state from carrying over between tests. + */ + func reset() { + deltaQueue.removeAll() + executors.removeAll() + deltasToExecutorMap.removeAll() + paused = false + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/OneSignalOSCoreMocks.h b/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/OneSignalOSCoreMocks.h new file mode 100644 index 000000000..0c6e6d949 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/OneSignalOSCoreMocks.h @@ -0,0 +1,38 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +#import + +//! Project version number for OneSignalOSCoreMocks. +FOUNDATION_EXPORT double OneSignalOSCoreMocksVersionNumber; + +//! Project version string for OneSignalOSCoreMocks. +FOUNDATION_EXPORT const unsigned char OneSignalOSCoreMocksVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift index 5e7b48f4b..3230ac5dc 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift @@ -27,6 +27,7 @@ import Foundation import OneSignalOSCore +import OneSignalOSCoreMocks @testable import OneSignalUser @objc @@ -35,8 +36,7 @@ public class OneSignalUserMocks: NSObject { // TODO: create mocked server responses to user requests @objc public static func reset() { - // TODO: Reset Operation Repo first - // OSCoreMocks.resetOperationRepo() + OSCoreMocks.resetOperationRepo() OneSignalUserManagerImpl.sharedInstance.reset() } } From a7bf23036f7518a6ae1c5ac52817922ed826bb89 Mon Sep 17 00:00:00 2001 From: Nan Date: Mon, 24 Jun 2024 09:59:34 -0700 Subject: [PATCH 2/9] [flaky tests] Live Activities needs to wait for user setup * These tests check the mock client's number of requests executed, but it was counting the user setup request as well (such as Fetch Identity By Subscription). Add a waiting period for this before creating the Live Activities requests) * An alternative is to set the user and push subscription ID manually without triggering a request to be made. --- .../OSLiveActivitiesExecutorTests.swift | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift index 19c6c4852..1bab00084 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift @@ -56,6 +56,8 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { OneSignalCoreImpl.setSharedClient(mockClient) OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id") OneSignalUserManagerImpl.sharedInstance.start() + // Wait for any user setup requests to complete + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) mockClient.reset() let request = OSRequestSetStartToken(key: "my-activity-type", token: "my-token") @@ -81,6 +83,8 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { OneSignalCoreImpl.setSharedClient(mockClient) OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id") OneSignalUserManagerImpl.sharedInstance.start() + // Wait for any user setup requests to complete + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) mockClient.reset() let request = OSRequestRemoveStartToken(key: "my-activity-type") @@ -104,6 +108,8 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { OneSignalCoreImpl.setSharedClient(mockClient) OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id") OneSignalUserManagerImpl.sharedInstance.start() + // Wait for any user setup requests to complete + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) mockClient.reset() let request = OSRequestSetUpdateToken(key: "my-activity-id", token: "my-token") @@ -129,6 +135,8 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { OneSignalCoreImpl.setSharedClient(mockClient) OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id") OneSignalUserManagerImpl.sharedInstance.start() + // Wait for any user setup requests to complete + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) mockClient.reset() let request = OSRequestRemoveStartToken(key: "my-activity-id") @@ -171,6 +179,8 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { OneSignalCoreImpl.setSharedClient(mockClient) OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id") OneSignalUserManagerImpl.sharedInstance.start() + // Wait for any user setup requests to complete + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) mockClient.reset() let request = OSRequestSetStartToken(key: "my-activity-type", token: "my-token") @@ -196,6 +206,8 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { OneSignalCoreImpl.setSharedClient(mockClient) OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id") OneSignalUserManagerImpl.sharedInstance.start() + // Wait for any user setup requests to complete + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) mockClient.reset() let request = OSRequestSetStartToken(key: "my-activity-type", token: "my-token") @@ -219,6 +231,8 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { OneSignalCoreImpl.setSharedClient(mockClient) OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id") OneSignalUserManagerImpl.sharedInstance.start() + // Wait for any user setup requests to complete + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) mockClient.reset() let request1 = OSRequestSetStartToken(key: "my-activity-type", token: "my-token") @@ -246,6 +260,8 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { OneSignalCoreImpl.setSharedClient(mockClient) OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id") OneSignalUserManagerImpl.sharedInstance.start() + // Wait for any user setup requests to complete + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) mockClient.reset() let request1 = OSRequestSetStartToken(key: "my-activity-type", token: "my-token-1") @@ -274,6 +290,8 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { OneSignalCoreImpl.setSharedClient(mockClient) OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id") OneSignalUserManagerImpl.sharedInstance.start() + // Wait for any user setup requests to complete + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) mockClient.reset() let request1 = OSRequestSetStartToken(key: "my-activity-type", token: "my-token-1") @@ -301,6 +319,8 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { OneSignalCoreImpl.setSharedClient(mockClient) OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id") OneSignalUserManagerImpl.sharedInstance.start() + // Wait for any user setup requests to complete + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) mockClient.reset() let request1 = OSRequestSetUpdateToken(key: "my-activity-id", token: "my-token") @@ -328,6 +348,8 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { OneSignalCoreImpl.setSharedClient(mockClient) OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id") OneSignalUserManagerImpl.sharedInstance.start() + // Wait for any user setup requests to complete + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) mockClient.reset() let request1 = OSRequestSetUpdateToken(key: "my-activity-id", token: "my-token-1") @@ -356,6 +378,8 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { OneSignalCoreImpl.setSharedClient(mockClient) OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id") OneSignalUserManagerImpl.sharedInstance.start() + // Wait for any user setup requests to complete + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) mockClient.reset() let request1 = OSRequestSetUpdateToken(key: "my-activity-id", token: "my-token-1") From 1a947c15809f437e0f559d33735ecd7a9748b85f Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 25 Jun 2024 09:39:24 -0700 Subject: [PATCH 3/9] [flaky tests] move reset code into before tests * Instead of resetting after tests, reset before tests for cleanest starting state --- .../OSLiveActivitiesExecutorTests.swift | 9 ++++----- .../OneSignalUserTests/OneSignalUserObjcTests.m | 9 ++++----- .../OneSignalUserTests/OneSignalUserTests.swift | 9 ++++----- .../OneSignalUserTests/SwitchUserIntegrationTests.swift | 9 ++++----- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift index 1bab00084..9694d2667 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift @@ -37,17 +37,16 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { override func setUpWithError() throws { // TODO: Something like the existing [UnitTestCommonMethods beforeEachTest:self]; + // TODO: Need to clear all data between tests for user manager, models, etc. + OneSignalCoreMocks.clearUserDefaults() + OneSignalUserMocks.reset() // App ID is set because User Manager has guards against nil App ID OneSignalConfigManager.setAppId("test-app-id") // Temp. logging to help debug during testing OneSignalLog.setLogLevel(.LL_VERBOSE) } - override func tearDownWithError() throws { - // TODO: Need to clear all data between tests for user manager, models, etc. - OneSignalCoreMocks.clearUserDefaults() - OneSignalUserMocks.reset() - } + override func tearDownWithError() throws { } func testAppendSetStartTokenWithSuccessfulRequest() throws { /* Setup */ diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m index 5d52fd7fe..b367edbea 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m @@ -13,17 +13,16 @@ @implementation OneSignalUserObjcTests - (void)setUp { // TODO: Something like the existing [UnitTestCommonMethods beforeEachTest:self]; + // TODO: Need to clear all data between tests for client, user manager, models, etc. + [OneSignalCoreMocks clearUserDefaults]; + [OneSignalUserMocks reset]; // App ID is set because User Manager has guards against nil App ID [OneSignalConfigManager setAppId:@"test-app-id"]; // Temp. logging to help debug during testing [OneSignalLog setLogLevel:ONE_S_LL_VERBOSE]; } -- (void)tearDown { - // TODO: Need to clear all data between tests for client, user manager, models, etc. - [OneSignalCoreMocks clearUserDefaults]; - [OneSignalUserMocks reset]; -} +- (void)tearDown { } /** Tests passing purchase data to the User Manager to process and send. diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift index 4ec494e70..b72f2dd14 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift @@ -37,17 +37,16 @@ final class OneSignalUserTests: XCTestCase { override func setUpWithError() throws { // TODO: Something like the existing [UnitTestCommonMethods beforeEachTest:self]; + // TODO: Need to clear all data between tests for client, user manager, models, etc. + OneSignalCoreMocks.clearUserDefaults() + OneSignalUserMocks.reset() // App ID is set because User Manager has guards against nil App ID OneSignalConfigManager.setAppId("test-app-id") // Temp. logging to help debug during testing OneSignalLog.setLogLevel(.LL_VERBOSE) } - override func tearDownWithError() throws { - // TODO: Need to clear all data between tests for client, user manager, models, etc. - OneSignalCoreMocks.clearUserDefaults() - OneSignalUserMocks.reset() - } + override func tearDownWithError() throws { } // Comparable to Android test: "externalId is backed by the identity model" func testLoginSetsExternalId() throws { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift index f4ef0a632..39651e8b9 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift @@ -8,17 +8,16 @@ final class SwitchUserIntegrationTests: XCTestCase { override func setUpWithError() throws { // TODO: Something like the existing [UnitTestCommonMethods beforeEachTest:self]; + // TODO: Need to clear all data between tests for client, user manager, models, etc. + OneSignalCoreMocks.clearUserDefaults() + OneSignalUserMocks.reset() // App ID is set because User Manager has guards against nil App ID OneSignalConfigManager.setAppId("test-app-id") // Temp. logging to help debug during testing OneSignalLog.setLogLevel(.LL_VERBOSE) } - override func tearDownWithError() throws { - // TODO: Need to clear all data between tests for client, user manager, models, etc. - OneSignalCoreMocks.clearUserDefaults() - OneSignalUserMocks.reset() - } + override func tearDownWithError() throws { } func testIdentifyUserSuccessfully_thenLogin_sendsCorrectTags() throws { /* Setup */ From 156e3a4a311a3e1affa98b09f141dc05aee7ee2a Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 25 Jun 2024 10:47:45 -0700 Subject: [PATCH 4/9] [flaky tests] Testing batched operations need to wait * Currently the Operation Repo is a singleton and shared between tests, so a pending flush could be on the DispatchQueue from a previous test and it will flush unexpectedly. * This test tests combining operations and intentionally increases the `pollIntervalMilliseconds` to `300`, but a previous flush could interrupt this. --- .../OneSignalUserTests/OneSignalUserTests.swift | 5 ++++- .../OneSignalUserTests/SwitchUserIntegrationTests.swift | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift index b72f2dd14..0ff62f36c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift @@ -312,6 +312,9 @@ final class OneSignalUserTests: XCTestCase { // Increase flush interval to allow all the updates to batch OSOperationRepo.sharedInstance.pollIntervalMilliseconds = 300 + // Wait to let any pending flushes in the Operation Repo to run + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.1) + /* When */ OneSignalUserManagerImpl.sharedInstance.sendSessionTime(100) @@ -349,7 +352,7 @@ final class OneSignalUserTests: XCTestCase { /* Then */ - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 1) let expectedPayload: [String: Any] = [ "deltas": [ diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift index 39651e8b9..55ca2873f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift @@ -2,6 +2,7 @@ import XCTest import OneSignalCore import OneSignalCoreMocks import OneSignalUserMocks +@testable import OneSignalOSCore @testable import OneSignalUser final class SwitchUserIntegrationTests: XCTestCase { @@ -189,6 +190,7 @@ final class SwitchUserIntegrationTests: XCTestCase { */ func testAnonUser_thenIdentifyUserWithConflict_thenLogout_sendsCorrectUpdatesWithNoFetch() throws { /* Setup */ + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) let client = MockOneSignalClient() OneSignalCoreImpl.setSharedClient(client) @@ -316,6 +318,11 @@ final class SwitchUserIntegrationTests: XCTestCase { let client = MockOneSignalClient() OneSignalCoreImpl.setSharedClient(client) + // Increase flush interval to allow all the updates to batch + OSOperationRepo.sharedInstance.pollIntervalMilliseconds = 300 + // Wait to let any pending flushes in the Operation Repo to run + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.1) + // 1. Set up mock responses for the first anonymous user let tagsUserAnon = ["tag_anon": "value_anon"] MockUserRequests.setDefaultCreateAnonUserResponses(with: client) From 764eb713d1cd3f8165acfb6e1f9f27fe8ca0bf77 Mon Sep 17 00:00:00 2001 From: Nan Date: Thu, 1 Aug 2024 10:23:20 -0700 Subject: [PATCH 5/9] [tests] Move concurrency tests to new file --- .../OneSignal.xcodeproj/project.pbxproj | 4 + .../OneSignalUserTests.swift | 231 -------------- .../UserConcurrencyTests.swift | 281 ++++++++++++++++++ 3 files changed, 285 insertions(+), 231 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index ea3a1245c..319e96575 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -145,6 +145,7 @@ 3CC063E62B6D7F96002BB07F /* OneSignalUserMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC063E52B6D7F96002BB07F /* OneSignalUserMocks.swift */; }; 3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */; }; 3CC063EF2B6D7FE8002BB07F /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; }; + 3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */; }; 3CC9A6342AFA1FDE008F68FD /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3CC9A6332AFA1FDD008F68FD /* PrivacyInfo.xcprivacy */; }; 3CC9A6362AFA26E7008F68FD /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3CC9A6352AFA26E7008F68FD /* PrivacyInfo.xcprivacy */; }; 3CCF44BE299B17290021964D /* OneSignalWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 3CCF44BC299B17290021964D /* OneSignalWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1272,6 +1273,7 @@ 3CC063E52B6D7F96002BB07F /* OneSignalUserMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalUserMocks.swift; sourceTree = ""; }; 3CC063EB2B6D7FE8002BB07F /* OneSignalUserTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OneSignalUserTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalUserTests.swift; sourceTree = ""; }; + 3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConcurrencyTests.swift; sourceTree = ""; }; 3CC9A6332AFA1FDD008F68FD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 3CC9A6352AFA26E7008F68FD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 3CCF44BC299B17290021964D /* OneSignalWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalWrapper.h; sourceTree = ""; }; @@ -2147,6 +2149,7 @@ children = ( 3CDE664A2BFC2A55006DA114 /* OneSignalUserTests-Bridging-Header.h */, 3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */, + 3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */, 3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */, 3CDE664B2BFC2A56006DA114 /* OneSignalUserObjcTests.m */, ); @@ -4087,6 +4090,7 @@ files = ( 3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */, 3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */, + 3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */, 3CDE664C2BFC2A56006DA114 /* OneSignalUserObjcTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift index 0ff62f36c..3a4bf1745 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift @@ -64,237 +64,6 @@ final class OneSignalUserTests: XCTestCase { XCTAssertEqual(userInstanceExternalId, "my-external-id") } - /** - This test reproduces a crash in the Operation Repo's flushing delta queue. - It is possible for two threads to flush concurrently. - However, this test does not crash 100% of the time. - */ - func testOperationRepoFlushingConcurrency() throws { - /* Setup */ - OneSignalCoreImpl.setSharedClient(MockOneSignalClient()) - - /* When */ - - // 1. Enqueue 10 Deltas to the Operation Repo - for num in 0...9 { - OneSignalUserManagerImpl.sharedInstance.addTag(key: "tag\(num)", value: "value") - } - - // 2. Flush the delta queue from 4 multiple threads - for _ in 1...4 { - DispatchQueue.global().async { - print("🧪 flushDeltaQueue on thread \(Thread.current)") - OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() - } - } - - /* Then */ - // There are two places that can crash, as multiple threads are manipulating arrays: - // 1. OpRepo: `deltaQueue.remove(at: index)` index out of bounds - // 2. OSPropertyOperationExecutor: `deltaQueue.append(delta)` EXC_BAD_ACCESS - } - - /** - This test reproduces a crash in the Subscription Executor. - It is possible for two threads to modify and cache queues concurrently. - */ - func testSubscriptionExecutorConcurrency() throws { - /* Setup */ - - let client = MockOneSignalClient() - client.setMockResponseForRequest( - request: "", - response: [:] - ) - OneSignalCoreImpl.setSharedClient(client) - - let executor = OSSubscriptionOperationExecutor() - OSOperationRepo.sharedInstance.addExecutor(executor) - - /* When */ - - DispatchQueue.concurrentPerform(iterations: 50) { _ in - // 1. Enqueue Remove Subscription Deltas to the Operation Repo - OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: UUID().uuidString, model: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "email", value: "email")) - OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: UUID().uuidString, model: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "email", value: "email")) - - // 2. Flush Operation Repo - OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() - - // 3. Simulate updating the executor's request queue from a network response - executor.executeDeleteSubscriptionRequest(OSRequestDeleteSubscription(subscriptionModel: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer())), inBackground: false) - executor.executeDeleteSubscriptionRequest(OSRequestDeleteSubscription(subscriptionModel: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer())), inBackground: false) - } - - // 4. Run background threads - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - - /* Then */ - // Previously caused crash: signal SIGABRT - malloc: double free for ptr - // Assert that every request SDK makes has a response set, and is handled - XCTAssertTrue(client.allRequestsHandled) - } - - /** - This test reproduces a crash in the Identity Executor. - It is possible for two threads to modify and cache queues concurrently. - */ - func testIdentityExecutorConcurrency() throws { - /* Setup */ - let client = MockOneSignalClient() - let aliases = [UUID().uuidString: "id"] - - OneSignalCoreImpl.setSharedClient(client) - MockUserRequests.setAddAliasesResponse(with: client, aliases: aliases) - - let executor = OSIdentityOperationExecutor() - OSOperationRepo.sharedInstance.addExecutor(executor) - - /* When */ - - DispatchQueue.concurrentPerform(iterations: 50) { _ in - // 1. Enqueue Add Alias Deltas to the Operation Repo - OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: UUID().uuidString, model: OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()), property: "aliases", value: aliases)) - OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: UUID().uuidString, model: OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()), property: "aliases", value: aliases)) - - // 2. Flush Operation Repo - OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() - - // 3. Simulate updating the executor's request queue from a network response - executor.executeAddAliasesRequest(OSRequestAddAliases(aliases: aliases, identityModel: OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer())), inBackground: false) - executor.executeAddAliasesRequest(OSRequestAddAliases(aliases: aliases, identityModel: OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer())), inBackground: false) - } - - // 4. Run background threads - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - - /* Then */ - // Previously caused crash: signal SIGABRT - malloc: double free for ptr - // Assert that every request SDK makes has a response set, and is handled - XCTAssertTrue(client.allRequestsHandled) - } - - /** - This test aims to ensure concurrency safety in the Property Executor. - It is possible for two threads to modify and cache queues concurrently. - */ - func testPropertyExecutorConcurrency() throws { - /* Setup */ - let client = MockOneSignalClient() - // Ensure all requests fire the executor's callback so it will modify queues and cache it - client.fireSuccessForAllRequests = true - OneSignalCoreImpl.setSharedClient(client) - - let identityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()) - OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(identityModel) - - let executor = OSPropertyOperationExecutor() - OSOperationRepo.sharedInstance.addExecutor(executor) - - /* When */ - - DispatchQueue.concurrentPerform(iterations: 50) { _ in - // 1. Enqueue Deltas to the Operation Repo - OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "language", value: UUID().uuidString)) - OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "language", value: UUID().uuidString)) - - // 2. Flush Operation Repo - OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() - - // 3. Simulate updating the executor's request queue from a network response - executor.executeUpdatePropertiesRequest(OSRequestUpdateProperties(params: ["properties": ["language": UUID().uuidString], "refresh_device_metadata": false], identityModel: identityModel), inBackground: false) - } - - // 4. Run background threads - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - - /* Then */ - // No crash - } - - /** - This test aims to ensure concurrency safety in the User Executor. - It is possible for two threads to modify and cache queues concurrently. - Currently, this executor only allows one request to send at a time, which should prevent concurrent access. - But out of caution and future-proofing, this test is added. - */ - func testUserExecutorConcurrency() throws { - /* Setup */ - - let client = MockOneSignalClient() - // Ensure all requests fire the executor's callback so it will modify queues and cache it - client.fireSuccessForAllRequests = true - OneSignalCoreImpl.setSharedClient(client) - - let identityModel1 = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()) - let identityModel2 = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()) - - let userExecutor = OSUserExecutor() - - /* When */ - - DispatchQueue.concurrentPerform(iterations: 50) { _ in - let identifyRequest = OSRequestIdentifyUser(aliasLabel: OS_EXTERNAL_ID, aliasId: UUID().uuidString, identityModelToIdentify: identityModel1, identityModelToUpdate: identityModel2) - let fetchRequest = OSRequestFetchUser(identityModel: identityModel1, aliasLabel: OS_ONESIGNAL_ID, aliasId: UUID().uuidString, onNewSession: false) - - // Append and execute requests simultaneously - userExecutor.appendToQueue(identifyRequest) - userExecutor.appendToQueue(fetchRequest) - userExecutor.executeIdentifyUserRequest(identifyRequest) - userExecutor.executeFetchUserRequest(fetchRequest) - } - - // Run background threads - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - - /* Then */ - // No crash - } - - /** - This test reproduced a crash when the property model is being encoded. - */ - func testEncodingPropertiesModel_withConcurrency_doesNotCrash() throws { - /* Setup */ - let propertiesModel = OSPropertiesModel(changeNotifier: OSEventProducer()) - - /* When */ - DispatchQueue.concurrentPerform(iterations: 5_000) { i in - // 1. Add tags - for num in 0...9 { - propertiesModel.addTags(["\(i)tag\(num)": "value"]) - } - - // 2. Encode the model - OneSignalUserDefaults.initShared().saveCodeableData(forKey: "PropertyModel", withValue: propertiesModel) - - // 3. Clear the tags - propertiesModel.clearData() - } - } - - /** - This test reproduced a crash when the identity model is being encoded. - */ - func testEncodingIdentityModel_withConcurrency_doesNotCrash() throws { - /* Setup */ - let identityModel = OSIdentityModel(aliases: nil, changeNotifier: OSEventProducer()) - - /* When */ - DispatchQueue.concurrentPerform(iterations: 5_000) { i in - // 1. Add aliases - for num in 0...9 { - identityModel.addAliases(["\(i)alias\(num)": "value"]) - } - - // 2. Encode the model - OneSignalUserDefaults.initShared().saveCodeableData(forKey: "IdentityModel", withValue: identityModel) - - // 2. Clear the aliases - identityModel.clearData() - } - } - /** Tests multiple user updates should be combined and sent together. Multiple session times should be added. diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift new file mode 100644 index 000000000..f624c0e62 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift @@ -0,0 +1,281 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection +with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import XCTest +import OneSignalCore +import OneSignalCoreMocks +import OneSignalUserMocks +// Testable import OSCore to allow setting a different poll flush interval +@testable import OneSignalOSCore +@testable import OneSignalUser + +final class UserConcurrencyTests: XCTestCase { + + override func setUpWithError() throws { + // TODO: Something like the existing [UnitTestCommonMethods beforeEachTest:self]; + // TODO: Need to clear all data between tests for client, user manager, models, etc. + OneSignalCoreMocks.clearUserDefaults() + OneSignalUserMocks.reset() + // App ID is set because User Manager has guards against nil App ID + OneSignalConfigManager.setAppId("test-app-id") + // Temp. logging to help debug during testing + OneSignalLog.setLogLevel(.LL_VERBOSE) + } + + override func tearDownWithError() throws { } + + /** + This test reproduces a crash in the Operation Repo's flushing delta queue. + It is possible for two threads to flush concurrently. + However, this test does not crash 100% of the time. + */ + func testOperationRepoFlushingConcurrency() throws { + /* Setup */ + OneSignalCoreImpl.setSharedClient(MockOneSignalClient()) + + /* When */ + + // 1. Enqueue 10 Deltas to the Operation Repo + for num in 0...9 { + OneSignalUserManagerImpl.sharedInstance.addTag(key: "tag\(num)", value: "value") + } + + // 2. Flush the delta queue from 4 multiple threads + for _ in 1...4 { + DispatchQueue.global().async { + print("🧪 flushDeltaQueue on thread \(Thread.current)") + OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() + } + } + + /* Then */ + // There are two places that can crash, as multiple threads are manipulating arrays: + // 1. OpRepo: `deltaQueue.remove(at: index)` index out of bounds + // 2. OSPropertyOperationExecutor: `deltaQueue.append(delta)` EXC_BAD_ACCESS + } + + /** + This test reproduces a crash in the Subscription Executor. + It is possible for two threads to modify and cache queues concurrently. + */ + func testSubscriptionExecutorConcurrency() throws { + /* Setup */ + + let client = MockOneSignalClient() + client.setMockResponseForRequest( + request: "", + response: [:] + ) + OneSignalCoreImpl.setSharedClient(client) + + let executor = OSSubscriptionOperationExecutor() + OSOperationRepo.sharedInstance.addExecutor(executor) + + /* When */ + + DispatchQueue.concurrentPerform(iterations: 50) { _ in + // 1. Enqueue Remove Subscription Deltas to the Operation Repo + OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: UUID().uuidString, model: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "email", value: "email")) + OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: UUID().uuidString, model: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "email", value: "email")) + + // 2. Flush Operation Repo + OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() + + // 3. Simulate updating the executor's request queue from a network response + executor.executeDeleteSubscriptionRequest(OSRequestDeleteSubscription(subscriptionModel: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer())), inBackground: false) + executor.executeDeleteSubscriptionRequest(OSRequestDeleteSubscription(subscriptionModel: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer())), inBackground: false) + } + + // 4. Run background threads + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // Previously caused crash: signal SIGABRT - malloc: double free for ptr + // Assert that every request SDK makes has a response set, and is handled + XCTAssertTrue(client.allRequestsHandled) + } + + /** + This test reproduces a crash in the Identity Executor. + It is possible for two threads to modify and cache queues concurrently. + */ + func testIdentityExecutorConcurrency() throws { + /* Setup */ + let client = MockOneSignalClient() + let aliases = [UUID().uuidString: "id"] + + OneSignalCoreImpl.setSharedClient(client) + MockUserRequests.setAddAliasesResponse(with: client, aliases: aliases) + + let executor = OSIdentityOperationExecutor() + OSOperationRepo.sharedInstance.addExecutor(executor) + + /* When */ + + DispatchQueue.concurrentPerform(iterations: 50) { _ in + // 1. Enqueue Add Alias Deltas to the Operation Repo + OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: UUID().uuidString, model: OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()), property: "aliases", value: aliases)) + OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: UUID().uuidString, model: OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()), property: "aliases", value: aliases)) + + // 2. Flush Operation Repo + OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() + + // 3. Simulate updating the executor's request queue from a network response + executor.executeAddAliasesRequest(OSRequestAddAliases(aliases: aliases, identityModel: OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer())), inBackground: false) + executor.executeAddAliasesRequest(OSRequestAddAliases(aliases: aliases, identityModel: OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer())), inBackground: false) + } + + // 4. Run background threads + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // Previously caused crash: signal SIGABRT - malloc: double free for ptr + // Assert that every request SDK makes has a response set, and is handled + XCTAssertTrue(client.allRequestsHandled) + } + + /** + This test aims to ensure concurrency safety in the Property Executor. + It is possible for two threads to modify and cache queues concurrently. + */ + func testPropertyExecutorConcurrency() throws { + /* Setup */ + let client = MockOneSignalClient() + // Ensure all requests fire the executor's callback so it will modify queues and cache it + client.fireSuccessForAllRequests = true + OneSignalCoreImpl.setSharedClient(client) + + let identityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()) + OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(identityModel) + + let executor = OSPropertyOperationExecutor() + OSOperationRepo.sharedInstance.addExecutor(executor) + + /* When */ + + DispatchQueue.concurrentPerform(iterations: 50) { _ in + // 1. Enqueue Deltas to the Operation Repo + OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "language", value: UUID().uuidString)) + OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "language", value: UUID().uuidString)) + + // 2. Flush Operation Repo + OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() + + // 3. Simulate updating the executor's request queue from a network response + executor.executeUpdatePropertiesRequest(OSRequestUpdateProperties(params: ["properties": ["language": UUID().uuidString], "refresh_device_metadata": false], identityModel: identityModel), inBackground: false) + } + + // 4. Run background threads + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // No crash + } + + /** + This test aims to ensure concurrency safety in the User Executor. + It is possible for two threads to modify and cache queues concurrently. + Currently, this executor only allows one request to send at a time, which should prevent concurrent access. + But out of caution and future-proofing, this test is added. + */ + func testUserExecutorConcurrency() throws { + /* Setup */ + + let client = MockOneSignalClient() + // Ensure all requests fire the executor's callback so it will modify queues and cache it + client.fireSuccessForAllRequests = true + OneSignalCoreImpl.setSharedClient(client) + + let identityModel1 = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()) + let identityModel2 = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()) + + let userExecutor = OSUserExecutor() + + /* When */ + + DispatchQueue.concurrentPerform(iterations: 50) { _ in + let identifyRequest = OSRequestIdentifyUser(aliasLabel: OS_EXTERNAL_ID, aliasId: UUID().uuidString, identityModelToIdentify: identityModel1, identityModelToUpdate: identityModel2) + let fetchRequest = OSRequestFetchUser(identityModel: identityModel1, aliasLabel: OS_ONESIGNAL_ID, aliasId: UUID().uuidString, onNewSession: false) + + // Append and execute requests simultaneously + userExecutor.appendToQueue(identifyRequest) + userExecutor.appendToQueue(fetchRequest) + userExecutor.executeIdentifyUserRequest(identifyRequest) + userExecutor.executeFetchUserRequest(fetchRequest) + } + + // Run background threads + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // No crash + } + + /** + This test reproduced a crash when the property model is being encoded. + */ + func testEncodingPropertiesModel_withConcurrency_doesNotCrash() throws { + /* Setup */ + let propertiesModel = OSPropertiesModel(changeNotifier: OSEventProducer()) + + /* When */ + DispatchQueue.concurrentPerform(iterations: 5_000) { i in + // 1. Add tags + for num in 0...9 { + propertiesModel.addTags(["\(i)tag\(num)": "value"]) + } + + // 2. Encode the model + OneSignalUserDefaults.initShared().saveCodeableData(forKey: "PropertyModel", withValue: propertiesModel) + + // 3. Clear the tags + propertiesModel.clearData() + } + } + + /** + This test reproduced a crash when the identity model is being encoded. + */ + func testEncodingIdentityModel_withConcurrency_doesNotCrash() throws { + /* Setup */ + let identityModel = OSIdentityModel(aliases: nil, changeNotifier: OSEventProducer()) + + /* When */ + DispatchQueue.concurrentPerform(iterations: 5_000) { i in + // 1. Add aliases + for num in 0...9 { + identityModel.addAliases(["\(i)alias\(num)": "value"]) + } + + // 2. Encode the model + OneSignalUserDefaults.initShared().saveCodeableData(forKey: "IdentityModel", withValue: identityModel) + + // 2. Clear the aliases + identityModel.clearData() + } + } +} From 98d90ead6ecf48e46532228f6c875dfcb2d9755b Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 31 Jul 2024 16:21:08 -0700 Subject: [PATCH 6/9] [tests] Configure 2 test plans for the Unit Test App * The UnitTestApp which runs all tests now has 2 test plans named "Full" and "Reduced" * The default is the Full test * We may want to disable certain long-running tests from running in the CI under the Reduced plan * There are no tests disabled in this commit, only adding 2 identical test plans --- .../OneSignal.xcodeproj/project.pbxproj | 4 + .../xcschemes/UnitTestApp.xcscheme | 11 +- .../UnitTestApp_TestPlan_Full.xctestplan | 117 ++++++++++++++++++ .../UnitTestApp_TestPlan_Reduced.xctestplan | 59 +++++++++ 4 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Full.xctestplan create mode 100644 iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Reduced.xctestplan diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 319e96575..ac5dacd67 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -1242,6 +1242,8 @@ 3C8544B62C5AEFF600F542A9 /* OneSignalOSCoreMocks.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OneSignalOSCoreMocks.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3C8544B82C5AEFF700F542A9 /* OneSignalOSCoreMocks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalOSCoreMocks.h; sourceTree = ""; }; 3C8544C22C5AF18B00F542A9 /* OSCoreMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCoreMocks.swift; sourceTree = ""; }; + 3C8544CB2C5AFCA700F542A9 /* UnitTestApp_TestPlan_Full.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTestApp_TestPlan_Full.xctestplan; sourceTree = ""; }; + 3C8544CC2C5AFCC300F542A9 /* UnitTestApp_TestPlan_Reduced.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTestApp_TestPlan_Reduced.xctestplan; sourceTree = ""; }; 3C87066F2BDE0957000D8CD2 /* MockUserRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserRequests.swift; sourceTree = ""; }; 3C8706712BDEE076000D8CD2 /* MockUserDefines.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserDefines.swift; sourceTree = ""; }; 3C8706752BDEED75000D8CD2 /* NSDictionary+UnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSDictionary+UnitTests.swift"; sourceTree = ""; }; @@ -2876,6 +2878,8 @@ DEF5CCF22539321A0003E9CC /* UnitTestApp */ = { isa = PBXGroup; children = ( + 3C8544CB2C5AFCA700F542A9 /* UnitTestApp_TestPlan_Full.xctestplan */, + 3C8544CC2C5AFCC300F542A9 /* UnitTestApp_TestPlan_Reduced.xctestplan */, DEF5CD6A253935720003E9CC /* UnitTestApp.entitlements */, DEF5CCF32539321A0003E9CC /* AppDelegate.h */, DEF5CCF42539321A0003E9CC /* AppDelegate.m */, diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/xcshareddata/xcschemes/UnitTestApp.xcscheme b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/xcshareddata/xcschemes/UnitTestApp.xcscheme index 3a019bd92..78545068d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/xcshareddata/xcschemes/UnitTestApp.xcscheme +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/xcshareddata/xcschemes/UnitTestApp.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> @@ -143,6 +143,15 @@ ReferencedContainer = "container:OneSignal.xcodeproj"> + + + + + + diff --git a/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Full.xctestplan b/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Full.xctestplan new file mode 100644 index 000000000..92e20a510 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Full.xctestplan @@ -0,0 +1,117 @@ +{ + "configurations" : [ + { + "id" : "916FBCB8-539B-42E8-8BE2-4BBBD8D90F11", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : { + "targets" : [ + { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "911E2CB91E398AB3003112A4", + "name" : "UnitTests" + }, + { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "DE7D17E527026B95002D3A5D", + "name" : "OneSignalCore" + }, + { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "DE7D17F827026BA3002D3A5D", + "name" : "OneSignalExtension" + }, + { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "3E2400371D4FFC31008BDE70", + "name" : "OneSignalFramework" + }, + { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "3C115160289A259500565C41", + "name" : "OneSignalOSCore" + }, + { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "DE7D187F27037F43002D3A5D", + "name" : "OneSignalOutcomes" + }, + { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "DE69E19A282ED8060090BB3D", + "name" : "OneSignalUser" + }, + { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "3CC063EA2B6D7FE8002BB07F", + "name" : "OneSignalUserTests" + }, + { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "3CC063A02B6D7A8D002BB07F", + "name" : "OneSignalCoreTests" + }, + { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "DEBA2A192C20E35E00E234DB", + "name" : "OneSignalNotificationsTests" + } + ] + }, + "targetForVariableExpansion" : { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "DEF5CCF02539321A0003E9CC", + "name" : "UnitTestApp" + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "911E2CB91E398AB3003112A4", + "name" : "UnitTests" + } + }, + { + "target" : { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "3CC063A02B6D7A8D002BB07F", + "name" : "OneSignalCoreTests" + } + }, + { + "target" : { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "3CC063EA2B6D7FE8002BB07F", + "name" : "OneSignalUserTests" + } + }, + { + "target" : { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "473542492B8F93330016DB4C", + "name" : "OneSignalLiveActivitiesTests" + } + }, + { + "target" : { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "DEBA2A192C20E35E00E234DB", + "name" : "OneSignalNotificationsTests" + } + }, + { + "target" : { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "3C01518D2C2E298E0079E076", + "name" : "OneSignalInAppMessagesTests" + } + } + ], + "version" : 1 +} diff --git a/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Reduced.xctestplan b/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Reduced.xctestplan new file mode 100644 index 000000000..914deec34 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Reduced.xctestplan @@ -0,0 +1,59 @@ +{ + "configurations" : [ + { + "id" : "8253BCEA-8327-4C5E-9C87-76E08A0CA354", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "3CC063A02B6D7A8D002BB07F", + "name" : "OneSignalCoreTests" + } + }, + { + "target" : { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "3C01518D2C2E298E0079E076", + "name" : "OneSignalInAppMessagesTests" + } + }, + { + "target" : { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "473542492B8F93330016DB4C", + "name" : "OneSignalLiveActivitiesTests" + } + }, + { + "target" : { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "DEBA2A192C20E35E00E234DB", + "name" : "OneSignalNotificationsTests" + } + }, + { + "target" : { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "3CC063EA2B6D7FE8002BB07F", + "name" : "OneSignalUserTests" + } + }, + { + "target" : { + "containerPath" : "container:OneSignal.xcodeproj", + "identifier" : "911E2CB91E398AB3003112A4", + "name" : "UnitTests" + } + } + ], + "version" : 1 +} From d6d5ab74d1ef5b2b7990a542d0ea68f58e6fb461 Mon Sep 17 00:00:00 2001 From: Nan Date: Thu, 1 Aug 2024 10:25:16 -0700 Subject: [PATCH 7/9] [tests] Disable concurrency tests in the Reduced test plan * Under the Reduced Test Plan, concurrency tests are disabled. * These tests spam calls and can cause carry over to following tests --- .../UnitTestApp/UnitTestApp_TestPlan_Reduced.xctestplan | 3 +++ 1 file changed, 3 insertions(+) diff --git a/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Reduced.xctestplan b/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Reduced.xctestplan index 914deec34..3f3d11a33 100644 --- a/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Reduced.xctestplan +++ b/iOS_SDK/OneSignalSDK/UnitTestApp/UnitTestApp_TestPlan_Reduced.xctestplan @@ -41,6 +41,9 @@ } }, { + "skippedTests" : [ + "UserConcurrencyTests" + ], "target" : { "containerPath" : "container:OneSignal.xcodeproj", "identifier" : "3CC063EA2B6D7FE8002BB07F", From 869f58303878a09d81ee90594fb6f0f6561b84a0 Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 31 Jul 2024 16:43:28 -0700 Subject: [PATCH 8/9] [ci] Update CI to use Reduced test plan --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32fee9d77..e8163d6c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,10 +40,11 @@ jobs: - name: Test env: scheme: ${{ 'UnitTestApp' }} + test_plan: ${{ 'UnitTestApp_TestPlan_Reduced' }} platform: ${{ 'iOS Simulator' }} file_to_build: ${{ 'iOS_SDK/OneSignalSDK/OneSignal.xcodeproj' }} filetype_parameter: ${{ 'project' }} run: | # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"` - xcodebuild test-without-building -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device" + xcodebuild test-without-building -scheme "$scheme" -testPlan "$test_plan" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device" From f79af948948303416df29642f14f07753190b73e Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 31 Jul 2024 20:58:08 -0700 Subject: [PATCH 9/9] [tests] fix bug clearing user defaults between tests * Fix bug that made the helper method to clear user defaults between tests return early. --- .../OneSignalCoreMocks.swift | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift index 75276eb68..20272b36e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift @@ -28,20 +28,18 @@ import XCTest public class OneSignalCoreMocks: NSObject { @objc public static func clearUserDefaults() { - guard let userDefaults = OneSignalUserDefaults.initStandard().userDefaults else { - return - } - let dictionary = userDefaults.dictionaryRepresentation() - for key in dictionary.keys { - userDefaults.removeObject(forKey: key) + if let userDefaults = OneSignalUserDefaults.initStandard().userDefaults { + let dictionary = userDefaults.dictionaryRepresentation() + for key in dictionary.keys { + userDefaults.removeObject(forKey: key) + } } - guard let sharedUserDefaults = OneSignalUserDefaults.initShared().userDefaults else { - return - } - let sharedDictionary = sharedUserDefaults.dictionaryRepresentation() - for key in sharedDictionary.keys { - sharedUserDefaults.removeObject(forKey: key) + if let sharedUserDefaults = OneSignalUserDefaults.initShared().userDefaults { + let sharedDictionary = sharedUserDefaults.dictionaryRepresentation() + for key in sharedDictionary.keys { + sharedUserDefaults.removeObject(forKey: key) + } } }