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

Compile fixture code with macro #282

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a9a5502
Remove adding unchecked sendable
sidepelican Nov 12, 2024
4642bc9
Add requireSendable context parameter
sidepelican Nov 12, 2024
8f081e4
sendable support in method model
sidepelican Nov 12, 2024
ec1e646
a bit refactoring
sidepelican Nov 12, 2024
4aad37f
Fix unnecessary param name and type splitting
sidepelican Nov 15, 2024
5365599
Stop using implicit arguments and context
sidepelican Nov 15, 2024
bbbec5f
process argument history type
sidepelican Nov 15, 2024
1d89eb8
Add final when Sendable required
sidepelican Nov 21, 2024
e84af32
Fix state handler type
sidepelican Nov 21, 2024
fe8d1a3
sendable handler call
sidepelican Nov 22, 2024
a5f5b57
Add @Sendable to closure type
sidepelican Nov 22, 2024
1294be2
generate concurrency helpers
sidepelican Nov 22, 2024
eb019dc
Beautify generated code
sidepelican Nov 23, 2024
811376f
Merge branch 'master' into testvalidation
sidepelican Dec 3, 2024
7a57b99
Add fixture macro
sidepelican Dec 5, 2024
ecf6e8a
Use global let
sidepelican Dec 5, 2024
a226737
Use expression macro
sidepelican Dec 6, 2024
94081ca
replace fixture mock annotation
sidepelican Dec 6, 2024
b8434d7
Rewrite test cases
sidepelican Dec 6, 2024
bed5daf
Revert "Use expression macro"
sidepelican Dec 6, 2024
05a4c81
Use MemberMacro
sidepelican Dec 6, 2024
f2c4533
replace some tests with @Fixture
sidepelican Dec 6, 2024
2bb97d6
Merge branch 'master' into fixture_validation
sidepelican Dec 9, 2024
54a8f4a
Update minimum swift version
sidepelican Dec 9, 2024
23fb580
Avoid compiler crash on Swift 5.10
sidepelican Dec 10, 2024
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
19 changes: 15 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// swift-tools-version:5.7
// swift-tools-version:5.10
import CompilerPluginSupport
import PackageDescription

let package = Package(
name: "Mockolo",
platforms: [
.macOS(.v12),
.macOS(.v13),
],
products: [
.executable(name: "mockolo", targets: ["Mockolo"]),
Expand All @@ -21,7 +22,8 @@ let package = Package(
dependencies: [
"MockoloFramework",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]),
]
),
.target(
name: "MockoloFramework",
dependencies: [
Expand All @@ -30,12 +32,21 @@ let package = Package(
.product(name: "Algorithms", package: "swift-algorithms"),
]
),
.macro(
name: "MockoloTestSupportMacros",
dependencies: [
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
.product(name: "SwiftDiagnostics", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
]
),
.testTarget(
name: "MockoloTests",
dependencies: [
"MockoloFramework",
"MockoloTestSupportMacros",
],
path: "Tests"
)
),
]
)
11 changes: 3 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ This project may contain unstable APIs which may not be ready for general use. S

## System Requirements

* Swift 5.7 or later
* Xcode 14.2 or later
* macOS 12.0 or later and Linux
* Swift 5.10 or later
* Xcode 15.3 or later
* macOS 13.0 or later and Linux
* Support is included for the Swift Package Manager


