Skip to content

Commit

Permalink
Add integration tests for Inline/Expand Macro actions
Browse files Browse the repository at this point in the history
- 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
michael-weng committed Nov 14, 2024
1 parent e71bbfa commit dc2b562
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 0 deletions.
42 changes: 42 additions & 0 deletions assets/test/swift-macro/Package.swift
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 assets/test/swift-macro/Sources/swift-macro/swift_macro.swift
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")
8 changes: 8 additions & 0 deletions assets/test/swift-macro/Sources/swift-macroClient/main.swift
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)\"")
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,
]
}
158 changes: 158 additions & 0 deletions test/integration-tests/language/macro.test.ts
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();
}
}
});
});

0 comments on commit dc2b562

Please sign in to comment.