From 3116be0e253e3780ca68ed5f8ab242c9d7927944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kolos=20Folta=CC=81nyi?= Date: Mon, 2 Dec 2024 11:49:27 +0100 Subject: [PATCH 1/2] feat: add concurrency related tests --- Package.swift | 6 + .../ActorConformanceTests.swift | 157 ++++++++++++++++++ .../TestProtocol.swift => BuildTests.swift} | 70 +++++--- 3 files changed, 211 insertions(+), 22 deletions(-) rename Tests/MockableTests/{Protocols/TestProtocol.swift => BuildTests.swift} (72%) diff --git a/Package.swift b/Package.swift index dd7a499..2afc188 100644 --- a/Package.swift +++ b/Package.swift @@ -59,6 +59,9 @@ let package = Package( "MockableMacro", .product(name: "IssueReporting", package: "swift-issue-reporting") ], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency") + ], plugins: devPlugins ), .macro( @@ -67,6 +70,9 @@ let package = Package( .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax") ], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency") + ], plugins: devPlugins ) ], diff --git a/Tests/MockableMacroTests/ActorConformanceTests.swift b/Tests/MockableMacroTests/ActorConformanceTests.swift index f191683..fdc3d8c 100644 --- a/Tests/MockableMacroTests/ActorConformanceTests.swift +++ b/Tests/MockableMacroTests/ActorConformanceTests.swift @@ -11,6 +11,163 @@ import SwiftSyntax @testable import Mockable final class ActorConformanceTests: MockableMacroTestCase { + func test_global_actor_conformance() { + assertMacro { + """ + @MainActor + @Mockable + protocol Test { + var foo: Int { get } + nonisolated var quz: Int { get } + func bar(number: Int) -> Int + nonisolated func baz(number: Int) -> Int + } + """ + } expansion: { + """ + @MainActor + protocol Test { + var foo: Int { get } + nonisolated var quz: Int { get } + func bar(number: Int) -> Int + nonisolated func baz(number: Int) -> Int + } + + #if MOCKING + final class MockTest: Test, Mockable.MockableService { + typealias Mocker = Mockable.Mocker + private let mocker = Mocker() + @available(*, deprecated, message: "Use given(_ service:) instead. ") + nonisolated var given: ReturnBuilder { + .init(mocker: mocker) + } + @available(*, deprecated, message: "Use when(_ service:) instead. ") + nonisolated var when: ActionBuilder { + .init(mocker: mocker) + } + @available(*, deprecated, message: "Use verify(_ service:) instead. ") + nonisolated var verify: VerifyBuilder { + .init(mocker: mocker) + } + nonisolated func reset(_ scopes: Set = .all) { + mocker.reset(scopes: scopes) + } + nonisolated init(policy: Mockable.MockerPolicy? = nil) { + if let policy { + mocker.policy = policy + } + } + func bar(number: Int) -> Int { + let member: Member = .m3_bar(number: .value(number)) + return mocker.mock(member) { producer in + let producer = try cast(producer) as (Int) -> Int + return producer(number) + } + } + nonisolated func baz(number: Int) -> Int { + let member: Member = .m4_baz(number: .value(number)) + return mocker.mock(member) { producer in + let producer = try cast(producer) as (Int) -> Int + return producer(number) + } + } + var foo: Int { + get { + let member: Member = .m1_foo + return mocker.mock(member) { producer in + let producer = try cast(producer) as () -> Int + return producer() + } + } + } + nonisolated var quz: Int { + get { + let member: Member = .m2_quz + return mocker.mock(member) { producer in + let producer = try cast(producer) as () -> Int + return producer() + } + } + } + enum Member: Mockable.Matchable, Mockable.CaseIdentifiable, Sendable { + case m1_foo + case m2_quz + case m3_bar(number: Parameter) + case m4_baz(number: Parameter) + func match(_ other: Member) -> Bool { + switch (self, other) { + case (.m1_foo, .m1_foo): + return true + case (.m2_quz, .m2_quz): + return true + case (.m3_bar(number: let leftNumber), .m3_bar(number: let rightNumber)): + return leftNumber.match(rightNumber) + case (.m4_baz(number: let leftNumber), .m4_baz(number: let rightNumber)): + return leftNumber.match(rightNumber) + default: + return false + } + } + } + struct ReturnBuilder: Mockable.Builder { + private let mocker: Mocker + init(mocker: Mocker) { + self.mocker = mocker + } + var foo: Mockable.FunctionReturnBuilder Int> { + .init(mocker, kind: .m1_foo) + } + var quz: Mockable.FunctionReturnBuilder Int> { + .init(mocker, kind: .m2_quz) + } + func bar(number: Parameter) -> Mockable.FunctionReturnBuilder Int> { + .init(mocker, kind: .m3_bar(number: number)) + } + func baz(number: Parameter) -> Mockable.FunctionReturnBuilder Int> { + .init(mocker, kind: .m4_baz(number: number)) + } + } + struct ActionBuilder: Mockable.Builder { + private let mocker: Mocker + init(mocker: Mocker) { + self.mocker = mocker + } + var foo: Mockable.FunctionActionBuilder { + .init(mocker, kind: .m1_foo) + } + var quz: Mockable.FunctionActionBuilder { + .init(mocker, kind: .m2_quz) + } + func bar(number: Parameter) -> Mockable.FunctionActionBuilder { + .init(mocker, kind: .m3_bar(number: number)) + } + func baz(number: Parameter) -> Mockable.FunctionActionBuilder { + .init(mocker, kind: .m4_baz(number: number)) + } + } + struct VerifyBuilder: Mockable.Builder { + private let mocker: Mocker + init(mocker: Mocker) { + self.mocker = mocker + } + var foo: Mockable.FunctionVerifyBuilder { + .init(mocker, kind: .m1_foo) + } + var quz: Mockable.FunctionVerifyBuilder { + .init(mocker, kind: .m2_quz) + } + func bar(number: Parameter) -> Mockable.FunctionVerifyBuilder { + .init(mocker, kind: .m3_bar(number: number)) + } + func baz(number: Parameter) -> Mockable.FunctionVerifyBuilder { + .init(mocker, kind: .m4_baz(number: number)) + } + } + } + #endif + """ + } + } func test_actor_requirement() { assertMacro { """ diff --git a/Tests/MockableTests/Protocols/TestProtocol.swift b/Tests/MockableTests/BuildTests.swift similarity index 72% rename from Tests/MockableTests/Protocols/TestProtocol.swift rename to Tests/MockableTests/BuildTests.swift index 678f6c7..5bea9a2 100644 --- a/Tests/MockableTests/Protocols/TestProtocol.swift +++ b/Tests/MockableTests/BuildTests.swift @@ -8,26 +8,24 @@ import Mockable @Mockable -protocol TestProtocol: Actor, Sendable where Item2: Identifiable { - - // MARK: Associated Types - +protocol TestAssociatedTypes where Item2: Identifiable { associatedtype Item1 associatedtype Item2: Equatable, Hashable associatedtype Item3 where Item3: Equatable, Item3: Hashable - func foo(item1: Item1) -> Item1 func foo(item2: Item2) -> Item2 func foo(item3: Item3) -> Item3 +} - // MARK: Exotic Parameters - +@Mockable +protocol TestExoticParameters { func modifyValue(_ value: inout Int) func printValues(_ values: Int...) func execute(operation: @escaping () throws -> Void) +} - // MARK: Function Effects - +@Mockable +protocol TestFunctionEffects { func canThrowError() throws func returnsAndThrows() throws -> String func call(operation: @escaping () throws -> Void) rethrows @@ -35,26 +33,29 @@ protocol TestProtocol: Actor, Sendable where Item2: Identifiable { func asyncThrowingFunction() async throws func asyncParamFunction(param: @escaping () async throws -> Void) async nonisolated func nonisolatedFunction() async +} - // MARK: Generic Functions - +@Mockable +protocol TestGenericFunctions { func foo(item: (Array<[(Set, String)]>, Int)) func genericFunc(item: T) -> V + func getInts() -> any Collection func method( prop1: T, prop2: E, prop3: C, prop4: I ) where E: Equatable, E: Hashable, C: Codable - func getInts() -> any Collection - - // MARK: Name Collision +} +@Mockable +protocol TestNameCollisions { func fetchData(for name: Int) -> String func fetchData(for name: String) -> String func fetchData(forA name: String) -> String func fetchData(forB name: String) -> String func `repeat`(param: Bool) -> Int +} - // MARK: Property Requirements - +@Mockable +protocol TestPropertyRequirements { var computedInt: Int { get } var computedString: String { get } var mutableInt: Int { get set } @@ -63,21 +64,46 @@ protocol TestProtocol: Actor, Sendable where Item2: Identifiable { var asyncProperty: String { get async } var asyncThrowingProperty: String { get async throws } nonisolated var nonisolatedProperty: String { get set } +} - // MARK: Init - +@Mockable +protocol TestInitRequirements { init?() async throws init(index: Int) init(name value: String, index: Int) +} - // MARK: Attributes - +@Mockable +protocol TestAttributes { @available(iOS 16, *) init(attributed: String) - @available(iOS 16, *) var attributedProp: Int { get } - @available(iOS 16, *) func attributedTest() } + +#if canImport(Foundation) +import Foundation + +@Mockable +protocol TestNSObject: NSObjectProtocol { + func foo(param: Int) -> String +} +#endif + +@Mockable +protocol TestActorConformance: Actor { + func foo(param: Int) -> String +} + +@Mockable +@MainActor +protocol TestGlobalActor { + func foo(param: Int) -> String +} + +@Mockable +protocol TestSendable: Sendable { + func foo(param: Int) -> String +} From f2b8d7ebddc2ac60fbee33711925e4f1b0dfa083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kolos=20Folta=CC=81nyi?= Date: Mon, 2 Dec 2024 11:50:53 +0100 Subject: [PATCH 2/2] fix: use legacy issue-reporting package name for compatibility --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 2afc188..f9f1d3c 100644 --- a/Package.swift +++ b/Package.swift @@ -50,14 +50,14 @@ let package = Package( ], dependencies: devDependencies + [ .package(url: "https://github.com/swiftlang/swift-syntax.git", "509.0.0"..<"601.0.0"), - .package(url: "https://github.com/pointfreeco/swift-issue-reporting", .upToNextMajor(from: "1.4.1")) + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", .upToNextMajor(from: "1.4.1")) ], targets: devTargets + [ .target( name: "Mockable", dependencies: [ "MockableMacro", - .product(name: "IssueReporting", package: "swift-issue-reporting") + .product(name: "IssueReporting", package: "xctest-dynamic-overlay") ], swiftSettings: [ .enableExperimentalFeature("StrictConcurrency")