Skip to content

Commit 5e18396

Browse files
Add a chai plugin to check for path equality (#1950)
1 parent f563712 commit 5e18396

File tree

12 files changed

+162
-82
lines changed

12 files changed

+162
-82
lines changed

.github/workflows/pull_request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ jobs:
6464
linux_pre_build_command: . .github/workflows/scripts/setup-linux.sh
6565
linux_build_command: ./scripts/test.sh
6666
# Windows
67-
windows_exclude_swift_versions: "${{ contains(github.event.pull_request.labels.*.name, 'full-test-run') && '[{\"swift_version\": \"nightly-6.1\"},{\"swift_version\": \"nightly\"}]' || '[{\"swift_version\": \"nightly-6.1\"},{\"swift_version\": \"nightly-6.2\"},{\"swift_version\": \"nightly\"}]' }}"
67+
windows_exclude_swift_versions: "${{ contains(github.event.pull_request.labels.*.name, 'full-test-run') && '[{\"swift_version\": \"nightly-6.1\"},{\"swift_version\": \"nightly\", {\"swift_version\": \"6.2\"}}]' || '[{\"swift_version\": \"nightly-6.1\"},{\"swift_version\": \"nightly-6.2\"},{\"swift_version\": \"nightly\"}]' }}"
6868
windows_env_vars: |
6969
CI=1
7070
FAST_TEST_RUN=${{ contains(github.event.pull_request.labels.*.name, 'full-test-run') && '0' || '1'}}

test/chai-path-plugin.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2025 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+
import * as path from "path";
15+
16+
declare global {
17+
// eslint-disable-next-line @typescript-eslint/no-namespace
18+
namespace Chai {
19+
interface Assertion {
20+
/**
21+
* Asserts that the object equals the expected path.
22+
*
23+
* This assertion will convert both paths to the same file separator and then
24+
* compare them to ensure consistent behavior on all platforms.
25+
*
26+
* @param expected The expected path string.
27+
*/
28+
equalPath(expected: string): Assertion;
29+
30+
/**
31+
* Asserts that the object equals the expected path.
32+
*
33+
* This assertion will convert both paths to the same file separator and then
34+
* compare them to ensure consistent behavior on all platforms.
35+
*
36+
* @param expected The expected path string.
37+
*/
38+
equalsPath(expected: string): Assertion;
39+
}
40+
41+
interface PromisedAssertion {
42+
/**
43+
* Asserts that the object equals the expected path.
44+
*
45+
* This assertion will convert both paths to the same file separator and then
46+
* compare them to ensure consistent behavior on all platforms.
47+
*
48+
* @param expected The expected path string.
49+
*/
50+
equalPath(expected: string): PromisedAssertion;
51+
52+
/**
53+
* Asserts that the object equals the expected path.
54+
*
55+
* This assertion will convert both paths to the same file separator and then
56+
* compare them to ensure consistent behavior on all platforms.
57+
*
58+
* @param expected The expected path string.
59+
*/
60+
equalsPath(expected: string): PromisedAssertion;
61+
}
62+
}
63+
}
64+
65+
export function chaiPathPlugin(chai: Chai.ChaiStatic, _utils: Chai.ChaiUtils): void {
66+
function assertPath(this: Chai.AssertionStatic, expected: string): void {
67+
const obj = this._obj;
68+
// First make sure the object is a string.
69+
new chai.Assertion(obj).to.be.a.string;
70+
// Then check for path equality.
71+
const expectedNormalized = normalizePath(expected);
72+
const objNormalized = normalizePath(obj);
73+
this.assert(
74+
objNormalized === expectedNormalized,
75+
`expected path "${objNormalized}" to equal "${expectedNormalized}"`,
76+
`expected path "${objNormalized}" to not equal "${expectedNormalized}"`,
77+
expectedNormalized,
78+
objNormalized
79+
);
80+
}
81+
82+
chai.Assertion.addMethod("equalPath", assertPath);
83+
chai.Assertion.addMethod("equalsPath", assertPath);
84+
}
85+
86+
function normalizePath(input: string): string {
87+
return normalizeWindowsDriveLetter(path.resolve(input));
88+
}
89+
90+
function normalizeWindowsDriveLetter(input: string): string {
91+
if (process.platform !== "win32") {
92+
return input;
93+
}
94+
const root = path.parse(input).root;
95+
return root.toLocaleUpperCase() + input.slice(root.length);
96+
}

test/common.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import * as sinonChai from "sinon-chai";
2121
import * as sourceMapSupport from "source-map-support";
2222
import * as tsConfigPaths from "tsconfig-paths";
2323

24+
import { chaiPathPlugin } from "./chai-path-plugin";
2425
import { installTagSupport } from "./tags";
2526

2627
// Use source-map-support to get better stack traces.
@@ -48,8 +49,11 @@ tsConfigPaths.register({
4849
paths: tsConfig.compilerOptions.paths,
4950
});
5051

52+
// Install chai plugins
5153
chai.use(sinonChai);
52-
chai.use(chaiAsPromised);
5354
chai.use(chaiSubset);
55+
chai.use(chaiPathPlugin);
56+
// chai-as-promised must always be installed last!
57+
chai.use(chaiAsPromised);
5458

5559
installTagSupport();

test/integration-tests/SwiftSnippet.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ tag("large").suite("SwiftSnippet Test Suite", function () {
7878

7979
expect(succeeded).to.be.true;
8080
const session = await sessionPromise;
81-
expect(vscode.Uri.file(session.configuration.program).fsPath).to.equal(
81+
expect(session.configuration.program).to.equalPath(
8282
realpathSync(
8383
testAssetUri(
8484
"defaultPackage/.build/debug/hello" +
@@ -118,7 +118,7 @@ tag("large").suite("SwiftSnippet Test Suite", function () {
118118
expect(succeeded).to.be.true;
119119

120120
const session = await sessionPromise;
121-
expect(vscode.Uri.file(session.configuration.program).fsPath).to.equal(
121+
expect(session.configuration.program).to.equalPath(
122122
realpathSync(
123123
testAssetUri(
124124
"defaultPackage/.build/debug/hello" +

test/integration-tests/ui/ProjectPanelProvider.test.ts

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -313,9 +313,8 @@ tag("medium").suite("ProjectPanelProvider Test Suite", function () {
313313
const dep = items.find(n => n.name === "swift-markdown") as PackageNode;
314314
expect(dep, `${JSON.stringify(items, null, 2)}`).to.not.be.undefined;
315315
expect(dep?.location).to.equal("https://github.com/swiftlang/swift-markdown.git");
316-
assertPathsEqual(
317-
dep?.path,
318-
path.join(testAssetPath("targets"), ".build/checkouts/swift-markdown")
316+
expect(dep?.path).to.equalPath(
317+
testAssetPath("targets/.build/checkouts/swift-markdown")
319318
);
320319
});
321320

@@ -326,8 +325,8 @@ tag("medium").suite("ProjectPanelProvider Test Suite", function () {
326325
dep,
327326
`Expected to find defaultPackage, but instead items were ${items.map(n => n.name)}`
328327
).to.not.be.undefined;
329-
assertPathsEqual(dep?.location, testAssetPath("defaultPackage"));
330-
assertPathsEqual(dep?.path, testAssetPath("defaultPackage"));
328+
expect(dep?.location).to.equalPath(testAssetPath("defaultPackage"));
329+
expect(dep?.path).to.equalPath(testAssetPath("defaultPackage"));
331330
});
332331

333332
test("Lists local dependency file structure", async () => {
@@ -343,24 +342,22 @@ tag("medium").suite("ProjectPanelProvider Test Suite", function () {
343342
const folder = folders.find(n => n.name === "Sources") as FileNode;
344343
expect(folder).to.not.be.undefined;
345344

346-
assertPathsEqual(folder?.path, path.join(testAssetPath("defaultPackage"), "Sources"));
345+
expect(folder?.path).to.equalPath(testAssetPath("defaultPackage/Sources"));
347346

348347
const childFolders = await treeProvider.getChildren(folder);
349348
const childFolder = childFolders.find(n => n.name === "PackageExe") as FileNode;
350349
expect(childFolder).to.not.be.undefined;
351350

352-
assertPathsEqual(
353-
childFolder?.path,
354-
path.join(testAssetPath("defaultPackage"), "Sources/PackageExe")
351+
expect(childFolder?.path).to.equalPath(
352+
testAssetPath("defaultPackage/Sources/PackageExe")
355353
);
356354

357355
const files = await treeProvider.getChildren(childFolder);
358356
const file = files.find(n => n.name === "main.swift") as FileNode;
359357
expect(file).to.not.be.undefined;
360358

361-
assertPathsEqual(
362-
file?.path,
363-
path.join(testAssetPath("defaultPackage"), "Sources/PackageExe/main.swift")
359+
expect(file?.path).to.equalPath(
360+
testAssetPath("defaultPackage/Sources/PackageExe/main.swift")
364361
);
365362
});
366363

@@ -375,19 +372,19 @@ tag("medium").suite("ProjectPanelProvider Test Suite", function () {
375372
expect(folder).to.not.be.undefined;
376373

377374
const depPath = path.join(testAssetPath("targets"), ".build/checkouts/swift-markdown");
378-
assertPathsEqual(folder?.path, path.join(depPath, "Sources"));
375+
expect(folder?.path).to.equalPath(path.join(depPath, "Sources"));
379376

380377
const childFolders = await treeProvider.getChildren(folder);
381378
const childFolder = childFolders.find(n => n.name === "CAtomic") as FileNode;
382379
expect(childFolder).to.not.be.undefined;
383380

384-
assertPathsEqual(childFolder?.path, path.join(depPath, "Sources/CAtomic"));
381+
expect(childFolder?.path).to.equalPath(path.join(depPath, "Sources/CAtomic"));
385382

386383
const files = await treeProvider.getChildren(childFolder);
387384
const file = files.find(n => n.name === "CAtomic.c") as FileNode;
388385
expect(file).to.not.be.undefined;
389386

390-
assertPathsEqual(file?.path, path.join(depPath, "Sources/CAtomic/CAtomic.c"));
387+
expect(file?.path).to.equalPath(path.join(depPath, "Sources/CAtomic/CAtomic.c"));
391388
});
392389

393390
test("Shows a flat dependency list", async () => {
@@ -501,11 +498,4 @@ tag("medium").suite("ProjectPanelProvider Test Suite", function () {
501498
throw error;
502499
}
503500
}
504-
505-
function assertPathsEqual(path1: string | undefined, path2: string | undefined) {
506-
expect(path1).to.not.be.undefined;
507-
expect(path2).to.not.be.undefined;
508-
// Convert to vscode.Uri to normalize paths, including drive letter capitalization on Windows.
509-
expect(vscode.Uri.file(path1!).fsPath).to.equal(vscode.Uri.file(path2!).fsPath);
510-
}
511501
});

test/unit-tests/debugger/debugAdapterFactory.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
//===----------------------------------------------------------------------===//
1414
import { expect } from "chai";
1515
import * as mockFS from "mock-fs";
16-
import * as path from "path";
1716
import * as vscode from "vscode";
1817

1918
import { FolderContext } from "@src/FolderContext";
@@ -173,10 +172,11 @@ suite("LLDBDebugConfigurationProvider Tests", () => {
173172
target: "executable",
174173
}
175174
);
176-
expect(launchConfig).to.have.property(
177-
"program",
178-
path.normalize("/path/to/swift-executable/.build/arm64-apple-macosx/debug/executable")
179-
);
175+
expect(launchConfig)
176+
.to.have.property("program")
177+
.that.equalsPath(
178+
"/path/to/swift-executable/.build/arm64-apple-macosx/debug/executable"
179+
);
180180
});
181181

182182
suite("CodeLLDB selected in settings", () => {

test/unit-tests/tasks/SwiftPluginTaskProvider.test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414
import * as assert from "assert";
15+
import { expect } from "chai";
1516
import * as os from "os";
16-
import * as path from "path";
1717
import { match } from "sinon";
1818
import * as vscode from "vscode";
1919

@@ -122,7 +122,7 @@ suite("SwiftPluginTaskProvider Unit Test Suite", () => {
122122
new vscode.CancellationTokenSource().token
123123
);
124124
const swiftExecution = resolvedTask.execution as SwiftExecution;
125-
assert.equal(swiftExecution.options.cwd, `${workspaceFolder.uri.fsPath}/myCWD`);
125+
expect(swiftExecution.options.cwd).to.equalPath(`${workspaceFolder.uri.fsPath}/myCWD`);
126126
});
127127

128128
test("includes scope cwd", async () => {
@@ -141,7 +141,7 @@ suite("SwiftPluginTaskProvider Unit Test Suite", () => {
141141
new vscode.CancellationTokenSource().token
142142
);
143143
const swiftExecution = resolvedTask.execution as SwiftExecution;
144-
assert.equal(swiftExecution.options.cwd, workspaceFolder.uri.fsPath);
144+
expect(swiftExecution.options.cwd).to.equalPath(workspaceFolder.uri.fsPath);
145145
});
146146

147147
test("includes resolved cwd", async () => {
@@ -161,10 +161,7 @@ suite("SwiftPluginTaskProvider Unit Test Suite", () => {
161161
new vscode.CancellationTokenSource().token
162162
);
163163
const swiftExecution = resolvedTask.execution as SwiftExecution;
164-
assert.equal(
165-
swiftExecution.options.cwd,
166-
path.normalize(`${workspaceFolder.uri.fsPath}/myCWD`)
167-
);
164+
expect(swiftExecution.options.cwd).to.equalPath(`${workspaceFolder.uri.fsPath}/myCWD`);
168165
});
169166

170167
test("includes fallback cwd", async () => {

test/unit-tests/tasks/SwiftTaskProvider.test.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414
import * as assert from "assert";
15+
import { expect } from "chai";
1516
import * as os from "os";
1617
import { match } from "sinon";
1718
import * as vscode from "vscode";
@@ -379,7 +380,9 @@ suite("SwiftTaskProvider Unit Test Suite", () => {
379380
new vscode.CancellationTokenSource().token
380381
);
381382
const swiftExecution = resolvedTask.execution as SwiftExecution;
382-
assert.equal(swiftExecution.options.cwd, `${workspaceFolder.uri.fsPath}/macos`);
383+
expect(swiftExecution.options.cwd).to.equalPath(
384+
`${workspaceFolder.uri.fsPath}/macos`
385+
);
383386
});
384387

385388
test("includes linux cwd", () => {
@@ -403,7 +406,9 @@ suite("SwiftTaskProvider Unit Test Suite", () => {
403406
new vscode.CancellationTokenSource().token
404407
);
405408
const swiftExecution = resolvedTask.execution as SwiftExecution;
406-
assert.equal(swiftExecution.options.cwd, `${workspaceFolder.uri.fsPath}/linux`);
409+
expect(swiftExecution.options.cwd).to.equalPath(
410+
`${workspaceFolder.uri.fsPath}/linux`
411+
);
407412
});
408413

409414
test("includes windows cwd", () => {
@@ -427,7 +432,9 @@ suite("SwiftTaskProvider Unit Test Suite", () => {
427432
new vscode.CancellationTokenSource().token
428433
);
429434
const swiftExecution = resolvedTask.execution as SwiftExecution;
430-
assert.equal(swiftExecution.options.cwd, `${workspaceFolder.uri.fsPath}/windows`);
435+
expect(swiftExecution.options.cwd).to.equalPath(
436+
`${workspaceFolder.uri.fsPath}/windows`
437+
);
431438
});
432439

433440
test("fallback default cwd", () => {
@@ -451,7 +458,7 @@ suite("SwiftTaskProvider Unit Test Suite", () => {
451458
new vscode.CancellationTokenSource().token
452459
);
453460
const swiftExecution = resolvedTask.execution as SwiftExecution;
454-
assert.equal(swiftExecution.options.cwd, workspaceFolder.uri.fsPath);
461+
expect(swiftExecution.options.cwd).to.equalPath(workspaceFolder.uri.fsPath);
455462
});
456463
});
457464

0 commit comments

Comments
 (0)