Skip to content

Commit

Permalink
242
Browse files Browse the repository at this point in the history
  • Loading branch information
stephencelis committed Jul 18, 2023
1 parent 49a241c commit 14a095e
Show file tree
Hide file tree
Showing 19 changed files with 1,638 additions and 0 deletions.
5 changes: 5 additions & 0 deletions 0242-reliably-testing-async-pt5/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## [Point-Free](https://www.pointfree.co)

> #### This directory contains code from Point-Free Episode: [Reliable Async Tests: The Point](https://www.pointfree.co/episodes/ep242-reliable-async-tests-the-point)
>
> What’s the point of the work we did to make async testing reliable and deterministic, and are we even testing reality anymore? We conclude our series by rewriting our feature and tests using Combine instead of async-await, and comparing both approaches.

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CAC2DD7C2A30B827007675BD"
BuildableName = "ReliablyTestingAsync.app"
BlueprintName = "ReliablyTestingAsync"
ReferencedContainer = "container:ReliablyTestingAsync.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:ReliablyTestingAsyncTests/ReliablyTestingAsync.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CAC2DD8C2A30B829007675BD"
BuildableName = "ReliablyTestingAsyncTests.xctest"
BlueprintName = "ReliablyTestingAsyncTests"
ReferencedContainer = "container:ReliablyTestingAsync.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CAC2DD962A30B829007675BD"
BuildableName = "ReliablyTestingAsyncUITests.xctest"
BlueprintName = "ReliablyTestingAsyncUITests"
ReferencedContainer = "container:ReliablyTestingAsync.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CAC2DD7C2A30B827007675BD"
BuildableName = "ReliablyTestingAsync.app"
BlueprintName = "ReliablyTestingAsync"
ReferencedContainer = "container:ReliablyTestingAsync.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CAC2DD7C2A30B827007675BD"
BuildableName = "ReliablyTestingAsync.app"
BlueprintName = "ReliablyTestingAsync"
ReferencedContainer = "container:ReliablyTestingAsync.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import SwiftUI

@main
struct ReliablyTestingAsyncApp: App {
var body: some Scene {
WindowGroup {
if NSClassFromString("XCTestCase") == nil {
ContentView(model: CombineNumberFactModel())
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import Combine
import Dependencies
import SwiftUI

struct NumberFactClient {
var fact: @Sendable (Int) async throws -> String
var factPublisher: @Sendable (Int) -> AnyPublisher<String, Error>
}

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
)
} factPublisher: { number in
URLSession.shared.dataTaskPublisher(for: URL(string: "http://numbersapi.com/\(number)")!)
// .delay(for: 1, scheduler: DispatchQueue.main)
.map { data, _ in String(decoding: data, as: UTF8.self) }
.mapError { $0 as Error }
.eraseToAnyPublisher()
}
}

extension DependencyValues {
var numberFact: NumberFactClient {
get { self[NumberFactClient.self] }
set { self[NumberFactClient.self] = newValue }
}
}

@MainActor
class CombineNumberFactModel: ObservableObject {
@Dependency(\.mainQueue) var mainQueue
@Dependency(\.numberFact) var numberFact

@Published var count = 0
@Published var fact: String?
@Published var factCancellable: AnyCancellable?
var isLoading: Bool { self.factCancellable != nil }

func incrementButtonTapped() {
self.fact = nil
self.factCancellable?.cancel()
self.factCancellable = nil
self.count += 1
}
func decrementButtonTapped() {
self.fact = nil
self.factCancellable?.cancel()
self.factCancellable = nil
self.count -= 1
}
func getFactButtonTapped() {
self.factCancellable?.cancel()

self.fact = nil
self.factCancellable = self.numberFact.factPublisher(self.count)
.receive(on: self.mainQueue)
.sink(
receiveCompletion: { [weak self] _ in
// TODO: Handle error
self?.factCancellable = nil
},
receiveValue: { [weak self] fact in
self?.fact = fact
}
)
}
func cancelButtonTapped() {
self.factCancellable?.cancel()
self.factCancellable = nil
}
var notificationCancellable: AnyCancellable?
func onTask() {
self.notificationCancellable = NotificationCenter.default.publisher(for: UIApplication.userDidTakeScreenshotNotification)
.sink { [weak self] _ in
self?.count += 1
}
}
}
@MainActor
class NumberFactModel: ObservableObject {
@Dependency(\.numberFact) var numberFact

@Published var count = 0
@Published var fact: String?
@Published var factTask: Task<String, Error>?
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: CombineNumberFactModel

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: CombineNumberFactModel())
}
}
Loading

0 comments on commit 14a095e

Please sign in to comment.