Skip to content

Update macro template to support Swift Testing tests #8890 #8897

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
100 changes: 78 additions & 22 deletions Sources/Workspace/InitPackage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,6 @@ public final class InitPackage {
installedSwiftPMConfiguration: InstalledSwiftPMConfiguration,
fileSystem: FileSystem
) throws {
if options.packageType == .macro && options.supportedTestingLibraries.contains(.swiftTesting) {
// FIXME: https://github.com/swiftlang/swift-syntax/issues/2400
throw InitError.unsupportedTestingLibraryForPackageType(.swiftTesting, .macro)
}

self.options = options
self.pkgname = name
self.moduleName = name.spm_mangledToC99ExtendedIdentifier()
Expand Down Expand Up @@ -346,7 +341,6 @@ public final class InitPackage {
dependencies: [
"\(pkgname)Macros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
.product(name: "Testing", package: "swift-testing"),
]
),
"""
Expand Down Expand Up @@ -726,15 +720,22 @@ public final class InitPackage {
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import SwiftSyntaxMacroExpansion
Copy link

@vanvoorden vanvoorden Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should also import SwiftSyntaxMacrosGenericTestSupport?

Ahh… nevermind… I see it below!


"""##

if options.supportedTestingLibraries.contains(.swiftTesting) {
content += "import Testing\n"
}
if options.supportedTestingLibraries.contains(.xctest) {
content += "import XCTest\n"
content += ##"""
import Testing
import SwiftSyntaxMacrosGenericTestSupport

"""##
} else if options.supportedTestingLibraries.contains(.xctest) {
content += ##"""
import XCTest
import SwiftSyntaxMacrosTestSupport

"""##
}

content += ##"""
Expand All @@ -743,8 +744,8 @@ public final class InitPackage {
#if canImport(\##(moduleName)Macros)
import \##(moduleName)Macros

let testMacros: [String: Macro.Type] = [
"stringify": StringifyMacro.self,
let testMacros: [String: MacroSpec] = [
"stringify": MacroSpec(type: StringifyMacro.self),
]
#endif

Expand All @@ -755,10 +756,68 @@ public final class InitPackage {
// for it *and* Testing if it is enabled.

if options.supportedTestingLibraries.contains(.swiftTesting) {
// FIXME: https://github.com/swiftlang/swift-syntax/issues/2400
}

if options.supportedTestingLibraries.contains(.xctest) {
content += ##"""
struct \##(moduleName)Tests {

@Test
func macro() {
#if canImport(\##(moduleName)Macros)
assertMacroExpansion(
"""
#stringify(a + b)
""",
expandedSource: """
(a + b, "a + b")
""",
macroSpecs: testMacros
) {
Issue.record(
"\($0.message)",
sourceLocation:
SourceLocation(
fileID: $0.location.fileID,
filePath: $0.location.filePath,
line: $0.location.line,
column: $0.location.column
)
)
Comment on lines +774 to +783

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nit: Could this be refactored to an extension on Issue? Then we can put this work in one place instead of duplicated it across N different tests?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extension Issue {
  @discardableResult static func record(_ failure: TestFailureSpec) -> Self {
    Self.record(
      Comment(rawValue: failure.message),
      sourceLocation: SourceLocation(
        fileID: failure.location.fileID,
        filePath: failure.location.filePath,
        line: failure.location.line,
        column: failure.location.column
      )
    )
  }
}

cc @grynspan Was there anything blocking us from shipping that extension directly in swift-testing? Or is there another canonical way to bridge between SwiftSyntaxMacrosGenericTestSupport.TestFailureSpec and Testing.Issue?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also choose to factor this to a free function:

func failureHander(_ failure: TestFailureSpec) {
  Issue.record(
    Comment(rawValue: failure.message),
    sourceLocation:
      SourceLocation(
        fileID: failure.location.fileID,
        filePath: failure.location.filePath,
        line: failure.location.line,
        column: failure.location.column
      )
  )
}

And then pass that failureHander to every test similar to how we pass testMacros.

Copy link
Contributor

@grynspan grynspan Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swift Testing cannot directly link to swift-syntax since it doesn't exist in the toolchain at runtime. That means we cannot export any symbols that reference types from swift-syntax (except in our macro target which is only loaded/run at compile time.)

}
#else
Issue.record("macros are only supported when running tests for the host platform")
#endif
}

@Test
func macroWithStringLiteral() {
#if canImport(\##(moduleName)Macros)
assertMacroExpansion(
#"""
#stringify("Hello, \(name)")
"""#,
expandedSource: #"""
("Hello, \(name)", #""Hello, \(name)""#)
"""#,
macroSpecs: testMacros
) {
Issue.record(
"\($0.message)",
sourceLocation:
SourceLocation(
fileID: $0.location.fileID,
filePath: $0.location.filePath,
line: $0.location.line,
column: $0.location.column
)
)
}
#else
Issue.record("macros are only supported when running tests for the host platform")
#endif
}
}

"""##
} else if options.supportedTestingLibraries.contains(.xctest) {
content += ##"""
final class \##(moduleName)Tests: XCTestCase {
func testMacro() throws {
Expand All @@ -770,7 +829,7 @@ public final class InitPackage {
expandedSource: """
(a + b, "a + b")
""",
macros: testMacros
macroSpecs: testMacros
)
#else
throw XCTSkip("macros are only supported when running tests for the host platform")
Expand All @@ -786,7 +845,7 @@ public final class InitPackage {
expandedSource: #"""
("Hello, \(name)", #""Hello, \(name)""#)
"""#,
macros: testMacros
macroSpecs: testMacros
)
#else
throw XCTSkip("macros are only supported when running tests for the host platform")
Expand Down Expand Up @@ -887,16 +946,13 @@ public final class InitPackage {

private enum InitError: Swift.Error {
case manifestAlreadyExists
case unsupportedTestingLibraryForPackageType(_ testingLibrary: TestingLibrary, _ packageType: InitPackage.PackageType)
}

extension InitError: CustomStringConvertible {
var description: String {
switch self {
case .manifestAlreadyExists:
return "a manifest file already exists in this directory"
case let .unsupportedTestingLibraryForPackageType(library, packageType):
return "\(library) cannot be used when initializing a \(packageType) package"
}
}
}
Expand Down
88 changes: 88 additions & 0 deletions Tests/WorkspaceTests/InitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,94 @@ final class InitTests: XCTestCase {
}
}

func testInitPackageMacroWithXCTest() throws {
try testWithTemporaryDirectory { tmpPath in
let fs = localFileSystem
let path = tmpPath.appending("Foo")
let name = path.basename
try fs.createDirectory(path)

// Create the package
try InitPackage(
name: name,
packageType: .macro,
supportedTestingLibraries: [.xctest],
destinationPath: path,
fileSystem: localFileSystem
).writePackageStructure()

// Verify basic file system content that we expect in the package
let manifest = path.appending("Package.swift")
XCTAssertFileExists(manifest)
let manifestContents: String = try localFileSystem.readFileContents(manifest)


XCTAssertMatch(manifestContents, .and(.contains(".executable("), .contains("targets: [\"FooClient\"]")))
XCTAssertMatch(manifestContents, .and(.contains(".macro("), .contains("name: \"FooMacros\"")))
XCTAssertMatch(manifestContents, .contains(".executableTarget(name: \"FooClient\", dependencies: [\"Foo\"]),"))

let sourceLibrary = path.appending("Sources", "Foo", "Foo.swift")
XCTAssertFileExists(sourceLibrary)
let sourceClient = path.appending("Sources", "FooClient", "main.swift")
XCTAssertFileExists(sourceClient)
let sourceMacros = path.appending("Sources", "FooMacros", "FooMacro.swift")
XCTAssertFileExists(sourceMacros)

let sourceTests = path.appending("Tests", "FooTests", "FooTests.swift")
XCTAssertFileExists(sourceTests)
let sourceTestsContents: String = try localFileSystem.readFileContents(sourceTests)
XCTAssertMatch(sourceTestsContents, .contains("import XCTest"))
XCTAssertMatch(sourceTestsContents, .contains("import SwiftSyntaxMacrosTestSupport"))
XCTAssertNoMatch(sourceTestsContents, .contains("import Testing"))
XCTAssertNoMatch(sourceTestsContents, .contains("import SwiftSyntaxMacrosGenericTestSupport"))
XCTAssertMatch(sourceTestsContents, .contains("final class FooTests: XCTestCase {"))
}
}

func testInitPackageMacroWithSwiftTesting() throws {
try testWithTemporaryDirectory { tmpPath in
let fs = localFileSystem
let path = tmpPath.appending("Foo")
let name = path.basename
try fs.createDirectory(path)

// Create the package
try InitPackage(
name: name,
packageType: .macro,
supportedTestingLibraries: [.swiftTesting],
destinationPath: path,
fileSystem: localFileSystem
).writePackageStructure()

// Verify basic file system content that we expect in the package
let manifest = path.appending("Package.swift")
XCTAssertFileExists(manifest)
let manifestContents: String = try localFileSystem.readFileContents(manifest)


XCTAssertMatch(manifestContents, .and(.contains(".executable("), .contains("targets: [\"FooClient\"]")))
XCTAssertMatch(manifestContents, .and(.contains(".macro("), .contains("name: \"FooMacros\"")))
XCTAssertMatch(manifestContents, .contains(".executableTarget(name: \"FooClient\", dependencies: [\"Foo\"]),"))

let sourceLibrary = path.appending("Sources", "Foo", "Foo.swift")
XCTAssertFileExists(sourceLibrary)
let sourceClient = path.appending("Sources", "FooClient", "main.swift")
XCTAssertFileExists(sourceClient)
let sourceMacros = path.appending("Sources", "FooMacros", "FooMacro.swift")
XCTAssertFileExists(sourceMacros)

let sourceTests = path.appending("Tests", "FooTests", "FooTests.swift")
XCTAssertFileExists(sourceTests)
let sourceTestsContents: String = try localFileSystem.readFileContents(sourceTests)
XCTAssertMatch(sourceTestsContents, .contains("import Testing"))
XCTAssertMatch(sourceTestsContents, .contains("import SwiftSyntaxMacrosGenericTestSupport"))
XCTAssertNoMatch(sourceTestsContents, .contains("import XCTest"))
XCTAssertNoMatch(sourceTestsContents, .contains("import SwiftSyntaxMacrosTestSupport"))
XCTAssertMatch(sourceTestsContents, .contains("struct FooTests {"))
}
}

// MARK: Special case testing

func testInitPackageNonc99Directory() async throws {
Expand Down