Skip to content

Commit 44e28a5

Browse files
committed
Exclude bundled gems from test discovery
Test discovery was finding tests in .bundle/gems and vendor/bundle directories, which contain bundled gem code. This adds an exclude pattern to prevent these directories from being scanned during test discovery. Fixes #3949.
1 parent 31ed10a commit 44e28a5

3 files changed

Lines changed: 192 additions & 8 deletions

File tree

jekyll/test_explorer.markdown

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ To discover all test files in the workspace with decent performance, the Ruby LS
5656
conventions. For a test file to be discovered, the file path must match this glob:
5757
`**/{test,spec,features}/**/{*_test.rb,test_*.rb,*_spec.rb,*.feature}`
5858
59+
Tests in certain directories are automatically excluded from discovery: `.bundle`, `vendor/bundle`, `node_modules`, `tmp`, and `log`.
60+
5961
### Dynamically defined tests
6062
6163
There is limited support for tests defined via meta-programming. Initially, they will not be present in the test

vscode/src/test/suite/testController.test.ts

Lines changed: 158 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,23 +147,50 @@ suite("TestController", () => {
147147
);
148148
}
149149

150-
function createWorkspaceWithTestFile() {
150+
function createWorkspaceWithTestFile(
151+
options: {
152+
testDir?: string;
153+
testFileName?: string;
154+
testContent?: string;
155+
index?: number;
156+
additionalFiles?: Array<{ path: string; content: string }>;
157+
} = {},
158+
) {
159+
const {
160+
testDir = "test",
161+
testFileName = "foo_test.rb",
162+
testContent = "require 'test_helper'\n\nclass FooTest < Minitest::Test; def test_foo; end; end",
163+
index = 1,
164+
additionalFiles = [],
165+
} = options;
166+
151167
const workspacePath = fs.mkdtempSync(path.join(os.tmpdir(), "ruby-lsp-test-controller-"));
152168
const workspaceUri = vscode.Uri.file(workspacePath);
153169

154-
fs.mkdirSync(path.join(workspaceUri.fsPath, "test"));
155-
const testFilePath = path.join(workspaceUri.fsPath, "test", "foo_test.rb");
156-
fs.writeFileSync(testFilePath, "require 'test_helper'\n\nclass FooTest < Minitest::Test; def test_foo; end; end");
170+
fs.mkdirSync(path.join(workspaceUri.fsPath, testDir), { recursive: true });
171+
const testFilePath = path.join(workspaceUri.fsPath, testDir, testFileName);
172+
fs.writeFileSync(testFilePath, testContent);
173+
174+
// Create additional files if specified
175+
additionalFiles.forEach(({ path: filePath, content }) => {
176+
const fullPath = path.join(workspaceUri.fsPath, filePath);
177+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
178+
fs.writeFileSync(fullPath, content);
179+
});
157180

158181
const workspaceFolder: vscode.WorkspaceFolder = {
159182
uri: workspaceUri,
160183
name: path.basename(workspacePath),
161-
index: 1,
184+
index,
162185
};
163186

164187
return { workspaceFolder, testFileUri: vscode.Uri.file(testFilePath) };
165188
}
166189

190+
function cleanupWorkspace(workspaceFolder: vscode.WorkspaceFolder) {
191+
fs.rmSync(workspaceFolder.uri.fsPath, { recursive: true, force: true });
192+
}
193+
167194
function stubWorkspaceOperations(...workspaces: vscode.WorkspaceFolder[]) {
168195
workspaceStubs.forEach((stub) => stub.restore());
169196
workspaceStubs = [];
@@ -927,4 +954,130 @@ suite("TestController", () => {
927954

928955
assert.ok(spy.calledOnce);
929956
});
957+
958+
test("does not discover tests in .bundle directory", async () => {
959+
const { workspaceFolder, testFileUri } = createWorkspaceWithTestFile({
960+
testFileName: "my_test.rb",
961+
testContent: "class MyTest < Minitest::Test; end",
962+
index: 0,
963+
additionalFiles: [
964+
{
965+
path: ".bundle/gems/test/gem_test.rb",
966+
content: "class GemTest < Minitest::Test; end",
967+
},
968+
],
969+
});
970+
971+
stubWorkspaceOperations(workspaceFolder);
972+
973+
await controller.testController.resolveHandler!(undefined);
974+
975+
const collection = controller.testController.items;
976+
const testDir = collection.get(vscode.Uri.joinPath(workspaceFolder.uri, "test").toString());
977+
978+
// Should find the regular test file
979+
assert.ok(testDir);
980+
const regularTest = testDir.children.get(testFileUri.toString());
981+
assert.ok(regularTest, "Regular test file should be discovered");
982+
983+
// Should NOT find the test file in ignored directory
984+
const ignoredTestDir = collection.get(
985+
vscode.Uri.joinPath(workspaceFolder.uri, ".bundle", "gems", "test").toString(),
986+
);
987+
assert.strictEqual(ignoredTestDir, undefined, "Ignored directory should not be discovered");
988+
989+
cleanupWorkspace(workspaceFolder);
990+
});
991+
992+
test("does not discover tests in vendor/bundle directory", async () => {
993+
const { workspaceFolder, testFileUri } = createWorkspaceWithTestFile({
994+
testDir: "spec",
995+
testFileName: "my_spec.rb",
996+
testContent: "RSpec.describe 'MySpec' do; end",
997+
index: 0,
998+
additionalFiles: [
999+
{
1000+
path: "vendor/bundle/gems/spec/gem_spec.rb",
1001+
content: "RSpec.describe 'GemSpec' do; end",
1002+
},
1003+
],
1004+
});
1005+
1006+
stubWorkspaceOperations(workspaceFolder);
1007+
1008+
await controller.testController.resolveHandler!(undefined);
1009+
1010+
const collection = controller.testController.items;
1011+
const specDir = collection.get(vscode.Uri.joinPath(workspaceFolder.uri, "spec").toString());
1012+
1013+
// Should find the regular spec file
1014+
assert.ok(specDir);
1015+
const regularSpec = specDir.children.get(testFileUri.toString());
1016+
assert.ok(regularSpec, "Regular spec file should be discovered");
1017+
1018+
// Should NOT find the test file in ignored directory
1019+
const ignoredTestDir = collection.get(
1020+
vscode.Uri.joinPath(workspaceFolder.uri, "vendor", "bundle", "gems", "test").toString(),
1021+
);
1022+
assert.strictEqual(ignoredTestDir, undefined, "Ignored directory should not be discovered");
1023+
1024+
cleanupWorkspace(workspaceFolder);
1025+
});
1026+
1027+
suite("isInIgnoredFolder", () => {
1028+
test("detects files in .bundle directory", () => {
1029+
const uri = vscode.Uri.file("/workspace/.bundle/gems/ruby/3.3.0/gems/rspec-core-3.12.0/spec/rspec_spec.rb");
1030+
assert.ok((controller as any).isInIgnoredFolder(uri));
1031+
});
1032+
1033+
test("detects files in vendor/bundle directory", () => {
1034+
const uri = vscode.Uri.file("/workspace/vendor/bundle/ruby/3.3.0/gems/rspec-core-3.12.0/spec/rspec_spec.rb");
1035+
assert.ok((controller as any).isInIgnoredFolder(uri));
1036+
});
1037+
1038+
test("detects files in nested .bundle paths", () => {
1039+
const uri = vscode.Uri.file("/workspace/subfolder/.bundle/ruby/3.3.0/gems/minitest-5.0.0/test/minitest_test.rb");
1040+
assert.ok((controller as any).isInIgnoredFolder(uri));
1041+
});
1042+
1043+
test("detects files in node_modules directory", () => {
1044+
const uri = vscode.Uri.file("/workspace/node_modules/@types/node/test/test.rb");
1045+
assert.ok((controller as any).isInIgnoredFolder(uri));
1046+
});
1047+
1048+
test("detects files in tmp directory", () => {
1049+
const uri = vscode.Uri.file("/workspace/tmp/cache/test_file.rb");
1050+
assert.ok((controller as any).isInIgnoredFolder(uri));
1051+
});
1052+
1053+
test("detects files in log directory", () => {
1054+
const uri = vscode.Uri.file("/workspace/log/test/test_spec.rb");
1055+
assert.ok((controller as any).isInIgnoredFolder(uri));
1056+
});
1057+
1058+
test("does not detect regular test files", () => {
1059+
const uri = vscode.Uri.file("/workspace/test/models/user_test.rb");
1060+
assert.strictEqual((controller as any).isInIgnoredFolder(uri), false);
1061+
});
1062+
1063+
test("does not detect spec files in regular paths", () => {
1064+
const uri = vscode.Uri.file("/workspace/spec/models/user_spec.rb");
1065+
assert.strictEqual((controller as any).isInIgnoredFolder(uri), false);
1066+
});
1067+
1068+
test("does not detect files with 'bundle' in name but not in ignored directory", () => {
1069+
const uri = vscode.Uri.file("/workspace/test/bundle_test.rb");
1070+
assert.strictEqual((controller as any).isInIgnoredFolder(uri), false);
1071+
});
1072+
1073+
test("does not detect files with 'vendor' in name but not in vendor/bundle", () => {
1074+
const uri = vscode.Uri.file("/workspace/test/vendor_test.rb");
1075+
assert.strictEqual((controller as any).isInIgnoredFolder(uri), false);
1076+
});
1077+
1078+
test("does not detect files in vendor directory that are not in vendor/bundle", () => {
1079+
const uri = vscode.Uri.file("/workspace/vendor/plugins/my_plugin/test/plugin_test.rb");
1080+
assert.strictEqual((controller as any).isInIgnoredFolder(uri), false);
1081+
});
1082+
});
9301083
});

