-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add MockFunction and Mock macros
- Loading branch information
1 parent
2c537aa
commit e96d8a0
Showing
22 changed files
with
1,097 additions
and
536 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
// | ||
// Created on 6/18/24. | ||
// Copyright © 2024 Turo Open Source. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
/// Mocks the body of a function, intended for internal use only. | ||
@attached(body) | ||
public macro _MockFunction() = #externalMacro(module: "TestDRSMacros", type: "MockFunctionMacro") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// | ||
// Created on 6/20/24. | ||
// Copyright © 2024 Turo Open Source. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
/// This macro mocks the members of the struct or class that it is applied to. | ||
/// Properties of the attached type should not be initialized inline (using an initializer is allowed, but not required) | ||
/// and functions should not have bodies. | ||
/// In most circumstances the attached type should conform to a protocol, | ||
/// so an easy way to generate the member definitions is to just apply the fixit that pops up after adopting the protocol | ||
/// and then delete the empty bodies of the functions. | ||
/// | ||
/// This macro adds conformance to the `StubProviding` and `Spy` protocols. This allows you to stub out each method and expect that methods were called in your tests. | ||
@attached(extension, conformances: Mock) | ||
@attached(member, conformances: Mock, names: named(blackBox), named(stubRegistry)) | ||
@attached(memberAttribute) | ||
public macro Mock() = #externalMacro(module: "TestDRSMacros", type: "MockMacro") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
Sources/TestDRSMacros/Mocking/MockExpansionDiagnostic.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// | ||
// Created on 6/25/24. | ||
// Copyright © 2024 Turo Open Source. All rights reserved. | ||
// | ||
|
||
import SwiftDiagnostics | ||
|
||
enum MockExpansionDiagnostic: String, DiagnosticMessage { | ||
|
||
case invalidType | ||
case propertyWithInitializer | ||
case functionWithBody | ||
|
||
var message: String { | ||
switch self { | ||
case .invalidType: | ||
"@Mock can only be applied to classes and structs" | ||
case .propertyWithInitializer: | ||
"@Mock can't be applied to types containing properties that have initializers" | ||
case .functionWithBody: | ||
"@Mock can't be applied to types containing functions that have bodies" | ||
} | ||
} | ||
|
||
var diagnosticID: MessageID { | ||
MessageID(domain: Self.moduleDomain, id: "\(Self.self).\(rawValue)") | ||
} | ||
|
||
var severity: DiagnosticSeverity { .error } | ||
|
||
} |
25 changes: 25 additions & 0 deletions
25
Sources/TestDRSMacros/Mocking/MockFunctionExpansionDiagnostic.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// | ||
// Created on 6/18/24. | ||
// Copyright © 2024 Turo Open Source. All rights reserved. | ||
// | ||
|
||
import SwiftDiagnostics | ||
|
||
enum MockFunctionExpansionDiagnostic: String, DiagnosticMessage { | ||
|
||
case existingBody | ||
|
||
var message: String { | ||
switch self { | ||
case .existingBody: | ||
"@_MockFunction can only be applied to function without an existing body as the function will be mocked and any existing body would just be ignored anyway." | ||
} | ||
} | ||
|
||
var diagnosticID: MessageID { | ||
MessageID(domain: Self.moduleDomain, id: "\(Self.self).\(rawValue)") | ||
} | ||
|
||
var severity: DiagnosticSeverity { .error } | ||
|
||
} |
72 changes: 72 additions & 0 deletions
72
Sources/TestDRSMacros/Mocking/MockFunctionMacroExpansion.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
// | ||
// Created on 6/18/24. | ||
// Copyright © 2024 Turo Open Source. All rights reserved. | ||
// | ||
|
||
import SwiftDiagnostics | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacros | ||
|
||
public struct MockFunctionMacro: BodyMacro { | ||
|
||
public static func expansion( | ||
of node: SwiftSyntax.AttributeSyntax, | ||
providingBodyFor declaration: some SwiftSyntax.DeclSyntaxProtocol & SwiftSyntax.WithOptionalCodeBlockSyntax, | ||
in context: some SwiftSyntaxMacros.MacroExpansionContext | ||
) throws -> [SwiftSyntax.CodeBlockItemSyntax] { | ||
if let method = declaration as? FunctionDeclSyntax { | ||
guard method.body == nil else { | ||
context.diagnose( | ||
Diagnostic( | ||
node: Syntax(node), | ||
message: MockFunctionExpansionDiagnostic.existingBody | ||
) | ||
) | ||
return [] | ||
} | ||
return mockMethodBody(for: method).statements.map { $0 } | ||
} | ||
|
||
return [] | ||
} | ||
|
||
static func mockMethodBody(for method: FunctionDeclSyntax) -> CodeBlockSyntax { | ||
CodeBlockSyntax( | ||
leftBrace: .leftBraceToken(), | ||
statements: CodeBlockItemListSyntax { | ||
recordCallSyntax(for: method) | ||
ReturnStmtSyntax(expression: stubOutputSyntax(for: method)) | ||
}, | ||
rightBrace: .rightBraceToken() | ||
) | ||
} | ||
|
||
private static func stubOutputSyntax(for method: FunctionDeclSyntax) -> ExprSyntax { | ||
FunctionCallExprSyntax(callee: ExprSyntax(stringLiteral: method.isThrowing ? "throwingStubOutput" : "stubOutput")) { | ||
if method.hasParameters { | ||
LabeledExprSyntax( | ||
label: "for", | ||
expression: method.inputParameters | ||
) | ||
} | ||
} | ||
.wrappedInTry(method.isThrowing) | ||
} | ||
|
||
private static func recordCallSyntax(for method: FunctionDeclSyntax) -> FunctionCallExprSyntax { | ||
FunctionCallExprSyntax(callee: ExprSyntax(stringLiteral: "recordCall")) { | ||
if method.hasParameters { | ||
LabeledExprSyntax(label: "with", expression: method.inputParameters) | ||
} | ||
|
||
if let returnClause = method.signature.returnClause { | ||
LabeledExprSyntax( | ||
label: "returning", | ||
expression: ExprSyntax(stringLiteral: "\(returnClause.type.trimmed).self") | ||
) | ||
} | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
// | ||
// Created on 6/20/24. | ||
// Copyright © 2024 Turo Open Source. All rights reserved. | ||
// | ||
|
||
import SwiftDiagnostics | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacros | ||
|
||
public struct MockMacro: ExtensionMacro, MemberMacro, MemberAttributeMacro { | ||
|
||
public static func expansion( | ||
of node: SwiftSyntax.AttributeSyntax, | ||
attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, | ||
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, | ||
conformingTo protocols: [SwiftSyntax.TypeSyntax], | ||
in context: some SwiftSyntaxMacros.MacroExpansionContext | ||
) throws -> [SwiftSyntax.ExtensionDeclSyntax] { | ||
guard declarationTypeIsSupported(declaration) else { | ||
context.diagnose( | ||
Diagnostic( | ||
node: Syntax(node), | ||
message: MockExpansionDiagnostic.invalidType | ||
) | ||
) | ||
return [] | ||
} | ||
|
||
let inheritedType = InheritedTypeSyntax(type: TypeSyntax(stringLiteral: "Mock")) | ||
let typeList = InheritedTypeListSyntax(arrayLiteral: inheritedType) | ||
let inheritanceClause = InheritanceClauseSyntax(inheritedTypes: typeList) | ||
|
||
return [ | ||
ExtensionDeclSyntax(extendedType: type, inheritanceClause: inheritanceClause) {} | ||
] | ||
} | ||
|
||
public static func expansion( | ||
of node: SwiftSyntax.AttributeSyntax, | ||
attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, | ||
providingAttributesFor member: some SwiftSyntax.DeclSyntaxProtocol, | ||
in context: some SwiftSyntaxMacros.MacroExpansionContext | ||
) throws -> [SwiftSyntax.AttributeSyntax] { | ||
guard declarationTypeIsSupported(declaration) else { return [] } | ||
|
||
if let variable = member.as(VariableDeclSyntax.self) { | ||
guard variable.bindings.allSatisfy({ $0.initializer == nil }) else { | ||
context.diagnose( | ||
Diagnostic( | ||
node: Syntax(variable), | ||
message: MockExpansionDiagnostic.propertyWithInitializer | ||
) | ||
) | ||
return [] | ||
} | ||
return ["@_MockProperty"] | ||
} else if let function = member.as(FunctionDeclSyntax.self) { | ||
guard function.body == nil else { | ||
context.diagnose( | ||
Diagnostic( | ||
node: Syntax(function), | ||
message: MockExpansionDiagnostic.functionWithBody | ||
) | ||
) | ||
return [] | ||
} | ||
return ["@_MockFunction"] | ||
} | ||
|
||
return [] | ||
} | ||
|
||
public static func expansion(of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext) throws -> [DeclSyntax] { | ||
guard declarationTypeIsSupported(declaration) else { return [] } | ||
|
||
return ["let blackBox = BlackBox()", "let stubRegistry = StubRegistry()"] | ||
} | ||
|
||
private static func declarationTypeIsSupported(_ declaration: some DeclGroupSyntax) -> Bool { | ||
declaration.is(ClassDeclSyntax.self) || declaration.is(StructDeclSyntax.self) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.