Expand Down Expand Up @@ -424,11 +424,6 @@ This will generate:
public class FooMock: FooProtocol { ... }
```

## Used libraries

[SwiftSyntax](https://github.com/apple/swift-syntax) |


## How to contribute to Mockolo
See [CONTRIBUTING](CONTRIBUTING.md) for more info.

Expand Down
6 changes: 2 additions & 4 deletions Sources/Mockolo/Executor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,8 @@ struct Executor: ParsableCommand {
excludeImports: excludeImports,
to: outputFilePath,
loggingLevel: loggingLevel,
concurrencyLimit: concurrencyLimit,
onCompletion: { _ in
log("Done. Exiting program.", level: .info)
})
concurrencyLimit: concurrencyLimit)
log("Done. Exiting program.", level: .info)
} catch {
fatalError("Generation error: \(error)")
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/MockoloFramework/Operations/Generator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum InputError: Error {
}

/// Performs end to end mock generation flow
@discardableResult
public func generate(sourceDirs: [String],
sourceFiles: [String],
parser: SourceParser,
Expand All @@ -43,8 +44,7 @@ public func generate(sourceDirs: [String],
excludeImports: [String],
to outputFilePath: String,
loggingLevel: Int,
concurrencyLimit: Int?,
onCompletion: @escaping (String) -> ()) throws {
concurrencyLimit: Int?) throws -> String {
guard sourceDirs.count > 0 || sourceFiles.count > 0 else {
log("Source files or directories do not exist", level: .error)
throw InputError.sourceFilesError
Expand Down Expand Up @@ -184,5 +184,5 @@ public func generate(sourceDirs: [String],
log("TOTAL", t5-t0, level: .verbose)
log("#Protocols = \(protocolMap.count), #Annotated protocols = \(annotatedProtocolMap.count), #Parent mock classes = \(parentMocks.count), #Final mock classes = \(candidates.count), File LoC = \(count)", level: .verbose)

onCompletion(result)
return result
}
49 changes: 49 additions & 0 deletions Sources/MockoloTestSupportMacros/Fixture.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import SwiftBasicFormat
import SwiftSyntax
import SwiftSyntaxMacros

struct Fixture: MemberMacro {
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
let baseItems = declaration.memberBlock.members.filter { (item: MemberBlockItemSyntax) in
if let decl = item.decl.asProtocol(WithAttributesSyntax.self) {
let isFixtureAnnotated = decl.attributes.contains { (attr: AttributeListSyntax.Element) in
return attr.trimmedDescription == "@Fixture"
}
return !isFixtureAnnotated
}
return true
}

let indent = BasicFormat.inferIndentation(of: declaration) ?? .spaces(4)
let sourceContent = baseItems.trimmedDescription(matching: \.isNewline)

let varDecl = VariableDeclSyntax(
modifiers: [.init(name: .keyword(.static))],
.let,
name: "_source",
initializer: .init(
value: StringLiteralExprSyntax(
multilineContent: sourceContent,
endIndent: Trivia(pieces: node.leadingTrivia.filter(\.isSpaceOrTab)) + indent
)
)
)

return [DeclSyntax(varDecl)]
}
}

extension StringLiteralExprSyntax {
fileprivate init(multilineContent: String, endIndent: Trivia) {
self = StringLiteralExprSyntax(
openingQuote: .multilineStringQuoteToken(),
segments: [.stringSegment(.init(content: .stringSegment(multilineContent)))],
closingQuote: .multilineStringQuoteToken(leadingTrivia: endIndent)
)
}
}
8 changes: 8 additions & 0 deletions Sources/MockoloTestSupportMacros/MacroMain.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main struct MacroMain: CompilerPlugin {
let providingMacros: [any Macro.Type] = [
Fixture.self,
]
}
15 changes: 8 additions & 7 deletions Tests/MockoloTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,14 @@ class MockoloTestCase: XCTestCase {
excludeImports: [],
to: dstFilePath,
loggingLevel: 3,
concurrencyLimit: concurrencyLimit,
onCompletion: { ret in
let output = (try? String(contentsOf: URL(fileURLWithPath: self.defaultDstFilePath), encoding: .utf8)) ?? ""
let outputContents = output.components(separatedBy: .newlines).filter { !$0.isEmpty && !$0.allSatisfy(\.isWhitespace) }
let fixtureContents = dstContent.components(separatedBy: .newlines).filter { !$0.isEmpty && !$0.allSatisfy(\.isWhitespace) }
XCTAssert(outputContents.contains(subArray: fixtureContents), "output:\n" + output)
})
concurrencyLimit: concurrencyLimit)
let output = (try? String(contentsOf: URL(fileURLWithPath: self.defaultDstFilePath), encoding: .utf8)) ?? ""
let outputContents = output.components(separatedBy: .newlines).filter { !$0.isEmpty && !$0.allSatisfy(\.isWhitespace) }
let fixtureContents = dstContent.components(separatedBy: .newlines).filter { !$0.isEmpty && !$0.allSatisfy(\.isWhitespace) }
if fixtureContents.isEmpty {
throw XCTSkip("empty fixture")
}
XCTAssert(outputContents.contains(subArray: fixtureContents), "output:\n" + output)
}
}

Expand Down
16 changes: 8 additions & 8 deletions Tests/TestActor/ActorTests.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
final class ActorTests: MockoloTestCase {
func testActorProtocol() {
verify(srcContent: actorProtocol,
dstContent: actorProtocolMock)
verify(srcContent: actorProtocol._source,
dstContent: actorProtocol.expected._source)
}

func testParentProtocolInheritsActor() {
verify(srcContent: parentProtocolInheritsActor,
dstContent: parentProtocolInheritsActorMock)
verify(srcContent: parentProtocolInheritsActor._source,
dstContent: parentProtocolInheritsActor.expected._source)
}

func testGlobalActorProtocol() {
verify(srcContent: globalActorProtocol,
dstContent: globalActorProtocolMock)
verify(srcContent: globalActorProtocol._source,
dstContent: globalActorProtocol.expected._source)
}

func testAttributeAboveAnnotationComment() {
verify(srcContent: attributeAboveAnnotationComment,
dstContent: attributeAboveAnnotationCommentMock,
verify(srcContent: attributeAboveAnnotationComment._source,
dstContent: attributeAboveAnnotationComment.expected._source,
declType: .all)
}
}
125 changes: 78 additions & 47 deletions Tests/TestActor/FixtureActor.swift
Original file line number Diff line number Diff line change
@@ -1,63 +1,94 @@
import MockoloFramework
@Fixture enum actorProtocol {
/// @mockable
protocol Foo: Actor {
func foo(arg: String) async -> Result<String, Error>
var bar: Int { get }
}

let actorProtocol = """
/// \(String.mockAnnotation)
protocol Foo: Actor {
func foo(arg: String) async -> Result<String, Error>
var bar: Int { get }
}
"""

let actorProtocolMock = """
actor FooMock: Foo {
init() { }
init(bar: Int = 0) {
self.bar = bar
}
private(set) var fooCallCount = 0
var fooHandler: ((String) async -> Result<String, Error>)?
func foo(arg: String) async -> Result<String, Error> {
fooCallCount += 1
if let fooHandler = fooHandler {
return await fooHandler(arg)
@Fixture enum expected {
actor FooMock: Foo {
init() { }
init(bar: Int = 0) {
self.bar = bar
}
private(set) var fooCallCount = 0
var fooHandler: ((String) async -> Result<String, Error>)?
func foo(arg: String) async -> Result<String, Error> {
fooCallCount += 1
if let fooHandler = fooHandler {
return await fooHandler(arg)
}
fatalError("fooHandler returns can't have a default value thus its handler must be set")
}

var bar: Int = 0
}
fatalError("fooHandler returns can't have a default value thus its handler must be set")
}

var bar: Int = 0
}
"""

@Fixture enum parentProtocolInheritsActor {
protocol Bar: Actor {
var bar: Int { get }
}

/// @mockable
protocol Foo: Bar {
func baz(arg: String) async -> Int
}

@Fixture enum expected {
actor FooMock: Foo {
init() { }
init(bar: Int = 0) {
self.bar = bar
}

let parentProtocolInheritsActor = """
protocol Bar: Actor {
var bar: Int { get }
}

/// \(String.mockAnnotation)
protocol Foo: Bar {
func baz(arg: String) async -> Int
var bar: Int = 0

private(set) var bazCallCount = 0
var bazHandler: ((String) async -> Int)?
func baz(arg: String) async -> Int {
bazCallCount += 1
if let bazHandler = bazHandler {
return await bazHandler(arg)
}
return 0
}
}
}
}
"""

let parentProtocolInheritsActorMock = """
actor FooMock: Foo {
init() { }
init(bar: Int = 0) {
self.bar = bar
@Fixture enum attributeAboveAnnotationComment {
@MainActor
/// @mockable
protocol P0 {
}

@MainActor
/// @mockable
@available(iOS 18.0, *) protocol P1 {
}

var bar: Int = 0
@MainActor
/// @mockable
public class C0 {
init() {}
}

@Fixture enum expected {
class P0Mock: P0 {
init() { }
}

class P1Mock: P1 {
init() { }
}

private(set) var bazCallCount = 0
var bazHandler: ((String) async -> Int)?
func baz(arg: String) async -> Int {
bazCallCount += 1
if let bazHandler = bazHandler {
return await bazHandler(arg)
public class C0Mock: C0 {
override init() {
super.init()
}
}
return 0
}
}
"""
Loading
Loading