-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add integration tests for Inline/Expand Macro actions
- Validate the workflow of user calling Inline/Expand Macro actions on a swift project with macro - Add test fixture for swift macro - Verify inline macro by asserting on inlined value after calling the action - Verify expand macro by asserting expanded macro document contain the right macro Issue: #1205
- Loading branch information
1 parent
e71bbfa
commit dc2b562
Showing
5 changed files
with
252 additions
and
0 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,42 @@ | ||
// swift-tools-version:5.9 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
import CompilerPluginSupport | ||
|
||
let package = Package( | ||
name: "swift-macro", | ||
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], | ||
products: [ | ||
// Products define the executables and libraries a package produces, making them visible to other packages. | ||
.library( | ||
name: "swift-macro", | ||
targets: ["swift-macro"] | ||
), | ||
.executable( | ||
name: "swift-macroClient", | ||
targets: ["swift-macroClient"] | ||
), | ||
], | ||
dependencies: [ | ||
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest"), | ||
], | ||
targets: [ | ||
// Targets are the basic building blocks of a package, defining a module or a test suite. | ||
// Targets can depend on other targets in this package and products from dependencies. | ||
// Macro implementation that performs the source transformation of a macro. | ||
.macro( | ||
name: "swift-macroMacros", | ||
dependencies: [ | ||
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"), | ||
.product(name: "SwiftCompilerPlugin", package: "swift-syntax") | ||
] | ||
), | ||
|
||
// Library that exposes a macro as part of its API, which is used in client programs. | ||
.target(name: "swift-macro", dependencies: ["swift-macroMacros"]), | ||
|
||
// A client of the library, which is able to use the macro in its own code. | ||
.executableTarget(name: "swift-macroClient", dependencies: ["swift-macro"]), | ||
] | ||
) |
11 changes: 11 additions & 0 deletions
11
assets/test/swift-macro/Sources/swift-macro/swift_macro.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,11 @@ | ||
// The Swift Programming Language | ||
// https://docs.swift.org/swift-book | ||
|
||
/// A macro that produces both a value and a string containing the | ||
/// source code that generated the value. For example, | ||
/// | ||
/// #stringify(x + y) | ||
/// | ||
/// produces a tuple `(x + y, "x + y")`. | ||
@freestanding(expression) | ||
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "swift_macroMacros", type: "StringifyMacro") |
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,8 @@ | ||
import swift_macro | ||
|
||
let a = 17 | ||
let b = 25 | ||
|
||
let (result, code) = #stringify(a + b) | ||
|
||
print("The value \(result) was produced by the code \"\(code)\"") |
33 changes: 33 additions & 0 deletions
33
assets/test/swift-macro/Sources/swift-macroMacros/swift_macroMacro.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,33 @@ | ||
import SwiftCompilerPlugin | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacros | ||
|
||
/// Implementation of the `stringify` macro, which takes an expression | ||
/// of any type and produces a tuple containing the value of that expression | ||
/// and the source code that produced the value. For example | ||
/// | ||
/// #stringify(x + y) | ||
/// | ||
/// will expand to | ||
/// | ||
/// (x + y, "x + y") | ||
public struct StringifyMacro: ExpressionMacro { | ||
public static func expansion( | ||
of node: some FreestandingMacroExpansionSyntax, | ||
in context: some MacroExpansionContext | ||
) -> ExprSyntax { | ||
guard let argument = node.arguments.first?.expression else { | ||
fatalError("compiler bug: the macro does not have any arguments") | ||
} | ||
|
||
return "(\(argument), \(literal: argument.description))" | ||
} | ||
} | ||
|
||
@main | ||
struct swift_macroPlugin: CompilerPlugin { | ||
let providingMacros: [Macro.Type] = [ | ||
StringifyMacro.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,158 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the VS Code Swift open source project | ||
// | ||
// Copyright (c) 2024 the VS Code Swift project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import * as vscode from "vscode"; | ||
import * as langclient from "vscode-languageclient/node"; | ||
import { expect } from "chai"; | ||
import { LanguageClientManager } from "../../../src/sourcekit-lsp/LanguageClientManager"; | ||
import { WorkspaceContext } from "../../../src/WorkspaceContext"; | ||
import { folderContextPromise, globalWorkspaceContextPromise } from "../extension.test"; | ||
import { testAssetUri } from "../../fixtures"; | ||
import { FolderContext } from "../../../src/FolderContext"; | ||
import { waitForEndTaskProcess, waitForNoRunningTasks } from "../../utilities"; | ||
import { getBuildAllTask, SwiftTask } from "../../../src/tasks/SwiftTaskProvider"; | ||
import { Version } from "../../../src/utilities/version"; | ||
|
||
async function waitForClientState( | ||
languageClientManager: LanguageClientManager, | ||
expectedState: langclient.State | ||
): Promise<langclient.State> { | ||
let clientState = undefined; | ||
while (clientState !== expectedState) { | ||
clientState = await languageClientManager.useLanguageClient(async client => client.state); | ||
console.warn("Language client is not ready yet. Retrying in 100 ms..."); | ||
await new Promise(resolve => setTimeout(resolve, 100)); | ||
} | ||
return clientState; | ||
} | ||
|
||
suite("Integration, Macros Functionality Support with Sourcekit-lsp", function () { | ||
// Take around 60 seconds if running in isolation, longer than default timeout | ||
this.timeout(2 * 60 * 1000); | ||
|
||
let clientManager: LanguageClientManager; | ||
let workspaceContext: WorkspaceContext; | ||
let folderContext: FolderContext; | ||
|
||
suiteSetup(async function () { | ||
workspaceContext = await globalWorkspaceContextPromise; | ||
|
||
// Wait for a clean starting point, and build all tasks for the fixture | ||
await waitForNoRunningTasks(); | ||
folderContext = await folderContextPromise("swift-macro"); | ||
await workspaceContext.focusFolder(folderContext); | ||
const tasks = (await getBuildAllTask(folderContext)) as SwiftTask; | ||
const exitPromise = waitForEndTaskProcess(tasks); | ||
await vscode.tasks.executeTask(tasks); | ||
const exitCode = await exitPromise; | ||
expect(exitCode).to.equal(0); | ||
|
||
// Ensure lsp client is ready | ||
clientManager = workspaceContext.languageClientManager; | ||
const clientState = await waitForClientState(clientManager, langclient.State.Running); | ||
expect(clientState).to.equals(langclient.State.Running); | ||
}); | ||
|
||
test("Inline/Expand Macro", async function () { | ||
// Focus on the file of interest | ||
const uri = testAssetUri("swift-macro/Sources/swift-macroClient/main.swift"); | ||
const editor = await vscode.window.showTextDocument(uri); | ||
|
||
// Beginning of macro, # | ||
const position = new vscode.Position(5, 21); | ||
|
||
// Create a range starting and ending at the specified position | ||
const range = new vscode.Range(position, position); | ||
|
||
// Execute the code action provider command | ||
const codeActions = await vscode.commands.executeCommand<vscode.CodeAction[]>( | ||
"vscode.executeCodeActionProvider", | ||
uri, | ||
range | ||
); | ||
|
||
// Log and assert the code actions | ||
expect(codeActions).to.be.an("array"); | ||
// Expand Macro action requires Swift 6.10 | ||
if (workspaceContext.swiftVersion.isGreaterThanOrEqual(new Version(6, 1, 0))) { | ||
expect(codeActions.length).to.be.equal(2); | ||
} else { | ||
expect(codeActions.length).to.be.equal(1); | ||
} | ||
|
||
const expectedMacro = '(a + b, "a + b")'; | ||
// Loop through the code actions and execute them based on the command id | ||
for (const action of codeActions) { | ||
expect(action.command).is.not.undefined; | ||
const command = action.command!; | ||
// The id for the action is not clear, the title is "inline macro" | ||
if (command.command === "semantic.refactor.command") { | ||
// Run inline macro action | ||
await vscode.commands.executeCommand(command.command, ...(command.arguments ?? [])); | ||
|
||
// Assert that the macro was inlined correctly | ||
const endPosition = new vscode.Position(5, 37); | ||
const inlineRange = new vscode.Range(position, endPosition); | ||
const updatedText = editor.document.getText(inlineRange); | ||
expect(updatedText).to.equal(expectedMacro); | ||
|
||
// Ensure we are refocusing on the text document for the undo step | ||
await vscode.window.showTextDocument(uri); | ||
|
||
// We need to undo the inline macro or subsequent action will fail | ||
await vscode.commands.executeCommand("undo"); | ||
} else if (command.command === "expand.macro.command") { | ||
// Set up a promise that resolves when the expected document is opened | ||
const expandedMacroUriPromise = new Promise<vscode.TextDocument>( | ||
(resolve, reject) => { | ||
const disposable = vscode.workspace.onDidOpenTextDocument( | ||
openedDocument => { | ||
if (openedDocument.uri.scheme === "sourcekit-lsp") { | ||
disposable.dispose(); // Stop listening once we find the desired document | ||
resolve(openedDocument); | ||
} | ||
} | ||
); | ||
|
||
// Set a timeout to reject the promise if the document is not found | ||
setTimeout(() => { | ||
disposable.dispose(); | ||
reject( | ||
new Error( | ||
"Timed out waiting for sourcekit-lsp document to be opened." | ||
) | ||
); | ||
}, 10000); // Wait up to 10 seconds for the document | ||
} | ||
); | ||
|
||
// Run expand macro action | ||
await vscode.commands.executeCommand(command.command, ...(command.arguments ?? [])); | ||
|
||
// Wait for the expanded macro document to be opened | ||
const referenceDocument = await expandedMacroUriPromise; | ||
|
||
// Verify that the reference document was successfully opened | ||
expect(referenceDocument).to.not.be.undefined; | ||
|
||
// Assert that the content contains the expected result | ||
const content = referenceDocument.getText(); | ||
expect(content).to.include(expectedMacro); | ||
} else { | ||
// New action got added, we need to add a new test case if this is hit. | ||
expect.fail(); | ||
} | ||
} | ||
}); | ||
}); |