Skip to content

Commit dc2b562

Browse files
committed
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
1 parent e71bbfa commit dc2b562

File tree

5 files changed

+252
-0
lines changed

5 files changed

+252
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// swift-tools-version:5.9
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
import CompilerPluginSupport
6+
7+
let package = Package(
8+
name: "swift-macro",
9+
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
10+
products: [
11+
// Products define the executables and libraries a package produces, making them visible to other packages.
12+
.library(
13+
name: "swift-macro",
14+
targets: ["swift-macro"]
15+
),
16+
.executable(
17+
name: "swift-macroClient",
18+
targets: ["swift-macroClient"]
19+
),
20+
],
21+
dependencies: [
22+
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest"),
23+
],
24+
targets: [
25+
// Targets are the basic building blocks of a package, defining a module or a test suite.
26+
// Targets can depend on other targets in this package and products from dependencies.
27+
// Macro implementation that performs the source transformation of a macro.
28+
.macro(
29+
name: "swift-macroMacros",
30+
dependencies: [
31+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
32+
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
33+
]
34+
),
35+
36+
// Library that exposes a macro as part of its API, which is used in client programs.
37+
.target(name: "swift-macro", dependencies: ["swift-macroMacros"]),
38+
39+
// A client of the library, which is able to use the macro in its own code.
40+
.executableTarget(name: "swift-macroClient", dependencies: ["swift-macro"]),
41+
]
42+
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// The Swift Programming Language
2+
// https://docs.swift.org/swift-book
3+
4+
/// A macro that produces both a value and a string containing the
5+
/// source code that generated the value. For example,
6+
///
7+
/// #stringify(x + y)
8+
///
9+
/// produces a tuple `(x + y, "x + y")`.
10+
@freestanding(expression)
11+
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "swift_macroMacros", type: "StringifyMacro")
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import swift_macro
2+
3+
let a = 17
4+
let b = 25
5+
6+
let (result, code) = #stringify(a + b)
7+
8+
print("The value \(result) was produced by the code \"\(code)\"")
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import SwiftCompilerPlugin
2+
import SwiftSyntax
3+
import SwiftSyntaxBuilder
4+
import SwiftSyntaxMacros
5+
6+
/// Implementation of the `stringify` macro, which takes an expression
7+
/// of any type and produces a tuple containing the value of that expression
8+
/// and the source code that produced the value. For example
9+
///
10+
/// #stringify(x + y)
11+
///
12+
/// will expand to
13+
///
14+
/// (x + y, "x + y")
15+
public struct StringifyMacro: ExpressionMacro {
16+
public static func expansion(
17+
of node: some FreestandingMacroExpansionSyntax,
18+
in context: some MacroExpansionContext
19+
) -> ExprSyntax {
20+
guard let argument = node.arguments.first?.expression else {
21+
fatalError("compiler bug: the macro does not have any arguments")
22+
}
23+
24+
return "(\(argument), \(literal: argument.description))"
25+
}
26+
}
27+
28+
@main
29+
struct swift_macroPlugin: CompilerPlugin {
30+
let providingMacros: [Macro.Type] = [
31+
StringifyMacro.self,
32+
]
33+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2024 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import * as vscode from "vscode";
16+
import * as langclient from "vscode-languageclient/node";
17+
import { expect } from "chai";
18+
import { LanguageClientManager } from "../../../src/sourcekit-lsp/LanguageClientManager";
19+
import { WorkspaceContext } from "../../../src/WorkspaceContext";
20+
import { folderContextPromise, globalWorkspaceContextPromise } from "../extension.test";
21+
import { testAssetUri } from "../../fixtures";
22+
import { FolderContext } from "../../../src/FolderContext";
23+
import { waitForEndTaskProcess, waitForNoRunningTasks } from "../../utilities";
24+
import { getBuildAllTask, SwiftTask } from "../../../src/tasks/SwiftTaskProvider";
25+
import { Version } from "../../../src/utilities/version";
26+
27+
async function waitForClientState(
28+
languageClientManager: LanguageClientManager,
29+
expectedState: langclient.State
30+
): Promise<langclient.State> {
31+
let clientState = undefined;
32+
while (clientState !== expectedState) {
33+
clientState = await languageClientManager.useLanguageClient(async client => client.state);
34+
console.warn("Language client is not ready yet. Retrying in 100 ms...");
35+
await new Promise(resolve => setTimeout(resolve, 100));
36+
}
37+
return clientState;
38+
}
39+
40+
suite("Integration, Macros Functionality Support with Sourcekit-lsp", function () {
41+
// Take around 60 seconds if running in isolation, longer than default timeout
42+
this.timeout(2 * 60 * 1000);
43+
44+
let clientManager: LanguageClientManager;
45+
let workspaceContext: WorkspaceContext;
46+
let folderContext: FolderContext;
47+
48+
suiteSetup(async function () {
49+
workspaceContext = await globalWorkspaceContextPromise;
50+
51+
// Wait for a clean starting point, and build all tasks for the fixture
52+
await waitForNoRunningTasks();
53+
folderContext = await folderContextPromise("swift-macro");
54+
await workspaceContext.focusFolder(folderContext);
55+
const tasks = (await getBuildAllTask(folderContext)) as SwiftTask;
56+
const exitPromise = waitForEndTaskProcess(tasks);
57+
await vscode.tasks.executeTask(tasks);
58+
const exitCode = await exitPromise;
59+
expect(exitCode).to.equal(0);
60+
61+
// Ensure lsp client is ready
62+
clientManager = workspaceContext.languageClientManager;
63+
const clientState = await waitForClientState(clientManager, langclient.State.Running);
64+
expect(clientState).to.equals(langclient.State.Running);
65+
});
66+
67+
test("Inline/Expand Macro", async function () {
68+
// Focus on the file of interest
69+
const uri = testAssetUri("swift-macro/Sources/swift-macroClient/main.swift");
70+
const editor = await vscode.window.showTextDocument(uri);
71+
72+
// Beginning of macro, #
73+
const position = new vscode.Position(5, 21);
74+
75+
// Create a range starting and ending at the specified position
76+
const range = new vscode.Range(position, position);
77+
78+
// Execute the code action provider command
79+
const codeActions = await vscode.commands.executeCommand<vscode.CodeAction[]>(
80+
"vscode.executeCodeActionProvider",
81+
uri,
82+
range
83+
);
84+
85+
// Log and assert the code actions
86+
expect(codeActions).to.be.an("array");
87+
// Expand Macro action requires Swift 6.10
88+
if (workspaceContext.swiftVersion.isGreaterThanOrEqual(new Version(6, 1, 0))) {
89+
expect(codeActions.length).to.be.equal(2);
90+
} else {
91+
expect(codeActions.length).to.be.equal(1);
92+
}
93+
94+
const expectedMacro = '(a + b, "a + b")';
95+
// Loop through the code actions and execute them based on the command id
96+
for (const action of codeActions) {
97+
expect(action.command).is.not.undefined;
98+
const command = action.command!;
99+
// The id for the action is not clear, the title is "inline macro"
100+
if (command.command === "semantic.refactor.command") {
101+
// Run inline macro action
102+
await vscode.commands.executeCommand(command.command, ...(command.arguments ?? []));
103+
104+
// Assert that the macro was inlined correctly
105+
const endPosition = new vscode.Position(5, 37);
106+
const inlineRange = new vscode.Range(position, endPosition);
107+
const updatedText = editor.document.getText(inlineRange);
108+
expect(updatedText).to.equal(expectedMacro);
109+
110+
// Ensure we are refocusing on the text document for the undo step
111+
await vscode.window.showTextDocument(uri);
112+
113+
// We need to undo the inline macro or subsequent action will fail
114+
await vscode.commands.executeCommand("undo");
115+
} else if (command.command === "expand.macro.command") {
116+
// Set up a promise that resolves when the expected document is opened
117+
const expandedMacroUriPromise = new Promise<vscode.TextDocument>(
118+
(resolve, reject) => {
119+
const disposable = vscode.workspace.onDidOpenTextDocument(
120+
openedDocument => {
121+
if (openedDocument.uri.scheme === "sourcekit-lsp") {
122+
disposable.dispose(); // Stop listening once we find the desired document
123+
resolve(openedDocument);
124+
}
125+
}
126+
);
127+
128+
// Set a timeout to reject the promise if the document is not found
129+
setTimeout(() => {
130+
disposable.dispose();
131+
reject(
132+
new Error(
133+
"Timed out waiting for sourcekit-lsp document to be opened."
134+
)
135+
);
136+
}, 10000); // Wait up to 10 seconds for the document
137+
}
138+
);
139+
140+
// Run expand macro action
141+
await vscode.commands.executeCommand(command.command, ...(command.arguments ?? []));
142+
143+
// Wait for the expanded macro document to be opened
144+
const referenceDocument = await expandedMacroUriPromise;
145+
146+
// Verify that the reference document was successfully opened
147+
expect(referenceDocument).to.not.be.undefined;
148+
149+
// Assert that the content contains the expected result
150+
const content = referenceDocument.getText();
151+
expect(content).to.include(expectedMacro);
152+
} else {
153+
// New action got added, we need to add a new test case if this is hit.
154+
expect.fail();
155+
}
156+
}
157+
});
158+
});

0 commit comments

Comments
 (0)