diff --git a/Sources/XCTestParametrizedMacro/XCTestParametrizedMacro.swift b/Sources/XCTestParametrizedMacro/XCTestParametrizedMacro.swift index 95371e4..eef7494 100644 --- a/Sources/XCTestParametrizedMacro/XCTestParametrizedMacro.swift +++ b/Sources/XCTestParametrizedMacro/XCTestParametrizedMacro.swift @@ -1,2 +1,5 @@ @attached(peer, names: arbitrary) -public macro Parametrize(input: [T]) = #externalMacro(module: "XCTestParametrizedMacroMacros", type: "ParametrizeMacro") +public macro Parametrize(input: [I]) = #externalMacro(module: "XCTestParametrizedMacroMacros", type: "ParametrizeMacro") + +@attached(peer, names: arbitrary) +public macro Parametrize(input: [I], output: [O]) = #externalMacro(module: "XCTestParametrizedMacroMacros", type: "ParametrizeMacro") diff --git a/Sources/XCTestParametrizedMacroClient/main.swift b/Sources/XCTestParametrizedMacroClient/main.swift index a9488ce..d733c95 100644 --- a/Sources/XCTestParametrizedMacroClient/main.swift +++ b/Sources/XCTestParametrizedMacroClient/main.swift @@ -1,4 +1,5 @@ import XCTestParametrizedMacro +import XCTest enum Foo: Int { case first = 1 @@ -6,9 +7,23 @@ enum Foo: Int { case third = 3 } +func pow2(_ n: Int) -> Int { + n*n +} + class Test { @Parametrize(input: [Foo.first, .second, .init(rawValue: 3)!]) func test_sample(input object: Foo) { print(object.rawValue) } + + @Parametrize(input: [1,2,3], output: [1,4,9]) + func testPow2(input n: Int, output result: Int) { + print("\(n) => \(result)") + } + + @Parametrize(input: ["Swift","SwiftMacro"], output: [5, 10]) + func testWordLength(input word: String, output length: Int) { + XCTAssertEqual(word.count, length) + } } diff --git a/Sources/XCTestParametrizedMacroMacros/MacroDeclarationHelper.swift b/Sources/XCTestParametrizedMacroMacros/MacroDeclarationHelper.swift index 8bffadb..5856109 100644 --- a/Sources/XCTestParametrizedMacroMacros/MacroDeclarationHelper.swift +++ b/Sources/XCTestParametrizedMacroMacros/MacroDeclarationHelper.swift @@ -8,6 +8,13 @@ struct MacroDeclarationHelper { self.declaration = declaration } + var funcName: TokenSyntax { + declaration.name + } + + var funcStatements: CodeBlockItemListSyntax? { + declaration.body?.statements + } /// Returns 'TokenSyntax' representing name of the input parameter. var inputParamName: TokenSyntax? { @@ -19,6 +26,16 @@ struct MacroDeclarationHelper { declaration.signature.parameterClause.parameters.first?.type } + /// Returns 'TokenSyntax' representing name of the output parameter. + var outputParamName: TokenSyntax? { + declaration.signature.parameterClause.parameters.last?.secondName + } + + /// Returns 'TokenSyntax' representing type of the output object. + var outputParamType: TypeSyntax? { + declaration.signature.parameterClause.parameters.last?.type + } + var firstAttribute: AttributeSyntax? { return declaration.attributes.first?.as(AttributeSyntax.self) } @@ -37,4 +54,21 @@ struct MacroDeclarationHelper { } } + var outputValues: ArrayElementListSyntax? { + get throws { + guard let firstMacroArgument = firstAttribute?.arguments?.as(LabeledExprListSyntax.self) else { + throw ParametrizeMacroError.macroAttributeNotAnArray + } + + guard let outputArgument = firstMacroArgument.first(where: { $0.label?.text == "output" }) else { + return nil + } + + guard let arrayOfValues = outputArgument.as(LabeledExprSyntax.self)?.expression.as(ArrayExprSyntax.self)?.elements else { + throw ParametrizeMacroError.macroAttributeNotAnArray + } + + return arrayOfValues + } + } } diff --git a/Sources/XCTestParametrizedMacroMacros/ParametrizeMacroError.swift b/Sources/XCTestParametrizedMacroMacros/ParametrizeMacroError.swift index 61d3cd3..a4943c2 100644 --- a/Sources/XCTestParametrizedMacroMacros/ParametrizeMacroError.swift +++ b/Sources/XCTestParametrizedMacroMacros/ParametrizeMacroError.swift @@ -6,6 +6,7 @@ enum ParametrizeMacroError: Error, CustomStringConvertible { case functionInputParamTypeMissing case functionBodyEmpty case macroAttributeNotAnArray + case macroAttributeArraysMismatchSize var description: String { switch self { @@ -18,7 +19,9 @@ enum ParametrizeMacroError: Error, CustomStringConvertible { case .functionBodyEmpty: return "Function must have a body." case .macroAttributeNotAnArray: - return "Parametrize macro requires at least one attribute as array of input values." + return "Parametrize macro requires at least one attribute as array of input/output values." + case .macroAttributeArraysMismatchSize: + return "Size of the input array and output array should be the same." } } } diff --git a/Sources/XCTestParametrizedMacroMacros/TestMethodsFactory.swift b/Sources/XCTestParametrizedMacroMacros/TestMethodsFactory.swift new file mode 100644 index 0000000..a301d17 --- /dev/null +++ b/Sources/XCTestParametrizedMacroMacros/TestMethodsFactory.swift @@ -0,0 +1,95 @@ +import Foundation +import SwiftSyntax + +struct TestMethodsFactory { + + let macroDeclarationHelper: MacroDeclarationHelper + + var bodyFunc: String { + get throws { + guard let codeStatements = macroDeclarationHelper.funcStatements, codeStatements.count > 0 else { + throw ParametrizeMacroError.functionBodyEmpty + } + return codeStatements.map { "\($0.trimmed)" }.joined(separator: "\n") + } + } + + func create() throws -> [DeclSyntax] { + + let funcName = macroDeclarationHelper.funcName + + guard let inputParamName = macroDeclarationHelper.inputParamName?.text else { + throw ParametrizeMacroError.functionInputParamSecondNameMissing + } + + guard let inputParamType = macroDeclarationHelper.inputParamType else { + throw ParametrizeMacroError.functionInputParamTypeMissing + } + + let outputParamName = macroDeclarationHelper.outputParamName?.text + let outputParamType = macroDeclarationHelper.outputParamType + + let outputValues = try macroDeclarationHelper.outputValues + let inputValues = try macroDeclarationHelper.inputValues + if let outputValues = outputValues, + let outputParamName = outputParamName, + let outputParamType = outputParamType { + let input = inputValues.map { $0 } + let output = outputValues.map { $0 } + guard input.count == output.count else { + throw ParametrizeMacroError.macroAttributeArraysMismatchSize + } + return try zip(input, output).map { input, output in + """ + \(raw: buildTestMethodSignature(funcName: funcName, inputParamName: inputParamName, inputObject: input, outputParamName: outputParamName, outputObject: output)) + \(raw: buildLocalVariables(inputParamName: inputParamName, + inputParamType: inputParamType, + inputObject: input, + outputParamName: outputParamName, + outputParamType: outputParamType, + outputObject: output)) + \(raw: try bodyFunc) + } + """ + } + } else { + return try inputValues + .map { + """ + \(raw: buildTestMethodSignature(funcName: funcName, inputParamName: inputParamName, inputObject: $0)) + \(raw: buildLocalVariables(inputParamName: inputParamName, inputParamType: inputParamType, inputObject: $0)) + \(raw: try bodyFunc) + } + """ + } + } + } + + func buildTestMethodSignature(funcName: TokenSyntax, + inputParamName: String, + inputObject: ArrayElementListSyntax.Element, + outputParamName: String? = nil, + outputObject: ArrayElementListSyntax.Element? = nil) -> String { + if let outputParamName = outputParamName, let outputObject = outputObject { + return "func \(funcName)_\(inputParamName.capitalizedFirst)_\(inputObject.asFunctionName)_\(outputParamName.capitalizedFirst)_\(outputObject.asFunctionName)() throws {" + } else { + return "func \(funcName)_\(inputParamName.capitalizedFirst)_\(inputObject.asFunctionName)() throws {" + } + } + + func buildLocalVariables(inputParamName: String, + inputParamType: TypeSyntax, + inputObject: ArrayElementListSyntax.Element, + outputParamName: String? = nil, + outputParamType: TypeSyntax? = nil, + outputObject: ArrayElementListSyntax.Element? = nil) -> String { + var decl = "let \(inputParamName):\(inputParamType) = \(inputObject.expression)" + if let outputParamName = outputParamName, + let outputParamType = outputParamType, + let outputObject = outputObject { + decl.append("\n") + decl.append("let \(outputParamName):\(outputParamType) = \(outputObject.expression)") + } + return decl + } +} diff --git a/Sources/XCTestParametrizedMacroMacros/XCTestParametrizedMacroMacro.swift b/Sources/XCTestParametrizedMacroMacros/XCTestParametrizedMacroMacro.swift index ea51edc..4e44e59 100644 --- a/Sources/XCTestParametrizedMacroMacros/XCTestParametrizedMacroMacro.swift +++ b/Sources/XCTestParametrizedMacroMacros/XCTestParametrizedMacroMacro.swift @@ -17,28 +17,7 @@ public struct ParametrizeMacro: PeerMacro { let macroDeclarationHelper = MacroDeclarationHelper(declaration) - let funcName = declaration.name - guard let inputParamName = macroDeclarationHelper.inputParamName?.text else { - throw ParametrizeMacroError.functionInputParamSecondNameMissing - } - - guard let inputParamType = macroDeclarationHelper.inputParamType else { - throw ParametrizeMacroError.functionInputParamTypeMissing - } - - guard let codeStatements = declaration.body?.statements, codeStatements.count > 0 else { - throw ParametrizeMacroError.functionBodyEmpty - } - - let textCode = codeStatements.map { "\($0.trimmed)" }.joined(separator: "\n") - return try macroDeclarationHelper.inputValues.map { - """ - func \(funcName)_\(raw: inputParamName.capitalizedFirst)_\(raw: $0.asFunctionName)() throws { - let \(raw: inputParamName):\(raw: inputParamType) = \($0.expression) - \(raw: textCode) - } - """ - } + return try TestMethodsFactory(macroDeclarationHelper: macroDeclarationHelper).create() } } diff --git a/Tests/XCTestParametrizedMacroTests/AttachmentTests.swift b/Tests/XCTestParametrizedMacroTests/AttachmentTests.swift index 24bfab3..c81a4bf 100644 --- a/Tests/XCTestParametrizedMacroTests/AttachmentTests.swift +++ b/Tests/XCTestParametrizedMacroTests/AttachmentTests.swift @@ -131,7 +131,7 @@ final class AttachmentTests: XCTestCase { } """, diagnostics: [ - DiagnosticSpec(message: "Parametrize macro requires at least one attribute as array of input values.", line: 2, column: 5) + DiagnosticSpec(message: "Parametrize macro requires at least one attribute as array of input/output values.", line: 2, column: 5) ], macros: testMacros ) @@ -157,10 +157,33 @@ final class AttachmentTests: XCTestCase { } """, diagnostics: [ - DiagnosticSpec(message: "Parametrize macro requires at least one attribute as array of input values.", line: 2, column: 5) + DiagnosticSpec(message: "Parametrize macro requires at least one attribute as array of input/output values.", line: 2, column: 5) ], macros: testMacros ) } + func testParametrizeInputOutput_DifferentSizeOfArrays_ShouldFail() throws { + assertMacroExpansion( + """ + struct TestStruct { + @Parametrize(input: [1,2,3], output: [1,4]) + func testPow2(input n: Int, output result: Int) { + XCTAssertEqual(pow2(n), result) + } + } + """, + expandedSource: """ + struct TestStruct { + func testPow2(input n: Int, output result: Int) { + XCTAssertEqual(pow2(n), result) + } + } + """, + diagnostics: [ + DiagnosticSpec(message: "Size of the input array and output array should be the same.", line: 2, column: 5) + ], + macros: testMacros + ) + } } diff --git a/Tests/XCTestParametrizedMacroTests/InputOutputParametersTests.swift b/Tests/XCTestParametrizedMacroTests/InputOutputParametersTests.swift new file mode 100644 index 0000000..efe85fc --- /dev/null +++ b/Tests/XCTestParametrizedMacroTests/InputOutputParametersTests.swift @@ -0,0 +1,105 @@ +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +import XCTestParametrizedMacroMacros + +final class InputOutputParametersTests: XCTestCase { + + let testMacros: [String: Macro.Type] = [ + "Parametrize": ParametrizeMacro.self, + ] + + func testParametrizeInputOutput_SingleInts() throws { + assertMacroExpansion( + """ + struct TestStruct { + @Parametrize(input: [3], output: [9]) + func testPow2(input n: Int, output result: Int) { + XCTAssertEqual(pow2(n),result) + } + } + """, + expandedSource: """ + struct TestStruct { + func testPow2(input n: Int, output result: Int) { + XCTAssertEqual(pow2(n),result) + } + + func testPow2_N_3_Result_9() throws { + let n: Int = 3 + let result: Int = 9 + XCTAssertEqual(pow2(n), result) + } + } + """, + macros: testMacros + ) + } + + func testParametrizeInputOutput_TwoInts() throws { + assertMacroExpansion( + """ + struct TestStruct { + @Parametrize(input: [4, 5], output: [16, 25]) + func testPow2(input n: Int, output result: Int) { + XCTAssertEqual(pow2(n),result) + } + } + """, + expandedSource: """ + struct TestStruct { + func testPow2(input n: Int, output result: Int) { + XCTAssertEqual(pow2(n),result) + } + + func testPow2_N_4_Result_16() throws { + let n: Int = 4 + let result: Int = 16 + XCTAssertEqual(pow2(n), result) + } + + func testPow2_N_5_Result_25() throws { + let n: Int = 5 + let result: Int = 25 + XCTAssertEqual(pow2(n), result) + } + } + """, + macros: testMacros + ) + } + + func testParametrizeInputOutput_TwoStringsTwoInts() throws { + assertMacroExpansion( + """ + struct TestStruct { + @Parametrize(input: ["Swift", "SwiftMacro"], output: [5, 10]) + func testWordLength(input word: String, output length: Int) { + XCTAssertEqual(word.count, length) + } + } + """, + expandedSource: """ + struct TestStruct { + func testWordLength(input word: String, output length: Int) { + XCTAssertEqual(word.count, length) + } + + func testWordLength_Word_Swift_Length_5() throws { + let word: String = "Swift" + let length: Int = 5 + XCTAssertEqual(word.count, length) + } + + func testWordLength_Word_SwiftMacro_Length_10() throws { + let word: String = "SwiftMacro" + let length: Int = 10 + XCTAssertEqual(word.count, length) + } + } + """, + macros: testMacros + ) + } +} diff --git a/Tests/XCTestParametrizedMacroTests/SimpleValuesTests.swift b/Tests/XCTestParametrizedMacroTests/SimpleValuesTests.swift index ab52605..522df47 100644 --- a/Tests/XCTestParametrizedMacroTests/SimpleValuesTests.swift +++ b/Tests/XCTestParametrizedMacroTests/SimpleValuesTests.swift @@ -270,4 +270,5 @@ final class SimpleValuesTests: XCTestCase { macros: testMacros ) } + }