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

Enforce module assembly happens on the main actor #203

Merged
merged 1 commit into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
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
4 changes: 1 addition & 3 deletions Docs/Modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

* Swinject provides the concept of an [Assembly](https://github.com/Swinject/Swinject/blob/master/Documentation/Assembler.md) which is responsible for registering a subset of the app services so that when all Assemblies are registered together via an Assembler all services are available.

`ModuleAssembly` extend the Assembly concept and define relationships between modules.
`ModuleAssembly` extends the Assembly concept to define relationships between modules.
When services are assembled using `ModuleAssembler` it must be able to resolve the entire tree of modules as for each service we do not know which other services it may require. By enforcing that all child modules are also registered we can guarantee that all expected services have been registered.

## AutoInitModuleAssembly
Expand All @@ -20,5 +20,3 @@ Overrides do not need to be in the same codebase as the original. The usual expe
## DefaultModuleAssemblyOverride

Using `replaces` provides the power to swap DI module implementations but requires explicit setup. A base assembly that implements `DefaultModuleAssemblyOverride` can automatically choose the override when default overrides are enabled in the `ModuleAssembler`. This defaults to true for unit testing and false for normal app runs.


9 changes: 5 additions & 4 deletions Sources/Knit/Module/ModuleAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public final class ModuleAssembler {
If the closure throws an error for any of the assemblies then a fatal error will occur.
- postAssemble: Hook after all assemblies are registered to make changes to the container.
*/
public convenience init(
@MainActor public convenience init(
parent: ModuleAssembler? = nil,
_ modules: [any ModuleAssembly],
overrideBehavior: OverrideBehavior = .defaultOverridesWhenTesting,
Expand Down Expand Up @@ -66,7 +66,7 @@ public final class ModuleAssembler {
}

// Internal required init that throws rather than fatal errors
required init(
@MainActor required init(
parent: ModuleAssembler? = nil,
_modules modules: [any ModuleAssembly],
overrideBehavior: OverrideBehavior = .defaultOverridesWhenTesting,
Expand All @@ -93,8 +93,9 @@ public final class ModuleAssembler {
let dependencyTree = builder.dependencyTree
self._container.register(DependencyTree.self) { _ in dependencyTree }

let assembler = Assembler(container: self._container)
assembler.apply(assemblies: builder.assemblies)
for assembly in builder.assemblies {
assembly.assemble(container: self._container)
}
postAssemble?(_container)

if overrideBehavior.useAbstractPlaceholders {
Expand Down
4 changes: 3 additions & 1 deletion Sources/Knit/Module/ModuleAssembly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
import Foundation
import Swinject

public protocol ModuleAssembly: Assembly {
public protocol ModuleAssembly {

associatedtype TargetResolver

static var resolverType: Self.TargetResolver.Type { get }

static var dependencies: [any ModuleAssembly.Type] { get }

@MainActor func assemble(container: Container)

/// A ModuleAssembly can replace any number of other module assemblies.
/// If this assembly replaces another it is expected to provide all registrations from the replaced assemblies.
/// A common case is a fake assembly that registers fake services matching those from the original module.
Expand Down
2 changes: 2 additions & 0 deletions Sources/Knit/Module/ScopedModuleAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public final class ScopedModuleAssembler<ScopedResolver> {
return internalAssembler._container
}

@MainActor
public convenience init(
parent: ModuleAssembler? = nil,
_ modules: [any ModuleAssembly],
Expand Down Expand Up @@ -46,6 +47,7 @@ public final class ScopedModuleAssembler<ScopedResolver> {
}

// Internal required init that throws rather than fatal errors
@MainActor
required init(
parent: ModuleAssembler? = nil,
_modules modules: [any ModuleAssembly],
Expand Down
6 changes: 6 additions & 0 deletions Tests/KnitTests/ModuleAssemblerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import XCTest

final class ModuleAssemblerTests: XCTestCase {

@MainActor
func test_auto_assembler() {
let resolver = ModuleAssembler([Assembly1()]).resolver
XCTAssertNotNil(resolver.resolve(Service1.self))
}

@MainActor
func test_non_auto_assembler() {
let resolver = ModuleAssembler([
Assembly3(),
Expand All @@ -21,13 +23,15 @@ final class ModuleAssemblerTests: XCTestCase {
XCTAssertNotNil(resolver.resolve(Service3.self))
}

@MainActor
func test_registered_modules() {
let assembler = ModuleAssembler([Assembly1()])
XCTAssertTrue(assembler.registeredModules.contains(where: {$0 == Assembly1.self}))
XCTAssertTrue(assembler.registeredModules.contains(where: {$0 == Assembly2.self}))
XCTAssertFalse(assembler.registeredModules.contains(where: {$0 == Assembly3.self}))
}

@MainActor
func test_parent_assembler() {
// Put some modules in the parent and some in the child.
let parent = ModuleAssembler([Assembly1()])
Expand All @@ -42,6 +46,7 @@ final class ModuleAssemblerTests: XCTestCase {
XCTAssertNil(parent.resolver.resolve(Service3.self))
}

@MainActor
func test_abstractAssemblyValidation() {
XCTAssertThrowsError(
try ModuleAssembler(
Expand All @@ -60,6 +65,7 @@ final class ModuleAssemblerTests: XCTestCase {
)
}

@MainActor
func test_abstractAssemblyPlaceholders() throws {
// No error is thrown as the graph is using abstract placeholders
_ = try ModuleAssembler(
Expand Down
12 changes: 12 additions & 0 deletions Tests/KnitTests/ModuleAssemblyOverrideTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,19 @@ final class ModuleAssemblyOverrideTests: XCTestCase {
)
}

@MainActor
func test_serviceRegisteredWithoutFakes() {
let resolver = ModuleAssembler([Assembly2()]).resolver
XCTAssertTrue(resolver.resolve(Service2Protocol.self) is Service2)
}

@MainActor
func test_servicesRegisteredWithFakes() {
let resolver = ModuleAssembler([Assembly2(), Assembly2Fake()]).resolver
XCTAssertTrue(resolver.resolve(Service2Protocol.self) is Service2Fake)
}

@MainActor
func test_assemblerWithDefaultOverrides() {
let assembler = ModuleAssembler([Assembly2()], overrideBehavior: .useDefaultOverrides)
XCTAssertTrue(assembler.registeredModules.contains(where: {$0 == Assembly1Fake.self}))
Expand All @@ -53,46 +56,53 @@ final class ModuleAssemblyOverrideTests: XCTestCase {
XCTAssertTrue(assembler.isRegistered(Assembly1.self))
}

@MainActor
func test_noDefaultOverrideForInputModules() {
let assembler = ModuleAssembler([Assembly1()], overrideBehavior: .useDefaultOverrides)
XCTAssertTrue(assembler.isRegistered(Assembly1.self))
// The fake is not automatically registered
XCTAssertFalse(assembler.isRegistered(Assembly1Fake.self))
}

@MainActor
func test_explicitInputOverride() {
let assembler = ModuleAssembler([Assembly1(), Assembly1Fake()], overrideBehavior: .useDefaultOverrides)
XCTAssertTrue(assembler.isRegistered(Assembly1.self))
XCTAssertTrue(assembler.isRegistered(Assembly1Fake.self))
}

@MainActor
func test_assemblerWithoutDefaultOverrides() {
let assembler = ModuleAssembler([Assembly2()], overrideBehavior: .disableDefaultOverrides)
XCTAssertTrue(assembler.isRegistered(Assembly1.self))
XCTAssertFalse(assembler.isRegistered(Assembly1Fake.self))
}

@MainActor
func test_assemblerWithFakes() {
let assembler = ModuleAssembler([Assembly2Fake()])
XCTAssertFalse(assembler.registeredModules.contains(where: {$0 == Assembly2.self}))
XCTAssertTrue(assembler.isRegistered(Assembly2.self))
XCTAssertTrue(assembler.isRegistered(Assembly2Fake.self))
}

@MainActor
func test_parentFakes() {
let parent = ModuleAssembler([Assembly1Fake()])
let child = ModuleAssembler(parent: parent, [Assembly2()])
XCTAssertTrue(child.isRegistered(Assembly1.self))
XCTAssertTrue(child.isRegistered(Assembly1Fake.self))
}

@MainActor
func test_autoFake() {
let assembler = ModuleAssembler([Assembly5()])
XCTAssertTrue(assembler.isRegistered(Assembly4.self))
XCTAssertTrue(assembler.isRegistered(Assembly4Fake.self))
XCTAssertTrue(assembler.isRegistered(Assembly5.self))
}

@MainActor
func test_overrideDefaultOverride() {
let assembler = ModuleAssembler(
[Assembly4(), Assembly4Fake2()],
Expand All @@ -111,6 +121,7 @@ final class ModuleAssemblyOverrideTests: XCTestCase {
XCTAssertTrue(builder.assemblies.first is NonAutoOverride)
}

@MainActor
func test_parentNonAutoOverride() {
let parent = ModuleAssembler([NonAutoOverride()])
let child = ModuleAssembler(parent: parent, [Assembly1()], overrideBehavior: .disableDefaultOverrides)
Expand All @@ -125,6 +136,7 @@ final class ModuleAssemblyOverrideTests: XCTestCase {
)
}

@MainActor
func test_multipleOverrides() {
let assembler = ModuleAssembler(
[MultipleDependencyAssembly(), MultipleOverrideAssembly()],
Expand Down
2 changes: 2 additions & 0 deletions Tests/KnitTests/ModuleCycleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import XCTest

final class ModuleCycleTests: XCTestCase {

@MainActor
func test_cycleResolution() {
let assembler = ModuleAssembler([Assembly1()])
XCTAssertTrue(assembler.isRegistered(Assembly1.self))
Expand All @@ -25,6 +26,7 @@ final class ModuleCycleTests: XCTestCase {
)
}

@MainActor
func test_sourceCycle() {
let assembler = ModuleAssembler([Assembly5()])
XCTAssertEqual(
Expand Down
5 changes: 5 additions & 0 deletions Tests/KnitTests/ScopedModuleAssemblerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import XCTest

final class ScopedModuleAssemblerTests: XCTestCase {

@MainActor
func testScoping() throws {
// Allows modules at the same level to be registered
let assembler = try ScopedModuleAssembler<TestResolver>(_modules: [Assembly1()])
XCTAssertEqual(assembler.internalAssembler.registeredModules.count, 1)
}

@MainActor
func testParentExcluded() throws {
let parent = try ScopedModuleAssembler<TestResolver>(_modules: [Assembly1()])
let assembler = try ScopedModuleAssembler<OutsideResolver>(
Expand All @@ -23,13 +25,15 @@ final class ScopedModuleAssemblerTests: XCTestCase {
XCTAssertEqual(assembler.internalAssembler.registeredModules.count, 1)
}

@MainActor
func testPostAssemble() throws {
let assembler = try ScopedModuleAssembler<TestResolver>(_modules: [Assembly1()]) { container in
container.register(String.self) { _ in "string" }
}
XCTAssertEqual(assembler.resolver.resolve(String.self), "string")
}

@MainActor
func testOutOfScopeAssemblyThrows() {
XCTAssertThrowsError(
try ScopedModuleAssembler<TestResolver>(
Expand All @@ -49,6 +53,7 @@ final class ScopedModuleAssemblerTests: XCTestCase {
)
}

@MainActor
func testIncorrectInputScope() throws {
let parent = try ScopedModuleAssembler<TestResolver>(_modules: [Assembly1()])
// Even though Assembly1 is already registered, because it was explicitly provided the validation should fail
Expand Down
2 changes: 2 additions & 0 deletions Tests/KnitTests/SynchronizationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import XCTest

final class SynchronizationTests: XCTestCase {

@MainActor
func testMultiThreadResolving() async throws {
// Use a parent/child relationship to test synchronization between containers
let parent = ModuleAssembler([Assembly1()])
Expand All @@ -28,6 +29,7 @@ final class SynchronizationTests: XCTestCase {
XCTAssertEqual(result.0.service1.id, result.1.service1.id)
}

@MainActor
func testMultiThreadingScopedAssembler() async throws {
let assembler = ScopedModuleAssembler<TestScopedResolver>([Assembly2()])

Expand Down