vscode/src/testController.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ const asyncExec = promisify(exec);
1515

1616
const NESTED_TEST_DIR_PATTERN = "**/{test,spec,features}/**/";
1717
const TEST_FILE_PATTERN = `${NESTED_TEST_DIR_PATTERN}{*_test.rb,test_*.rb,*_spec.rb,*.feature}`;
18+
const IGNORED_FOLDERS = [".bundle", "vendor/bundle", "node_modules", "tmp", "log"];
19+
const IGNORED_FOLDERS_EXCLUDE_PATTERN = `{${IGNORED_FOLDERS.map((folder) => `**/${folder}/**`).join(",")}}`;
20+
21+
// Build a regex to match paths containing any of the ignored folders
22+
const IGNORED_FOLDERS_PATH_REGEX = new RegExp(
23+
IGNORED_FOLDERS.map((folder) => `/${folder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`).join("|"),
24+
);
1825

1926
interface CodeLensData {
2027
type: string;
@@ -142,6 +149,10 @@ export class TestController {
142149
testFileWatcher,
143150
nestedTestDirWatcher,
144151
testFileWatcher.onDidCreate(async (uri) => {
152+
if (this.isInIgnoredFolder(uri)) {
153+
return;
154+
}
155+
145156
const workspace = vscode.workspace.getWorkspaceFolder(uri);
146157

147158
if (!workspace || !vscode.workspace.workspaceFolders) {
@@ -160,6 +171,10 @@ export class TestController {
160171
await this.addTestItemsForFile(uri, workspace, initialCollection);
161172
}),
162173
testFileWatcher.onDidChange(async (uri) => {
174+
if (this.isInIgnoredFolder(uri)) {
175+
return;
176+
}
177+
163178
const item = await this.getParentTestItem(uri);
164179

165180
if (item) {
@@ -172,6 +187,10 @@ export class TestController {
172187
}
173188
}),
174189
nestedTestDirWatcher.onDidDelete(async (uri) => {
190+
if (this.isInIgnoredFolder(uri)) {
191+
return;
192+
}
193+
175194
const pathParts = uri.fsPath.split(path.sep);
176195
if (pathParts.includes(".git")) {
177196
return;
@@ -184,6 +203,10 @@ export class TestController {
184203
}
185204
}),
186205
testFileWatcher.onDidDelete(async (uri) => {
206+
if (this.isInIgnoredFolder(uri)) {
207+
return;
208+
}
209+
187210
const item = await this.getParentTestItem(uri);
188211

189212
if (item) {
@@ -358,7 +381,8 @@ export class TestController {
358381
Does the file path match the expected glob pattern?
359382
[Read more](https://shopify.github.io/ruby-lsp/test_explorer.html)
360383
361-
Expected pattern: "**/{test,spec,features}/**/{*_test.rb,test_*.rb,*_spec.rb,*.feature}"`,
384+
Expected pattern: "**/{test,spec,features}/**/{*_test.rb,test_*.rb,*_spec.rb,*.feature}"
385+
Excluded paths: ${IGNORED_FOLDERS.map((p) => `"${p}"`).join(", ")}`,
362386
);
363387
return;
364388
}
@@ -981,7 +1005,7 @@ export class TestController {
9811005
for (const workspaceFolder of workspaceFolders) {
9821006
// Check if there is at least one Ruby test file in the workspace, otherwise we don't consider it
9831007
const pattern = this.testPattern(workspaceFolder);
984-
const files = await vscode.workspace.findFiles(pattern, undefined, 1);
1008+
const files = await vscode.workspace.findFiles(pattern, IGNORED_FOLDERS_EXCLUDE_PATTERN, 1);
9851009
if (files.length === 0) {
9861010
continue;
9871011
}
@@ -1002,7 +1026,7 @@ export class TestController {
10021026
) {
10031027
const initialCollection = item ? item.children : this.testController.items;
10041028
const pattern = this.testPattern(workspaceFolder);
1005-
const filePaths = await vscode.workspace.findFiles(pattern);
1029+
const filePaths = await vscode.workspace.findFiles(pattern, IGNORED_FOLDERS_EXCLUDE_PATTERN);
10061030
const increment = Math.floor(filePaths.length / 100);
10071031

10081032
for (const uri of filePaths) {
@@ -1146,6 +1170,11 @@ export class TestController {
11461170
return false;
11471171
}
11481172

1173+
private isInIgnoredFolder(uri: vscode.Uri): boolean {
1174+
const normalizedPath = uri.fsPath.split(path.sep).join("/");
1175+
return IGNORED_FOLDERS_PATH_REGEX.test(normalizedPath);
1176+
}
1177+
11491178
private testPattern(workspaceFolder: vscode.WorkspaceFolder) {
11501179
return new vscode.RelativePattern(workspaceFolder, TEST_FILE_PATTERN);
11511180
}

0 commit comments

Comments
 (0)