Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MainActor tests #176

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions Tests/KnitTests/MainActorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//
// Copyright © Block, Inc. All rights reserved.
//

import Combine
import Swinject
import XCTest

private actor ActorA {
func sayHello() -> String {
"Hello"
}
}

/// A class confined to `@MainActor`
@MainActor
private class MainClassA { }

/// Declare a custom global actor, used below
@globalActor
private actor CustomGlobalActor: GlobalActor {

static var shared = CustomGlobalActor()

typealias ActorType = CustomGlobalActor

}

/// A class confined to a custom global actor. Means it must not be instantiated on the main thread.
@CustomGlobalActor
private class CustomGlobalActorClass {

/// This initializer is confined to `@CustomGlobalActor` but has a dep that is `@MainActor` confined.
init(mainClassA: MainClassA) {}

func sayHello() -> String {
"Hello"
}

}

/// A class that is async init but otherwise has sync methods.
private class AsyncInitClass {

init() async {}

func sayHello() -> String {
"Hello"
}

}

/// Consumes the above types
private class FinalConsumer {

let actorA: ActorA

let mainClassA: MainClassA

/// The dependency here is on a future of CustomGlobalActorClass, not CustomGlobalActorClass itself
let customGlobalActorClass: Future<CustomGlobalActorClass, Never>

var asyncInitClass: AsyncInitClass?

private var cancellables = [AnyCancellable]()

init(
actorA: ActorA,
mainClassA: MainClassA,
customGlobalActorClass: Future<CustomGlobalActorClass, Never>,
asyncInitClass: Future<AsyncInitClass, Never>
) {
self.actorA = actorA
self.mainClassA = mainClassA
self.customGlobalActorClass = customGlobalActorClass

asyncInitClass.sink { [weak self] result in
self?.asyncInitClass = result
// Can also inform other methods that this property is now available
}.store(in: &cancellables)
}

/// Needs to be an async function due to `@CustomGlobalActor` confinement
func askCustomGlobalActorClassToSayHello() async -> String {
await customGlobalActorClass.value.sayHello()
}

func askActorAToSayHello() async -> String {
await actorA.sayHello()
}

}

private class TestAssembly: Assembly {

func assemble(container: Container) {

container.register(
ActorA.self,
mainActorFactory: { @MainActor resolver in
ActorA()
}
)

container.register(
MainClassA.self,
mainActorFactory: { @MainActor resolver in
MainClassA()
}
)

container.register(
Future<CustomGlobalActorClass, Never>.self,
mainActorFactory: { @MainActor resolver in
let mainClassA = resolver.resolve(MainClassA.self)!

return Future<CustomGlobalActorClass, Never>() { promise in
let customGlobalActorClass = await CustomGlobalActorClass(
mainClassA: mainClassA
)
promise(.success(customGlobalActorClass))
}
}
)

container.register(
Future<AsyncInitClass, Never>.self,
mainActorFactory: { @MainActor resolver in
return Future<AsyncInitClass, Never>() { promise in
promise(.success(await AsyncInitClass()))
}
}
)

container.register(
FinalConsumer.self,
mainActorFactory: { @MainActor resolver in
let actorA = resolver.resolve(ActorA.self)!
let mainClassA = resolver.resolve(MainClassA.self)!
let customGlobalActorClass = resolver.resolve(Future<CustomGlobalActorClass, Never>.self)!
let asyncInitClass = resolver.resolve(Future<AsyncInitClass, Never>.self)!
return FinalConsumer(
actorA: actorA,
mainClassA: mainClassA,
customGlobalActorClass: customGlobalActorClass,
asyncInitClass: asyncInitClass
)
}
)

}
}

class MainActorTests: XCTestCase {

func testAssembly() throws {
let container = Container()
TestAssembly().assemble(container: container)
let finalConsumer = try XCTUnwrap(container.resolve(FinalConsumer.self))

let asyncExpectation = expectation(description: "async task")

Task {
let result = await finalConsumer.askCustomGlobalActorClassToSayHello()
XCTAssertEqual(result, "Hello")
asyncExpectation.fulfill()
}

waitForExpectations(timeout: 5)
}
}

/// Allow `Future`s to be instantiated directly with async closures.
private extension Future {
convenience init(async asyncClosure: @escaping (@escaping Future<Output, Failure>.Promise) async -> Void) {
self.init { promise in
Task {
await asyncClosure(promise)
}
}
}
}