Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- Fix an error when performing "Run/Debug Tests Multiple Times" on Linux ([#1824](https://github.com/swiftlang/vscode-swift/pull/1824))
- Fix the `> Swift: Run Swift Script` command not running unless a Swift Package folder is open ([#1832](https://github.com/swiftlang/vscode-swift/pull/1832))
- Fix the SourceKit-LSP diagnostics reported progress ([#1799](https://github.com/swiftlang/vscode-swift/pull/1799))
- Omit incompatible `additionalTestArgs` when building tests for debugging ([#1864](https://github.com/swiftlang/vscode-swift/pull/1864))

## 2.11.20250806 - 2025-08-06

Expand Down
37 changes: 32 additions & 5 deletions src/debugger/buildConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { TestLibrary } from "../TestExplorer/TestRunner";
import configuration from "../configuration";
import { SwiftLogger } from "../logging/SwiftLogger";
import { buildOptions } from "../tasks/SwiftTaskProvider";
import { BuildFlags } from "../toolchain/BuildFlags";
import { ArgumentFilter, BuildFlags } from "../toolchain/BuildFlags";
import { packageName } from "../utilities/tasks";
import { regexEscapedString, swiftRuntimeEnv } from "../utilities/utilities";
import { Version } from "../utilities/version";
Expand Down Expand Up @@ -62,10 +62,13 @@ export class BuildConfigurationFactory {
additionalArgs = [...additionalArgs, "-Xswiftc", "-enable-testing"];
}
if (this.isTestBuild) {
additionalArgs = [
...additionalArgs,
...configuration.folder(this.ctx.workspaceFolder).additionalTestArguments,
];
// Exclude all arguments from TEST_ONLY_ARGUMENTS that would cause a `swift build` to fail.
const buildCompatibleArgs = BuildFlags.filterArguments(
configuration.folder(this.ctx.workspaceFolder).additionalTestArguments,
BuildConfigurationFactory.TEST_ONLY_ARGUMENTS,
true
);
additionalArgs = [...additionalArgs, ...buildCompatibleArgs];
}
}

Expand Down Expand Up @@ -99,6 +102,30 @@ export class BuildConfigurationFactory {
private get baseConfig() {
return getBaseConfig(this.ctx, true);
}

/**
* Arguments from additionalTestArguments that should be excluded from swift build commands.
* These are test-only arguments that would cause build failures if passed to swift build.
*/
private static TEST_ONLY_ARGUMENTS: ArgumentFilter[] = [
{ argument: "--parallel", include: 0 },
{ argument: "--no-parallel", include: 0 },
{ argument: "--num-workers", include: 1 },
{ argument: "--filter", include: 1 },
{ argument: "--skip", include: 1 },
{ argument: "-s", include: 1 },
{ argument: "--specifier", include: 1 },
{ argument: "-l", include: 0 },
{ argument: "--list-tests", include: 0 },
{ argument: "--show-codecov-path", include: 0 },
{ argument: "--show-code-coverage-path", include: 0 },
{ argument: "--show-coverage-path", include: 0 },
{ argument: "--xunit-output", include: 1 },
{ argument: "--enable-testable-imports", include: 0 },
{ argument: "--disable-testable-imports", include: 0 },
{ argument: "--attachments-path", include: 1 },
{ argument: "--skip-build", include: 0 },
];
}

export class SwiftTestingBuildAguments {
Expand Down
45 changes: 32 additions & 13 deletions src/toolchain/BuildFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,34 +296,53 @@ export class BuildFlags {
}

/**
* Filter argument list
* Filter argument list with support for both inclusion and exclusion logic
* @param args argument list
* @param filter argument list filter
* @param exclude if true, remove matching arguments (exclusion mode); if false, keep only matching arguments (inclusion mode)
* @returns filtered argument list
*/
static filterArguments(args: string[], filter: ArgumentFilter[]): string[] {
static filterArguments(args: string[], filter: ArgumentFilter[], exclude = false): string[] {
const filteredArguments: string[] = [];
let includeCount = 0;
let pendingCount = 0;

for (const arg of args) {
if (includeCount > 0) {
filteredArguments.push(arg);
includeCount -= 1;
if (pendingCount > 0) {
if (!exclude) {
filteredArguments.push(arg);
}
pendingCount -= 1;
continue;
}
const argFilter = filter.find(item => item.argument === arg);
if (argFilter) {
filteredArguments.push(arg);
includeCount = argFilter.include;

// Check if this argument matches any filter
const matchingFilter = filter.find(item => item.argument === arg);
if (matchingFilter) {
if (!exclude) {
filteredArguments.push(arg);
}
pendingCount = matchingFilter.include;
continue;
}
// find arguments of form arg=value
const argFilter2 = filter.find(

// Check for arguments of form --arg=value (only for filters with include=1)
const combinedArgFilter = filter.find(
item => item.include === 1 && arg.startsWith(item.argument + "=")
);
if (argFilter2) {
if (combinedArgFilter) {
if (!exclude) {
filteredArguments.push(arg);
}
continue;
}

// Handle unmatched arguments
if (exclude) {
filteredArguments.push(arg);
}
// In include mode, unmatched arguments are not added
}

return filteredArguments;
}
}
266 changes: 266 additions & 0 deletions test/unit-tests/debugger/buildConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
//===----------------------------------------------------------------------===//
//
// 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 { expect } from "chai";
import * as sinon from "sinon";

import { FolderContext } from "@src/FolderContext";
import { LinuxMain } from "@src/LinuxMain";
import { SwiftPackage } from "@src/SwiftPackage";
import configuration, { FolderConfiguration } from "@src/configuration";
import { BuildConfigurationFactory } from "@src/debugger/buildConfig";
import { BuildFlags } from "@src/toolchain/BuildFlags";
import { SwiftToolchain } from "@src/toolchain/toolchain";
import { Version } from "@src/utilities/version";

import { MockedObject, instance, mockGlobalValue, mockObject } from "../../MockUtils";

suite("BuildConfig Test Suite", () => {
let mockedFolderContext: MockedObject<FolderContext>;
let mockedSwiftPackage: MockedObject<SwiftPackage>;
let mockedToolchain: MockedObject<SwiftToolchain>;
let mockedBuildFlags: MockedObject<BuildFlags>;

const additionalTestArgumentsConfig = mockGlobalValue(configuration, "folder");

function createMockFolderConfig(additionalTestArguments: string[]): FolderConfiguration {
return {
testEnvironmentVariables: {},
additionalTestArguments,
searchSubfoldersForPackages: false,
autoGenerateLaunchConfigurations: false,
disableAutoResolve: false,
attachmentsPath: "",
pluginPermissions: () => ({ trusted: false }),
pluginArguments: () => [],
} as FolderConfiguration;
}

suiteSetup(() => {
mockedBuildFlags = mockObject<BuildFlags>({
getDarwinTarget: () => undefined,
});

mockedToolchain = mockObject<SwiftToolchain>({
buildFlags: instance(mockedBuildFlags),
swiftVersion: new Version(6, 0, 0),
sanitizer: () => undefined,
});

mockedSwiftPackage = mockObject<SwiftPackage>({
getTargets: sinon.stub().resolves([{ name: "TestTarget" }]),
name: Promise.resolve("TestPackage"),
});

mockedFolderContext = mockObject<FolderContext>({
toolchain: instance(mockedToolchain),
swiftPackage: instance(mockedSwiftPackage),
workspaceFolder: { uri: { fsPath: "/test/workspace" }, name: "TestWorkspace" } as any,
swiftVersion: new Version(6, 0, 0),
relativePath: "",
linuxMain: {
exists: true,
} as any as LinuxMain,
});
});

suite("TEST_ONLY_ARGUMENTS filtering", () => {
let filterArgumentsSpy: sinon.SinonSpy;

setup(() => {
// Reset any existing spies
sinon.restore();

// Spy on the BuildFlags.filterArguments method
filterArgumentsSpy = sinon.spy(BuildFlags, "filterArguments");

// Mock configuration.folder to return test arguments
additionalTestArgumentsConfig.setValue(() => createMockFolderConfig([]));
});

teardown(() => {
sinon.restore();
});

test("filters out test-only arguments for test builds", async () => {
additionalTestArgumentsConfig.setValue(() =>
createMockFolderConfig([
"--no-parallel",
"--filter",
"TestCase",
"--enable-code-coverage",
])
);

const config = await BuildConfigurationFactory.buildAll(
instance(mockedFolderContext),
true, // isTestBuild
false // isRelease
);

expect(filterArgumentsSpy).to.have.been.calledOnce;
const [args] = filterArgumentsSpy.firstCall.args;

expect(args).to.deep.equal([
"--no-parallel",
"--filter",
"TestCase",
"--enable-code-coverage",
]);

expect(config.args).to.include("--build-tests");
});

test("preserves build-compatible arguments for test builds", async () => {
additionalTestArgumentsConfig.setValue(() =>
createMockFolderConfig([
"--scratch-path",
"/tmp/build",
"-Xswiftc",
"-enable-testing",
])
);

// Act: Build configuration for test build
await BuildConfigurationFactory.buildAll(
instance(mockedFolderContext),
true, // isTestBuild
false // isRelease
);

expect(filterArgumentsSpy).to.have.been.calledOnce;
const [args] = filterArgumentsSpy.firstCall.args;
expect(args).to.deep.equal([
"--scratch-path",
"/tmp/build",
"-Xswiftc",
"-enable-testing",
]);
});

test("does not filter arguments for non-test builds", async () => {
additionalTestArgumentsConfig.setValue(() =>
createMockFolderConfig(["--no-parallel", "--scratch-path", "/tmp/build"])
);

await BuildConfigurationFactory.buildAll(
instance(mockedFolderContext),
false, // isTestBuild
false // isRelease
);

expect(filterArgumentsSpy).to.not.have.been.called;
});

test("handles empty additionalTestArguments", async () => {
additionalTestArgumentsConfig.setValue(() => createMockFolderConfig([]));

await BuildConfigurationFactory.buildAll(
instance(mockedFolderContext),
true, // isTestBuild
false // isRelease
);

expect(filterArgumentsSpy).to.have.been.calledOnce;
const [args] = filterArgumentsSpy.firstCall.args;
expect(args).to.deep.equal([]);
});

test("handles mixed arguments correctly", async () => {
additionalTestArgumentsConfig.setValue(() =>
createMockFolderConfig([
"--no-parallel", // test-only (should be filtered out)
"--scratch-path",
"/tmp", // build-compatible (should be preserved)
"--filter",
"MyTest", // test-only (should be filtered out)
"-Xswiftc",
"-O", // build-compatible (should be preserved)
"--enable-code-coverage", // test-only (should be filtered out)
"--verbose", // build-compatible (should be preserved)
])
);

await BuildConfigurationFactory.buildAll(
instance(mockedFolderContext),
true, // isTestBuild
false // isRelease
);

expect(filterArgumentsSpy).to.have.been.calledOnce;
const [args] = filterArgumentsSpy.firstCall.args;
expect(args).to.deep.equal([
"--no-parallel",
"--scratch-path",
"/tmp",
"--filter",
"MyTest",
"-Xswiftc",
"-O",
"--enable-code-coverage",
"--verbose",
]);
});

test("has correct include values for arguments with parameters", () => {
// Access the private static property through the class
const filter = (BuildConfigurationFactory as any).TEST_ONLY_ARGUMENTS;

// Arguments that take 1 parameter
const oneParamArgs = ["--filter", "--skip", "--num-workers", "--xunit-output"];
oneParamArgs.forEach(arg => {
const filterItem = filter.find((f: any) => f.argument === arg);
expect(filterItem, `${arg} should be in exclusion filter`).to.exist;
expect(filterItem.include, `${arg} should exclude 1 parameter`).to.equal(1);
});

// Arguments that take no parameters (flags)
const noParamArgs = ["--parallel", "--no-parallel", "--list-tests"];
noParamArgs.forEach(arg => {
const filterItem = filter.find((f: any) => f.argument === arg);
expect(filterItem, `${arg} should be in exclusion filter`).to.exist;
expect(filterItem.include, `${arg} should exclude 0 parameters`).to.equal(0);
});
});

test("excludes expected test-only arguments", () => {
// Access the private static property through the class
const filter = (BuildConfigurationFactory as any).TEST_ONLY_ARGUMENTS;

expect(filter).to.be.an("array");

// Verify test-only arguments are included in the exclusion list
expect(filter.some((f: any) => f.argument === "--no-parallel")).to.be.true;
expect(filter.some((f: any) => f.argument === "--parallel")).to.be.true;
expect(filter.some((f: any) => f.argument === "--filter")).to.be.true;
expect(filter.some((f: any) => f.argument === "--skip")).to.be.true;
expect(filter.some((f: any) => f.argument === "--list-tests")).to.be.true;
expect(filter.some((f: any) => f.argument === "--attachments-path")).to.be.true;
expect(filter.some((f: any) => f.argument === "--enable-testable-imports")).to.be.true;
expect(filter.some((f: any) => f.argument === "--xunit-output")).to.be.true;
});

test("does not exclude build-compatible arguments", () => {
// Access the private static property through the class
const filter = (BuildConfigurationFactory as any).TEST_ONLY_ARGUMENTS;

// Verify build-compatible arguments are NOT in the exclusion list
expect(filter.some((f: any) => f.argument === "--scratch-path")).to.be.false;
expect(filter.some((f: any) => f.argument === "-Xswiftc")).to.be.false;
expect(filter.some((f: any) => f.argument === "--build-system")).to.be.false;
expect(filter.some((f: any) => f.argument === "--sdk")).to.be.false;
expect(filter.some((f: any) => f.argument === "--verbose")).to.be.false;
expect(filter.some((f: any) => f.argument === "--configuration")).to.be.false;
});
});
});