Skip to content

Commit 722a910

Browse files
authored
Merge pull request #176 from cashapp/bradfol/main-actor-tests
MainActor tests
2 parents aa98631 + f50edf3 commit 722a910

4 files changed

Lines changed: 201 additions & 1 deletion

File tree

Sources/Knit/Future+Async.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// Future+Async.swift
3+
// Knit
4+
//
5+
6+
import Combine
7+
8+
public extension Future {
9+
10+
/// Allow `Future`s to be instantiated directly with async closures.
11+
convenience init(
12+
async asyncClosure: @escaping (@escaping Future<Output, Failure>.Promise) async -> Void
13+
) {
14+
self.init { promise in
15+
Task {
16+
await asyncClosure(promise)
17+
}
18+
}
19+
}
20+
21+
}

Sources/KnitCodeGen/TypeNamer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public enum TypeNamer {
7777
return type
7878
}
7979

80-
private static let suffixedGenericTypes = ["Publisher", "Subject", "Provider", "Set"]
80+
private static let suffixedGenericTypes = ["Publisher", "Subject", "Provider", "Set", "Future"]
8181

8282
static func isClosure(type: String) -> Bool {
8383
return type.contains("->")

Tests/KnitCodeGenTests/TypeNamerTests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,12 @@ final class TypeNamerTests: XCTestCase {
147147
type: "ValueProvider<Int>",
148148
expectedIdentifier: "intProvider"
149149
)
150+
151+
assertComputedIdentifier(
152+
type: "Future<MyClass, Never>",
153+
expectedIdentifier: "myClassFuture"
154+
)
155+
150156
}
151157

152158
func testMultipleGenerics() {
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
//
2+
// Copyright © Block, Inc. All rights reserved.
3+
//
4+
5+
import Combine
6+
import Knit
7+
import XCTest
8+
9+
private actor ActorA {
10+
func sayHello() -> String {
11+
"Hello"
12+
}
13+
}
14+
15+
/// A class confined to `@MainActor`
16+
@MainActor
17+
private class MainClassA { }
18+
19+
/// Declare a custom global actor, used below
20+
@globalActor
21+
private actor CustomGlobalActor: GlobalActor {
22+
23+
static var shared = CustomGlobalActor()
24+
25+
typealias ActorType = CustomGlobalActor
26+
27+
}
28+
29+
/// A class confined to a custom global actor. Means it must not be instantiated on the main thread.
30+
@CustomGlobalActor
31+
private class CustomGlobalActorClass {
32+
33+
/// This initializer is confined to `@CustomGlobalActor` but has a dep that is `@MainActor` confined.
34+
init(mainClassA: MainClassA) {}
35+
36+
func sayHello() -> String {
37+
"Hello"
38+
}
39+
40+
}
41+
42+
/// A class that is async init but otherwise has sync methods.
43+
private class AsyncInitClass {
44+
45+
init() async {}
46+
47+
func sayHello() -> String {
48+
"Hello"
49+
}
50+
51+
}
52+
53+
/// Consumes the above types
54+
private class FinalConsumer {
55+
56+
let actorA: ActorA
57+
58+
let mainClassA: MainClassA
59+
60+
/// The dependency here is on a future of CustomGlobalActorClass, not CustomGlobalActorClass itself
61+
let customGlobalActorClass: Future<CustomGlobalActorClass, Never>
62+
63+
var asyncInitClass: AsyncInitClass?
64+
65+
private var cancellables = [AnyCancellable]()
66+
67+
init(
68+
actorA: ActorA,
69+
mainClassA: MainClassA,
70+
customGlobalActorClass: Future<CustomGlobalActorClass, Never>,
71+
asyncInitClass: Future<AsyncInitClass, Never>
72+
) {
73+
self.actorA = actorA
74+
self.mainClassA = mainClassA
75+
self.customGlobalActorClass = customGlobalActorClass
76+
77+
asyncInitClass.sink { [weak self] result in
78+
self?.asyncInitClass = result
79+
// Can also inform other methods that this property is now available
80+
}.store(in: &cancellables)
81+
}
82+
83+
/// Needs to be an async function due to `@CustomGlobalActor` confinement
84+
func askCustomGlobalActorClassToSayHello() async -> String {
85+
await customGlobalActorClass.value.sayHello()
86+
}
87+
88+
func askActorAToSayHello() async -> String {
89+
await actorA.sayHello()
90+
}
91+
92+
}
93+
94+
private final class TestAssembly: AutoInitModuleAssembly {
95+
96+
typealias TargetResolver = TestResolver
97+
98+
func assemble(container: Container<TargetResolver>) {
99+
100+
container.register(
101+
ActorA.self,
102+
mainActorFactory: { @MainActor resolver in
103+
ActorA()
104+
}
105+
)
106+
107+
container.register(
108+
MainClassA.self,
109+
mainActorFactory: { @MainActor resolver in
110+
MainClassA()
111+
}
112+
)
113+
114+
container.register(
115+
Future<CustomGlobalActorClass, Never>.self,
116+
mainActorFactory: { @MainActor resolver in
117+
let mainClassA = resolver.unsafeResolver.resolve(MainClassA.self)!
118+
119+
return Future<CustomGlobalActorClass, Never>() { promise in
120+
let customGlobalActorClass = await CustomGlobalActorClass(
121+
mainClassA: mainClassA
122+
)
123+
promise(.success(customGlobalActorClass))
124+
}
125+
}
126+
)
127+
128+
container.register(
129+
Future<AsyncInitClass, Never>.self,
130+
mainActorFactory: { @MainActor resolver in
131+
return Future<AsyncInitClass, Never>() { promise in
132+
promise(.success(await AsyncInitClass()))
133+
}
134+
}
135+
)
136+
137+
container.register(
138+
FinalConsumer.self,
139+
mainActorFactory: { @MainActor resolver in
140+
let actorA = resolver.unsafeResolver.resolve(ActorA.self)!
141+
let mainClassA = resolver.unsafeResolver.resolve(MainClassA.self)!
142+
let customGlobalActorClass = resolver.unsafeResolver.resolve(Future<CustomGlobalActorClass, Never>.self)!
143+
let asyncInitClass = resolver.unsafeResolver.resolve(Future<AsyncInitClass, Never>.self)!
144+
return FinalConsumer(
145+
actorA: actorA,
146+
mainClassA: mainClassA,
147+
customGlobalActorClass: customGlobalActorClass,
148+
asyncInitClass: asyncInitClass
149+
)
150+
}
151+
)
152+
153+
}
154+
155+
init() {}
156+
157+
static var dependencies: [any Knit.ModuleAssembly.Type] { [] }
158+
159+
}
160+
161+
class MainActorTests: XCTestCase {
162+
163+
@MainActor
164+
func testAssembly() async throws {
165+
let assembler = ModuleAssembler(
166+
[TestAssembly()]
167+
)
168+
let finalConsumer = try XCTUnwrap(assembler.resolver.resolve(FinalConsumer.self))
169+
170+
let result = await finalConsumer.askCustomGlobalActorClassToSayHello()
171+
XCTAssertEqual(result, "Hello")
172+
}
173+
}

0 commit comments

Comments
 (0)