Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion src/TestExplorer/TestRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,8 @@ export class TestRunner {
testBuildConfig: vscode.DebugConfiguration,
runState: TestRunnerTestRunState
) {
await this.workspaceContext.tempFolder.withTemporaryFile("xml", async filename => {
const tempFolder = await TemporaryFolder.create();
await tempFolder.withTemporaryFile("xml", async filename => {
const args = [...(testBuildConfig.args ?? []), "--xunit-output", filename];

try {
Expand Down
14 changes: 1 addition & 13 deletions src/WorkspaceContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { StatusItem } from "./ui/StatusItem";
import { swiftLibraryPathKey } from "./utilities/utilities";
import { isExcluded, isPathInsidePath } from "./utilities/filesystem";
import { LanguageClientToolchainCoordinator } from "./sourcekit-lsp/LanguageClientToolchainCoordinator";
import { TemporaryFolder } from "./utilities/tempFolder";
import { TaskManager } from "./tasks/TaskManager";
import { makeDebugConfigurations } from "./debugger/launch";
import configuration from "./configuration";
Expand Down Expand Up @@ -76,9 +75,8 @@ export class WorkspaceContext implements vscode.Disposable {

public loggerFactory: SwiftLoggerFactory;

private constructor(
constructor(
extensionContext: vscode.ExtensionContext,
public tempFolder: TemporaryFolder,
public logger: SwiftLogger,
public globalToolchain: SwiftToolchain
) {
Expand Down Expand Up @@ -230,16 +228,6 @@ export class WorkspaceContext implements vscode.Disposable {
return this.globalToolchain.swiftVersion;
}

/** Get swift version and create WorkspaceContext */
static async create(
extensionContext: vscode.ExtensionContext,
logger: SwiftLogger,
toolchain: SwiftToolchain
): Promise<WorkspaceContext> {
const tempFolder = await TemporaryFolder.create();
return new WorkspaceContext(extensionContext, tempFolder, logger, toolchain);
}

/**
* Update context keys based on package contents
*/
Expand Down
10 changes: 9 additions & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,15 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] {
Commands.RESET_PACKAGE,
async (_ /* Ignore context */, folder) => await resetPackage(ctx, folder)
),
vscode.commands.registerCommand("swift.runScript", async () => await runSwiftScript(ctx)),
vscode.commands.registerCommand("swift.runScript", async () => {
if (ctx.currentFolder && vscode.window.activeTextEditor?.document) {
await runSwiftScript(
vscode.window.activeTextEditor.document,
ctx.tasks,
ctx.currentFolder.toolchain
);
}
}),
vscode.commands.registerCommand("swift.openPackage", async () => {
if (ctx.currentFolder) {
return await openPackage(ctx.currentFolder.swiftVersion, ctx.currentFolder.folder);
Expand Down
121 changes: 68 additions & 53 deletions src/commands/runSwiftScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,58 @@
import * as vscode from "vscode";
import * as path from "path";
import * as fs from "fs/promises";
import { createSwiftTask } from "../tasks/SwiftTaskProvider";
import { WorkspaceContext } from "../WorkspaceContext";
import { Version } from "../utilities/version";
import configuration from "../configuration";
import { createSwiftTask } from "../tasks/SwiftTaskProvider";
import { TemporaryFolder } from "../utilities/tempFolder";
import { TaskManager } from "../tasks/TaskManager";
import { SwiftToolchain } from "../toolchain/toolchain";

/**
* Run the active document through the Swift REPL
* Runs the Swift code in the supplied document.
*
* This function checks for a valid document and Swift version, then creates and executes
* a Swift task to run the script file. The task is configured to always reveal its output
* and clear previous output. The working directory is set to the script's location.
*
* @param document - The text document containing the Swift script to run. If undefined, the function returns early.
* @param tasks - The TaskManager instance used to execute and manage the Swift task.
* @param toolchain - The SwiftToolchain to use for running the script.
* @returns A promise that resolves when the script has finished running, or returns early if the user is prompted
* for which swift version to use and they exit the dialog without choosing one.
*/
export async function runSwiftScript(ctx: WorkspaceContext) {
const document = vscode.window.activeTextEditor?.document;
if (!document) {
return;
}

if (!ctx.currentFolder) {
export async function runSwiftScript(
document: vscode.TextDocument,
tasks: TaskManager,
toolchain: SwiftToolchain
) {
const targetVersion = await targetSwiftVersion();
if (!targetVersion) {
return;
}

// Swift scripts require new swift driver to work on Windows. Swift driver is available
// from v5.7 of Windows Swift
if (
process.platform === "win32" &&
ctx.currentFolder.swiftVersion.isLessThan(new Version(5, 7, 0))
) {
void vscode.window.showErrorMessage(
"Run Swift Script is unavailable with the legacy driver on Windows."
await withDocumentFile(document, async filename => {
const runTask = createSwiftTask(
["-swift-version", targetVersion, filename],
`Run ${filename}`,
{
scope: vscode.TaskScope.Global,
cwd: vscode.Uri.file(path.dirname(filename)),
presentationOptions: { reveal: vscode.TaskRevealKind.Always, clear: true },
},
toolchain
);
return;
}

let target: string;
await tasks.executeTaskAndWait(runTask);
});
}

/**
* Determines the target Swift language version to use for script execution.
* If the configuration is set to "Ask Every Run", prompts the user to select a version.
* Otherwise, returns the default version from the user's settings.
*
* @returns {Promise<string | undefined>} The selected Swift version, or undefined if no selection was made.
*/
async function targetSwiftVersion() {
const defaultVersion = configuration.scriptSwiftLanguageVersion;
if (defaultVersion === "Ask Every Run") {
const picked = await vscode.window.showQuickPick(
Expand All @@ -59,41 +79,36 @@ export async function runSwiftScript(ctx: WorkspaceContext) {
placeHolder: "Select a target Swift version",
}
);

if (!picked) {
return;
}
target = picked.value;
return picked?.value;
} else {
target = defaultVersion;
return defaultVersion;
}
}

let filename = document.fileName;
let isTempFile = false;
/**
* Executes a callback with the filename of the given `vscode.TextDocument`.
* If the document is untitled (not yet saved to disk), it creates a temporary file,
* writes the document's content to it, and passes its filename to the callback.
* Otherwise, it ensures the document is saved and passes its actual filename.
*
* The temporary file is automatically deleted when the callback completes.
*
* @param document - The VSCode text document to operate on.
* @param callback - An async function that receives the filename of the document or temporary file.
* @returns A promise that resolves when the callback has completed.
*/
async function withDocumentFile(
document: vscode.TextDocument,
callback: (filename: string) => Promise<void>
) {
if (document.isUntitled) {
// if document hasn't been saved, save it to a temporary file
isTempFile = true;
filename = ctx.tempFolder.filename(document.fileName, "swift");
const text = document.getText();
await fs.writeFile(filename, text);
const tmpFolder = await TemporaryFolder.create();
await tmpFolder.withTemporaryFile("swift", async filename => {
await fs.writeFile(filename, document.getText());
await callback(filename);
});
} else {
// otherwise save document
await document.save();
}
const runTask = createSwiftTask(
["-swift-version", target, filename],
`Run ${filename}`,
{
scope: vscode.TaskScope.Global,
cwd: vscode.Uri.file(path.dirname(filename)),
presentationOptions: { reveal: vscode.TaskRevealKind.Always, clear: true },
},
ctx.currentFolder.toolchain
);
await ctx.tasks.executeTaskAndWait(runTask);

// delete file after running swift
if (isTempFile) {
await fs.rm(filename);
await callback(document.fileName);
}
}
37 changes: 28 additions & 9 deletions src/coverage/LcovResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { FolderContext } from "../FolderContext";
import { execFileStreamOutput } from "../utilities/utilities";
import { BuildFlags } from "../toolchain/BuildFlags";
import { TestLibrary } from "../TestExplorer/TestRunner";
import { DisposableFileCollection } from "../utilities/tempFolder";
import { DisposableFileCollection, TemporaryFolder } from "../utilities/tempFolder";
import { TargetType } from "../SwiftPackage";
import { TestingConfigurationFactory } from "../debugger/buildConfig";
import { TestKind } from "../TestExplorer/TestKind";
Expand All @@ -35,13 +35,11 @@ interface CodeCovFile {

export class TestCoverage {
private lcovFiles: CodeCovFile[] = [];
private lcovTmpFiles: DisposableFileCollection;
private _lcovTmpFiles?: DisposableFileCollection;
private _lcovTmpFilesInit?: Promise<DisposableFileCollection>;
private coverageDetails = new Map<vscode.Uri, vscode.FileCoverageDetail[]>();

constructor(private folderContext: FolderContext) {
const tmpFolder = folderContext.workspaceContext.tempFolder;
this.lcovTmpFiles = tmpFolder.createDisposableFileCollection();
}
constructor(private folderContext: FolderContext) {}

/**
* Returns coverage information for the suppplied URI.
Expand All @@ -60,7 +58,7 @@ export class TestCoverage {
true
);
const result = await asyncfs.readFile(`${buildDirectory}/debug/codecov/default.profdata`);
const filename = this.lcovTmpFiles.file(testLibrary, "profdata");
const filename = (await this.lcovTmpFiles()).file(testLibrary, "profdata");
await asyncfs.writeFile(filename, result);
this.lcovFiles.push({ testLibrary, path: filename });
}
Expand Down Expand Up @@ -88,14 +86,14 @@ export class TestCoverage {
this.coverageDetails.set(uri, detailedCoverage);
}
}
await this.lcovTmpFiles.dispose();
await this._lcovTmpFiles?.dispose();
}

/**
* Merges multiple `.profdata` files into a single `.profdata` file.
*/
private async mergeProfdata(profDataFiles: string[]) {
const filename = this.lcovTmpFiles.file("merged", "profdata");
const filename = (await this.lcovTmpFiles()).file("merged", "profdata");
const toolchain = this.folderContext.toolchain;
const llvmProfdata = toolchain.getToolchainExecutable("llvm-profdata");
await execFileStreamOutput(
Expand Down Expand Up @@ -196,6 +194,27 @@ export class TestCoverage {
return buffer;
}

/**
* Lazily creates (once) and returns the disposable file collection used for LCOV processing.
* Safe against concurrent callers.
*/
private async lcovTmpFiles(): Promise<DisposableFileCollection> {
if (this._lcovTmpFiles) {
return this._lcovTmpFiles;
}

// Use an internal promise to avoid duplicate folder creation in concurrent calls.
if (!this._lcovTmpFilesInit) {
this._lcovTmpFilesInit = (async () => {
const tempFolder = await TemporaryFolder.create();
this._lcovTmpFiles = tempFolder.createDisposableFileCollection();
return this._lcovTmpFiles;
})();
}

return (await this._lcovTmpFilesInit)!;
}

/**
* Constructs a string containing all the paths to exclude from the code coverage report.
* This should exclude everything in the `.build` folder as well as all the test targets.
Expand Down
2 changes: 1 addition & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
};
}

const workspaceContext = await WorkspaceContext.create(context, logger, toolchain);
const workspaceContext = new WorkspaceContext(context, logger, toolchain);
context.subscriptions.push(workspaceContext);

context.subscriptions.push(new SwiftEnvironmentVariablesManager(context));
Expand Down
Loading