diff --git a/0239-reliably-testing-async-pt2/README.md b/0239-reliably-testing-async-pt2/README.md new file mode 100644 index 00000000..c0d821be --- /dev/null +++ b/0239-reliably-testing-async-pt2/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [Reliable Async Tests: More Problems](https://www.pointfree.co/episodes/ep239-reliable-async-tests-more-problems) +> +> We explore a few more advanced scenarios when it comes to async code—including cancellation, async sequences, and clocks—and how difficult they are to test. diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync.xcodeproj/project.pbxproj b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync.xcodeproj/project.pbxproj new file mode 100644 index 00000000..797d66ac --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync.xcodeproj/project.pbxproj @@ -0,0 +1,521 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 2A02A49F2A3100350073BDEC /* LockIsolated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A02A49E2A3100350073BDEC /* LockIsolated.swift */; }; + 4B07DB1E2A31063800037088 /* NumberFactModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B07DB1D2A31063800037088 /* NumberFactModelTests.swift */; }; + 4B07DB212A31082B00037088 /* Dependencies in Frameworks */ = {isa = PBXBuildFile; productRef = 4B07DB202A31082B00037088 /* Dependencies */; }; + 4B5108832A312ACE0092F1D9 /* Countdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5108822A312ACE0092F1D9 /* Countdown.swift */; }; + 4B5108852A3133C20092F1D9 /* ConcurrencyExtras.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5108842A3133C20092F1D9 /* ConcurrencyExtras.swift */; }; + CAC2DD812A30B827007675BD /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC2DD802A30B827007675BD /* App.swift */; }; + CAC2DD832A30B827007675BD /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC2DD822A30B827007675BD /* ContentView.swift */; }; + CAC2DD852A30B829007675BD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAC2DD842A30B829007675BD /* Assets.xcassets */; }; + CAC2DD882A30B829007675BD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAC2DD872A30B829007675BD /* Preview Assets.xcassets */; }; + CAC2DD922A30B829007675BD /* ReliablyTestingAsyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC2DD912A30B829007675BD /* ReliablyTestingAsyncTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + CAC2DD8E2A30B829007675BD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CAC2DD752A30B827007675BD /* Project object */; + proxyType = 1; + remoteGlobalIDString = CAC2DD7C2A30B827007675BD; + remoteInfo = ReliablyTestingAsync; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2A02A49D2A30FD3E0073BDEC /* ReliablyTestingAsync.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = ReliablyTestingAsync.xctestplan; sourceTree = ""; }; + 2A02A49E2A3100350073BDEC /* LockIsolated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockIsolated.swift; sourceTree = ""; }; + 4B07DB1D2A31063800037088 /* NumberFactModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFactModelTests.swift; sourceTree = ""; }; + 4B07DB222A310B1300037088 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 4B5108822A312ACE0092F1D9 /* Countdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Countdown.swift; sourceTree = ""; }; + 4B5108842A3133C20092F1D9 /* ConcurrencyExtras.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrencyExtras.swift; sourceTree = ""; }; + CAC2DD7D2A30B827007675BD /* ReliablyTestingAsync.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReliablyTestingAsync.app; sourceTree = BUILT_PRODUCTS_DIR; }; + CAC2DD802A30B827007675BD /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; + CAC2DD822A30B827007675BD /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + CAC2DD842A30B829007675BD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + CAC2DD872A30B829007675BD /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + CAC2DD8D2A30B829007675BD /* ReliablyTestingAsyncTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReliablyTestingAsyncTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + CAC2DD912A30B829007675BD /* ReliablyTestingAsyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReliablyTestingAsyncTests.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + CAC2DD7A2A30B827007675BD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B07DB212A31082B00037088 /* Dependencies in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CAC2DD8A2A30B829007675BD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + CAC2DD742A30B827007675BD = { + isa = PBXGroup; + children = ( + CAC2DD7F2A30B827007675BD /* ReliablyTestingAsync */, + CAC2DD902A30B829007675BD /* ReliablyTestingAsyncTests */, + CAC2DD7E2A30B827007675BD /* Products */, + ); + sourceTree = ""; + }; + CAC2DD7E2A30B827007675BD /* Products */ = { + isa = PBXGroup; + children = ( + CAC2DD7D2A30B827007675BD /* ReliablyTestingAsync.app */, + CAC2DD8D2A30B829007675BD /* ReliablyTestingAsyncTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + CAC2DD7F2A30B827007675BD /* ReliablyTestingAsync */ = { + isa = PBXGroup; + children = ( + 4B07DB222A310B1300037088 /* Info.plist */, + CAC2DD802A30B827007675BD /* App.swift */, + 4B5108842A3133C20092F1D9 /* ConcurrencyExtras.swift */, + CAC2DD822A30B827007675BD /* ContentView.swift */, + 4B5108822A312ACE0092F1D9 /* Countdown.swift */, + CAC2DD842A30B829007675BD /* Assets.xcassets */, + CAC2DD862A30B829007675BD /* Preview Content */, + ); + path = ReliablyTestingAsync; + sourceTree = ""; + }; + CAC2DD862A30B829007675BD /* Preview Content */ = { + isa = PBXGroup; + children = ( + CAC2DD872A30B829007675BD /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + CAC2DD902A30B829007675BD /* ReliablyTestingAsyncTests */ = { + isa = PBXGroup; + children = ( + 2A02A49D2A30FD3E0073BDEC /* ReliablyTestingAsync.xctestplan */, + CAC2DD912A30B829007675BD /* ReliablyTestingAsyncTests.swift */, + 4B07DB1D2A31063800037088 /* NumberFactModelTests.swift */, + 2A02A49E2A3100350073BDEC /* LockIsolated.swift */, + ); + path = ReliablyTestingAsyncTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + CAC2DD7C2A30B827007675BD /* ReliablyTestingAsync */ = { + isa = PBXNativeTarget; + buildConfigurationList = CAC2DDA12A30B829007675BD /* Build configuration list for PBXNativeTarget "ReliablyTestingAsync" */; + buildPhases = ( + CAC2DD792A30B827007675BD /* Sources */, + CAC2DD7A2A30B827007675BD /* Frameworks */, + CAC2DD7B2A30B827007675BD /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ReliablyTestingAsync; + packageProductDependencies = ( + 4B07DB202A31082B00037088 /* Dependencies */, + ); + productName = ReliablyTestingAsync; + productReference = CAC2DD7D2A30B827007675BD /* ReliablyTestingAsync.app */; + productType = "com.apple.product-type.application"; + }; + CAC2DD8C2A30B829007675BD /* ReliablyTestingAsyncTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = CAC2DDA42A30B829007675BD /* Build configuration list for PBXNativeTarget "ReliablyTestingAsyncTests" */; + buildPhases = ( + CAC2DD892A30B829007675BD /* Sources */, + CAC2DD8A2A30B829007675BD /* Frameworks */, + CAC2DD8B2A30B829007675BD /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + CAC2DD8F2A30B829007675BD /* PBXTargetDependency */, + ); + name = ReliablyTestingAsyncTests; + productName = ReliablyTestingAsyncTests; + productReference = CAC2DD8D2A30B829007675BD /* ReliablyTestingAsyncTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + CAC2DD752A30B827007675BD /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + TargetAttributes = { + CAC2DD7C2A30B827007675BD = { + CreatedOnToolsVersion = 15.0; + }; + CAC2DD8C2A30B829007675BD = { + CreatedOnToolsVersion = 15.0; + TestTargetID = CAC2DD7C2A30B827007675BD; + }; + }; + }; + buildConfigurationList = CAC2DD782A30B827007675BD /* Build configuration list for PBXProject "ReliablyTestingAsync" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = CAC2DD742A30B827007675BD; + packageReferences = ( + 4B07DB1F2A31082B00037088 /* XCRemoteSwiftPackageReference "swift-dependencies" */, + ); + productRefGroup = CAC2DD7E2A30B827007675BD /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + CAC2DD7C2A30B827007675BD /* ReliablyTestingAsync */, + CAC2DD8C2A30B829007675BD /* ReliablyTestingAsyncTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + CAC2DD7B2A30B827007675BD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CAC2DD882A30B829007675BD /* Preview Assets.xcassets in Resources */, + CAC2DD852A30B829007675BD /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CAC2DD8B2A30B829007675BD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + CAC2DD792A30B827007675BD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B5108852A3133C20092F1D9 /* ConcurrencyExtras.swift in Sources */, + CAC2DD832A30B827007675BD /* ContentView.swift in Sources */, + CAC2DD812A30B827007675BD /* App.swift in Sources */, + 4B5108832A312ACE0092F1D9 /* Countdown.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CAC2DD892A30B829007675BD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CAC2DD922A30B829007675BD /* ReliablyTestingAsyncTests.swift in Sources */, + 4B07DB1E2A31063800037088 /* NumberFactModelTests.swift in Sources */, + 2A02A49F2A3100350073BDEC /* LockIsolated.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + CAC2DD8F2A30B829007675BD /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = CAC2DD7C2A30B827007675BD /* ReliablyTestingAsync */; + targetProxy = CAC2DD8E2A30B829007675BD /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + CAC2DD9F2A30B829007675BD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + 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; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; + }; + name = Debug; + }; + CAC2DDA02A30B829007675BD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + 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; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + CAC2DDA22A30B829007675BD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"ReliablyTestingAsync/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ReliablyTestingAsync/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.ReliablyTestingAsync; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CAC2DDA32A30B829007675BD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"ReliablyTestingAsync/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ReliablyTestingAsync/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.ReliablyTestingAsync; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + CAC2DDA52A30B829007675BD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.ReliablyTestingAsyncTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ReliablyTestingAsync.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ReliablyTestingAsync"; + }; + name = Debug; + }; + CAC2DDA62A30B829007675BD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.ReliablyTestingAsyncTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ReliablyTestingAsync.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ReliablyTestingAsync"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + CAC2DD782A30B827007675BD /* Build configuration list for PBXProject "ReliablyTestingAsync" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CAC2DD9F2A30B829007675BD /* Debug */, + CAC2DDA02A30B829007675BD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CAC2DDA12A30B829007675BD /* Build configuration list for PBXNativeTarget "ReliablyTestingAsync" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CAC2DDA22A30B829007675BD /* Debug */, + CAC2DDA32A30B829007675BD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CAC2DDA42A30B829007675BD /* Build configuration list for PBXNativeTarget "ReliablyTestingAsyncTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CAC2DDA52A30B829007675BD /* Debug */, + CAC2DDA62A30B829007675BD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 4B07DB1F2A31082B00037088 /* XCRemoteSwiftPackageReference "swift-dependencies" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-dependencies.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.5.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 4B07DB202A31082B00037088 /* Dependencies */ = { + isa = XCSwiftPackageProductDependency; + package = 4B07DB1F2A31082B00037088 /* XCRemoteSwiftPackageReference "swift-dependencies" */; + productName = Dependencies; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = CAC2DD752A30B827007675BD /* Project object */; +} diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync.xcodeproj/xcshareddata/xcschemes/ReliablyTestingAsync.xcscheme b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync.xcodeproj/xcshareddata/xcschemes/ReliablyTestingAsync.xcscheme new file mode 100644 index 00000000..a0f9a012 --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync.xcodeproj/xcshareddata/xcschemes/ReliablyTestingAsync.xcscheme @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/App.swift b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/App.swift new file mode 100644 index 00000000..2e0b8cae --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/App.swift @@ -0,0 +1,12 @@ +import SwiftUI + +@main +struct ReliablyTestingAsyncApp: App { + var body: some Scene { + WindowGroup { + if NSClassFromString("XCTestCase") == nil { + ContentView(model: NumberFactModel()) + } + } + } +} diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Assets.xcassets/AccentColor.colorset/Contents.json b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Assets.xcassets/AppIcon.appiconset/Contents.json b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Assets.xcassets/Contents.json b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/ConcurrencyExtras.swift b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/ConcurrencyExtras.swift new file mode 100644 index 00000000..5fb53f50 --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/ConcurrencyExtras.swift @@ -0,0 +1,13 @@ +import Darwin + +typealias Original = @convention(thin) (UnownedJob) -> Void +typealias Hook = @convention(thin) (UnownedJob, Original) -> Void + +var swift_task_enqueueGlobal_hook: Hook? { + get { _swift_task_enqueueGlobal_hook.pointee } + set { _swift_task_enqueueGlobal_hook.pointee = newValue } +} + +private let _swift_task_enqueueGlobal_hook = + dlsym(dlopen(nil, RTLD_LAZY), "swift_task_enqueueGlobal_hook") + .assumingMemoryBound(to: Hook?.self) diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/ContentView.swift b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/ContentView.swift new file mode 100644 index 00000000..11d11200 --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/ContentView.swift @@ -0,0 +1,114 @@ +import Dependencies +import SwiftUI + +struct NumberFactClient { + var fact: @Sendable (Int) async throws -> String +} + +extension NumberFactClient: DependencyKey { + static let liveValue = Self { number in + try await Task.sleep(for: .seconds(1)) + return try await String( + decoding: URLSession.shared.data(from: URL(string: "http://numbersapi.com/\(number)")!).0, + as: UTF8.self + ) + } +} + +extension DependencyValues { + var numberFact: NumberFactClient { + get { self[NumberFactClient.self] } + set { self[NumberFactClient.self] = newValue } + } +} + +@MainActor +class NumberFactModel: ObservableObject { + @Dependency(\.numberFact) var numberFact + + @Published var count = 0 + @Published var fact: String? + @Published var factTask: Task? + var isLoading: Bool { self.factTask != nil } + + func incrementButtonTapped() { + self.fact = nil + self.factTask?.cancel() + self.factTask = nil + self.count += 1 + } + func decrementButtonTapped() { + self.fact = nil + self.factTask?.cancel() + self.factTask = nil + self.count -= 1 + } + func getFactButtonTapped() async { + self.factTask?.cancel() + + self.fact = nil + self.factTask = Task { + try await self.numberFact.fact(self.count) + } + defer { self.factTask = nil } + do { + self.fact = try await self.factTask?.value + } catch { + // TODO: handle error + } + } + func cancelButtonTapped() { + self.factTask?.cancel() + self.factTask = nil + } + func onTask() async { + for await _ in NotificationCenter.default.notifications(named: UIApplication.userDidTakeScreenshotNotification) { + self.count += 1 + } + } +} + +struct ContentView: View { + @ObservedObject var model: NumberFactModel + + var body: some View { + Form { + Section { + HStack { + Button("-") { self.model.decrementButtonTapped() } + Text("\(self.model.count)") + Button("+") { self.model.incrementButtonTapped() } + } + } + .buttonStyle(.plain) + + Section { + if self.model.isLoading { + HStack(spacing: 4) { + Button("Cancel") { + self.model.cancelButtonTapped() + } + Spacer() + ProgressView() + .id(UUID()) + } + } else { + Button("Get fact") { + Task { await self.model.getFactButtonTapped() } + } + } + } + + if let fact = self.model.fact { + Text(fact) + } + } + .task { await self.model.onTask() } + } +} + +struct ContentPreviews: PreviewProvider { + static var previews: some View { + ContentView(model: NumberFactModel()) + } +} diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Countdown.swift b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Countdown.swift new file mode 100644 index 00000000..e62c1781 --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Countdown.swift @@ -0,0 +1,119 @@ +import Clocks +import SwiftUI + +struct CountdownDemo: View { + @State var countdown = 1000 + @State var isConfettiVisible = false + let clock: any Clock + + init(clock: some Clock = ContinuousClock()) { + self.clock = clock + } + + var body: some View { + ZStack { + Text("\(self.countdown)") + .font(.system(size: 200).bold()) + if self.isConfettiVisible { + ForEach(1...100, id: \.self) { _ in + ConfettiView() + .offset(x: .random(in: -20...20), y: .random(in: -20...20)) + } + } + } + .task { + while true { + if self.countdown == 0 { + self.isConfettiVisible = true + break + } + try? await self.clock.sleep(for: .seconds(1)) + self.countdown -= 1 + } + } + } +} + +struct ParticlesModifier: ViewModifier { + @State var duration = Double.random(in: 2...5) + @State var time = 0.0 + @State var scale = 0.3 + + func body(content: Content) -> some View { + content + .scaleEffect(self.scale) + .modifier(ParticlesEffect(time: self.time)) + .opacity(1 - self.time / self.duration) + .onAppear { + withAnimation(.easeOut(duration: self.duration)) { + self.self.time = self.duration + self.self.scale = 2.0 + } + } + } +} + +struct ParticlesEffect: GeometryEffect { + var direction = Double.random(in: -Double.pi...Double.pi) + var distance = Double.random(in: 20...400) + var time: Double + + var animatableData: Double { + get { self.time } + set { self.time = newValue } + } + func effectValue(size: CGSize) -> ProjectionTransform { + ProjectionTransform( + CGAffineTransform( + translationX: self.distance * cos(self.direction) * self.time, + y: self.distance * sin(self.direction) * self.time + ) + ) + } +} + +struct ConfettiView: View { + @State var anchor = CGFloat.random(in: 0...1).rounded() + @State var color = pointFreeColors.randomElement()! + @State var isAnimating = false + @State var rotationOffsetX = Double.random(in: 0...360) + @State var rotationOffsetY = Double.random(in: 0...360) + @State var speedX = Double.random(in: 0.5...2) + @State var speedZ = Double.random(in: 0.5...2) + + var body: some View { + Rectangle() + .fill(self.color) + .frame(width: 20, height: 20, alignment: .center) + .onAppear(perform: { isAnimating = true }) + .rotation3DEffect( + .degrees(rotationOffsetX + (isAnimating ? 360 : 0)), axis: (x: 1, y: 0, z: 0) + ) + .animation( + Animation.linear(duration: self.speedX).repeatForever(autoreverses: false), + value: isAnimating + ) + .rotation3DEffect( + .degrees(rotationOffsetY + (isAnimating ? 360 : 0)), axis: (x: 0, y: 0, z: 1), + anchor: UnitPoint(x: self.anchor, y: self.anchor) + ) + .animation( + Animation.linear(duration: self.speedZ).repeatForever(autoreverses: false), + value: isAnimating + ) + .modifier(ParticlesModifier()) + } +} + +private let pointFreeColors = [ + Color.init(red: 152/255, green: 239/255, blue: 181/255), + Color.init(red: 252/255, green: 241/255, blue: 143/255), + Color.init(red: 113/255, green: 201/255, blue: 250/255), + Color.init(red: 141/255, green: 81/255, blue: 246/255), +] + +struct CountdownDemo_Previews: PreviewProvider { + static var previews: some View { + CountdownDemo(clock: .immediate) + } +} diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Info.plist b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Info.plist new file mode 100644 index 00000000..6a6654d9 --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Info.plist @@ -0,0 +1,11 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Preview Content/Preview Assets.xcassets/Contents.json b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsync/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsyncTests/LockIsolated.swift b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsyncTests/LockIsolated.swift new file mode 100644 index 00000000..2e368512 --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsyncTests/LockIsolated.swift @@ -0,0 +1,26 @@ +import Foundation + +public final class LockIsolated: @unchecked Sendable { + private var _value: Value + private let lock = NSRecursiveLock() + public init(_ value: @autoclosure @Sendable () throws -> Value) rethrows { + self._value = try value() + } + public func withValue( + _ operation: (inout Value) throws -> T + ) rethrows -> T { + try self.lock.withLock { + var value = self._value + defer { self._value = value } + return try operation(&value) + } + } +} + +extension LockIsolated where Value: Sendable { + public var value: Value { + self.lock.withLock { + self._value + } + } +} diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsyncTests/NumberFactModelTests.swift b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsyncTests/NumberFactModelTests.swift new file mode 100644 index 00000000..876a94d5 --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsyncTests/NumberFactModelTests.swift @@ -0,0 +1,144 @@ +import Dependencies +import XCTest +@testable import ReliablyTestingAsync + +@MainActor +final class NumberFactModelTests: XCTestCase { + func testIncrementDecrement() { + let model = NumberFactModel() + model.incrementButtonTapped() + XCTAssertEqual(model.count, 1) + model.decrementButtonTapped() + XCTAssertEqual(model.count, 0) + } + + func testGetFact() async { + let model = withDependencies { + $0.numberFact.fact = { "\($0) is a good number." } + } operation: { + NumberFactModel() + } + await model.getFactButtonTapped() + XCTAssertEqual(model.fact, "0 is a good number.") + + model.incrementButtonTapped() + XCTAssertEqual(model.fact, nil) + + await model.getFactButtonTapped() + XCTAssertEqual(model.fact, "1 is a good number.") + } + + func testFactClearsOut() async { + let fact = AsyncStream.makeStream(of: String.self) + + let model = withDependencies { + $0.numberFact.fact = { _ in + await fact.stream.first(where: { _ in true })! + } + } operation: { + NumberFactModel() + } + model.fact = "An old fact about 0." + + let task = Task { await model.getFactButtonTapped() } + await Task.yield() + XCTAssertEqual(model.fact, nil) + fact.continuation.yield("0 is a good number.") + await task.value + XCTAssertEqual(model.fact, "0 is a good number.") + } + + func testFactIsLoading() async { + let fact = AsyncStream.makeStream(of: String.self) + + let model = withDependencies { + $0.numberFact.fact = { _ in + await fact.stream.first(where: { _ in true })! + } + } operation: { + NumberFactModel() + } + model.fact = "An old fact about 0." + + let task = Task { await model.getFactButtonTapped() } + await Task.yield() + XCTAssertEqual(model.isLoading, true) + fact.continuation.yield("0 is a good number.") + await task.value + XCTAssertEqual(model.fact, "0 is a good number.") + XCTAssertEqual(model.isLoading, false) + } + + func testBackToBackGetFact() async throws { + let fact0 = AsyncStream.makeStream(of: String.self) + let fact1 = AsyncStream.makeStream(of: String.self) + let callCount = LockIsolated(0) + + let model = withDependencies { + $0.numberFact.fact = { number in + callCount.withValue { $0 += 1 } + if callCount.value == 1 { + return await fact0.stream.first(where: { _ in true }) ?? "" + } else if callCount.value == 2 { + return await fact1.stream.first(where: { _ in true }) ?? "" + } else { + fatalError() + } + } + } operation: { + NumberFactModel() + } + + let task0 = Task { await model.getFactButtonTapped() } + let task1 = Task { await model.getFactButtonTapped() } + await Task.yield() + fact1.continuation.yield("0 is a great number.") + try await Task.sleep(for: .milliseconds(100)) + fact0.continuation.yield("0 is a better number.") + await task0.value + await task1.value + XCTAssertEqual(model.fact, "0 is a great number.") + } + + func testCancel() async { + let model = withDependencies { + $0.numberFact.fact = { _ in try await Task.never() } + } operation: { + NumberFactModel() + } + let task = Task { await model.getFactButtonTapped() } + await Task.megaYield() + model.cancelButtonTapped() + await task.value + XCTAssertEqual(model.fact, nil) + } + + func testScreenshots() async { + let model = NumberFactModel() + + let task = Task { await model.onTask() } + + await Task.megaYield() + NotificationCenter.default.post(name: UIApplication.userDidTakeScreenshotNotification, object: nil) + while model.count != 1 { + await Task.yield() + } + XCTAssertEqual(model.count, 1) + + NotificationCenter.default.post(name: UIApplication.userDidTakeScreenshotNotification, object: nil) + while model.count != 2 { + await Task.yield() + } + XCTAssertEqual(model.count, 2) + } +} + +extension Task where Success == Never, Failure == Never { + static func megaYield() async { + for _ in 1...20 { + await Task.detached(priority: .background) { + await Task.yield() + }.value + } + } +} diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsyncTests/ReliablyTestingAsync.xctestplan b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsyncTests/ReliablyTestingAsync.xctestplan new file mode 100644 index 00000000..45b5984b --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsyncTests/ReliablyTestingAsync.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "1DBD9DAB-B1B3-419B-9A10-F5EDE94A2585", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:ReliablyTestingAsync.xcodeproj", + "identifier" : "CAC2DD8C2A30B829007675BD", + "name" : "ReliablyTestingAsyncTests" + } + } + ], + "version" : 1 +} diff --git a/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsyncTests/ReliablyTestingAsyncTests.swift b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsyncTests/ReliablyTestingAsyncTests.swift new file mode 100644 index 00000000..9b1ef76e --- /dev/null +++ b/0239-reliably-testing-async-pt2/ReliablyTestingAsync/ReliablyTestingAsyncTests/ReliablyTestingAsyncTests.swift @@ -0,0 +1,152 @@ +import XCTest +@testable import ReliablyTestingAsync + +final class ReliablyTestingAsyncTests: XCTestCase { + func testBasics() async throws { + let start = Date() + try await Task.sleep(for: .seconds(1)) + let end = Date() + XCTAssertEqual(end.timeIntervalSince(start), 1, accuracy: 0.1) + } + + //@MainActor + func testTaskStart() async { + let values = LockIsolated([Int]()) + let task = Task { + values.withValue { $0.append(1) } + } + values.withValue { $0.append(2) } + await task.value + XCTAssertEqual(values.value, [2, 1]) + } + + func testTaskStart_MainSerialExecutor() async { + swift_task_enqueueGlobal_hook = { job, _ in + MainActor.shared.enqueue(job) + } + + let values = LockIsolated([Int]()) + let task = Task { + values.withValue { $0.append(1) } + } + values.withValue { $0.append(2) } + await task.value + XCTAssertEqual(values.value, [2, 1]) + } + + func testTaskStartOrder() async { + let values = LockIsolated([Int]()) + let task1 = Task { + values.withValue { $0.append(1) } + } + let task2 = Task { + values.withValue { $0.append(2) } + } + _ = await (task1.value, task2.value) + XCTAssertEqual(values.value, [1, 2]) + } + + func testTaskStartOrder_MainSerialExecutor() async { + swift_task_enqueueGlobal_hook = { job, _ in + MainActor.shared.enqueue(job) + } + + let values = LockIsolated([Int]()) + let task1 = Task { + values.withValue { $0.append(1) } + } + let task2 = Task { + values.withValue { $0.append(2) } + } + _ = await (task1.value, task2.value) + XCTAssertEqual(values.value, [1, 2]) + } + + func testTaskGroupStartOrder() async { + let values = await withTaskGroup(of: [Int].self) { group in + for index in 1...100 { + group.addTask { [index] } + } + return await group.reduce(into: []) { $0 += $1 } + } + XCTAssertEqual(values, Array(1...100)) + } + + func testTaskGroupStartOrder_MainSerialExecutor() async { + swift_task_enqueueGlobal_hook = { job, _ in + MainActor.shared.enqueue(job) + } + + let values = await withTaskGroup(of: [Int].self) { group in + for index in 1...100 { + group.addTask { [index] } + } + return await group.reduce(into: []) { $0 += $1 } + } + XCTAssertEqual(values, Array(1...100)) + } + + func testYieldScheduling() async { + let count = 10 + let values = LockIsolated<[Int]>([]) + let tasks = (0...count).map { n in + Task { + values.withValue { $0.append(n * 2) } + await Task.yield() + values.withValue { $0.append(n * 2 + 1) } + } + } + for task in tasks { await task.value } + + XCTAssertEqual(values.value, [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21]) + } + + func testYieldScheduling_MainSerialExecutor() async { + swift_task_enqueueGlobal_hook = { job, _ in + MainActor.shared.enqueue(job) + } + + let count = 10 + let values = LockIsolated<[Int]>([]) + let tasks = (0...count).map { n in + Task { + values.withValue { $0.append(n * 2) } + await Task.yield() + values.withValue { $0.append(n * 2 + 1) } + } + } + for task in tasks { await task.value } + + XCTAssertEqual(values.value, [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21]) + } + + /* + [{task0}, task1, task2] + [0] + + [task2, task0, task1] + [0, 2] + + [task0, task1, task2] + [0, 2, 4] + + [task1, task2] + [0, 2, 4, 1] + + [task2] + [0, 2, 4, 1, 3] + + [] + [0, 2, 4, 1, 3, 5] + + */ + + @MainActor + func testEnqueueHook() async throws { + swift_task_enqueueGlobal_hook = { job, _ in + MainActor.shared.enqueue(job) + } + let (bytes, _) = try await URLSession.shared.bytes(from: URL(string: "https://www.google.com")!) + for try await _ in bytes {} + } +} diff --git a/README.md b/README.md index 090fe831..13a994b8 100644 --- a/README.md +++ b/README.md @@ -240,3 +240,4 @@ This repository is the home of code written on episodes of [Point-Free](https:// 1. [Composable Stacks: Effect Cancellation](0236-composable-navigation-pt15) 1. [Composable Stacks: Testing](0237-composable-navigation-pt16) 1. [Reliable Async Tests: The Problem](0238-reliably-testing-async-pt1) +1. [Reliable Async Tests: More Problems](0239-reliably-testing-async-pt2)