Skip to content

Commit ce875e7

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 ce875e7

3 files changed

Lines changed: 164 additions & 7 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 bundled gems (located in `.bundle/` or `vendor/bundle/`) are automatically excluded from discovery.
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: 138 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,110 @@ 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 bundled gem test file
984+
const bundledTestDir = collection.get(
985+
vscode.Uri.joinPath(workspaceFolder.uri, ".bundle", "gems", "test").toString(),
986+
);
987+
assert.strictEqual(bundledTestDir, undefined, "Bundled gem 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 bundled gem test file
1019+
const bundledTestDir = collection.get(
1020+
vscode.Uri.joinPath(workspaceFolder.uri, "vendor", "bundle", "gems", "test").toString(),
1021+
);
1022+
assert.strictEqual(bundledTestDir, undefined, "Bundled gem directory should not be discovered");
1023+
1024+
cleanupWorkspace(workspaceFolder);
1025+
});
1026+
1027+
suite("isInBundledGems", () => {
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).isInBundledGems(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).isInBundledGems(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).isInBundledGems(uri));
1041+
});
1042+
1043+
test("does not detect regular test files", () => {
1044+
const uri = vscode.Uri.file("/workspace/test/models/user_test.rb");
1045+
assert.strictEqual((controller as any).isInBundledGems(uri), false);
1046+
});
1047+
1048+
test("does not detect spec files in regular paths", () => {
1049+
const uri = vscode.Uri.file("/workspace/spec/models/user_spec.rb");
1050+
assert.strictEqual((controller as any).isInBundledGems(uri), false);
1051+
});
1052+
1053+
test("does not detect files with 'bundle' in name but not in bundled gems directory", () => {
1054+
const uri = vscode.Uri.file("/workspace/test/bundle_test.rb");
1055+
assert.strictEqual((controller as any).isInBundledGems(uri), false);
1056+
});
1057+
1058+
test("does not detect files with 'vendor' in name but not in vendor/bundle", () => {
1059+
const uri = vscode.Uri.file("/workspace/test/vendor_test.rb");
1060+
assert.strictEqual((controller as any).isInBundledGems(uri), false);
1061+
});
1062+
});
9301063
});

vscode/src/testController.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ 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 BUNDLED_GEMS_EXCLUDE_PATTERN = "{**/.bundle/**,**/vendor/bundle/**}";
1819

1920
interface CodeLensData {
2021
type: string;
@@ -142,6 +143,10 @@ export class TestController {
142143
testFileWatcher,
143144
nestedTestDirWatcher,
144145
testFileWatcher.onDidCreate(async (uri) => {
146+
if (this.isInBundledGems(uri)) {
147+
return;
148+
}
149+
145150
const workspace = vscode.workspace.getWorkspaceFolder(uri);
146151

147152
if (!workspace || !vscode.workspace.workspaceFolders) {
@@ -160,6 +165,10 @@ export class TestController {
160165
await this.addTestItemsForFile(uri, workspace, initialCollection);
161166
}),
162167
testFileWatcher.onDidChange(async (uri) => {
168+
if (this.isInBundledGems(uri)) {
169+
return;
170+
}
171+
163172
const item = await this.getParentTestItem(uri);
164173

165174
if (item) {
@@ -172,6 +181,10 @@ export class TestController {
172181
}
173182
}),
174183
nestedTestDirWatcher.onDidDelete(async (uri) => {
184+
if (this.isInBundledGems(uri)) {
185+
return;
186+
}
187+
175188
const pathParts = uri.fsPath.split(path.sep);
176189
if (pathParts.includes(".git")) {
177190
return;
@@ -184,6 +197,10 @@ export class TestController {
184197
}
185198
}),
186199
testFileWatcher.onDidDelete(async (uri) => {
200+
if (this.isInBundledGems(uri)) {
201+
return;
202+
}
203+
187204
const item = await this.getParentTestItem(uri);
188205

189206
if (item) {
@@ -981,7 +998,7 @@ export class TestController {
981998
for (const workspaceFolder of workspaceFolders) {
982999
// Check if there is at least one Ruby test file in the workspace, otherwise we don't consider it
9831000
const pattern = this.testPattern(workspaceFolder);
984-
const files = await vscode.workspace.findFiles(pattern, undefined, 1);
1001+
const files = await vscode.workspace.findFiles(pattern, BUNDLED_GEMS_EXCLUDE_PATTERN, 1);
9851002
if (files.length === 0) {
9861003
continue;
9871004
}
@@ -1002,7 +1019,7 @@ export class TestController {
10021019
) {
10031020
const initialCollection = item ? item.children : this.testController.items;
10041021
const pattern = this.testPattern(workspaceFolder);
1005-
const filePaths = await vscode.workspace.findFiles(pattern);
1022+
const filePaths = await vscode.workspace.findFiles(pattern, BUNDLED_GEMS_EXCLUDE_PATTERN);
10061023
const increment = Math.floor(filePaths.length / 100);
10071024

10081025
for (const uri of filePaths) {
@@ -1146,6 +1163,11 @@ export class TestController {
11461163
return false;
11471164
}
11481165

1166+
private isInBundledGems(uri: vscode.Uri): boolean {
1167+
const pathParts = uri.fsPath.split(path.sep);
1168+
return pathParts.includes(".bundle") || pathParts.includes("vendor");
1169+
}
1170+
11491171
private testPattern(workspaceFolder: vscode.WorkspaceFolder) {
11501172
return new vscode.RelativePattern(workspaceFolder, TEST_FILE_PATTERN);
11511173
}

0 commit comments

Comments
 (0)