Skip to content
Open
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
2 changes: 2 additions & 0 deletions jekyll/test_explorer.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ To discover all test files in the workspace with decent performance, the Ruby LS
conventions. For a test file to be discovered, the file path must match this glob:
`**/{test,spec,features}/**/{*_test.rb,test_*.rb,*_spec.rb,*.feature}`

Tests in certain directories are automatically excluded from discovery: `.bundle`, `vendor/bundle`, `node_modules`, `tmp`, and `log`.

### Dynamically defined tests

There is limited support for tests defined via meta-programming. Initially, they will not be present in the test
Expand Down
163 changes: 158 additions & 5 deletions vscode/src/test/suite/testController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,23 +147,50 @@ suite("TestController", () => {
);
}

function createWorkspaceWithTestFile() {
function createWorkspaceWithTestFile(
options: {
testDir?: string;
testFileName?: string;
testContent?: string;
index?: number;
additionalFiles?: Array<{ path: string; content: string }>;
} = {},
) {
const {
testDir = "test",
testFileName = "foo_test.rb",
testContent = "require 'test_helper'\n\nclass FooTest < Minitest::Test; def test_foo; end; end",
index = 1,
additionalFiles = [],
} = options;

const workspacePath = fs.mkdtempSync(path.join(os.tmpdir(), "ruby-lsp-test-controller-"));
const workspaceUri = vscode.Uri.file(workspacePath);

fs.mkdirSync(path.join(workspaceUri.fsPath, "test"));
const testFilePath = path.join(workspaceUri.fsPath, "test", "foo_test.rb");
fs.writeFileSync(testFilePath, "require 'test_helper'\n\nclass FooTest < Minitest::Test; def test_foo; end; end");
fs.mkdirSync(path.join(workspaceUri.fsPath, testDir), { recursive: true });
const testFilePath = path.join(workspaceUri.fsPath, testDir, testFileName);
fs.writeFileSync(testFilePath, testContent);

// Create additional files if specified
additionalFiles.forEach(({ path: filePath, content }) => {
const fullPath = path.join(workspaceUri.fsPath, filePath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content);
});

const workspaceFolder: vscode.WorkspaceFolder = {
uri: workspaceUri,
name: path.basename(workspacePath),
index: 1,
index,
};

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

function cleanupWorkspace(workspaceFolder: vscode.WorkspaceFolder) {
fs.rmSync(workspaceFolder.uri.fsPath, { recursive: true, force: true });
}

function stubWorkspaceOperations(...workspaces: vscode.WorkspaceFolder[]) {
workspaceStubs.forEach((stub) => stub.restore());
workspaceStubs = [];
Expand Down Expand Up @@ -927,4 +954,130 @@ suite("TestController", () => {

assert.ok(spy.calledOnce);
});

test("does not discover tests in .bundle directory", async () => {
const { workspaceFolder, testFileUri } = createWorkspaceWithTestFile({
testFileName: "my_test.rb",
testContent: "class MyTest < Minitest::Test; end",
index: 0,
additionalFiles: [
{
path: ".bundle/gems/test/gem_test.rb",
content: "class GemTest < Minitest::Test; end",
},
],
});

stubWorkspaceOperations(workspaceFolder);

await controller.testController.resolveHandler!(undefined);

const collection = controller.testController.items;
const testDir = collection.get(vscode.Uri.joinPath(workspaceFolder.uri, "test").toString());

// Should find the regular test file
assert.ok(testDir);
const regularTest = testDir.children.get(testFileUri.toString());
assert.ok(regularTest, "Regular test file should be discovered");

// Should NOT find the test file in ignored directory
const ignoredTestDir = collection.get(
vscode.Uri.joinPath(workspaceFolder.uri, ".bundle", "gems", "test").toString(),
);
assert.strictEqual(ignoredTestDir, undefined, "Ignored directory should not be discovered");

cleanupWorkspace(workspaceFolder);
});

test("does not discover tests in vendor/bundle directory", async () => {
const { workspaceFolder, testFileUri } = createWorkspaceWithTestFile({
testDir: "spec",
testFileName: "my_spec.rb",
testContent: "RSpec.describe 'MySpec' do; end",
index: 0,
additionalFiles: [
{
path: "vendor/bundle/gems/spec/gem_spec.rb",
content: "RSpec.describe 'GemSpec' do; end",
},
],
});

stubWorkspaceOperations(workspaceFolder);

await controller.testController.resolveHandler!(undefined);

const collection = controller.testController.items;
const specDir = collection.get(vscode.Uri.joinPath(workspaceFolder.uri, "spec").toString());

// Should find the regular spec file
assert.ok(specDir);
const regularSpec = specDir.children.get(testFileUri.toString());
assert.ok(regularSpec, "Regular spec file should be discovered");

// Should NOT find the test file in ignored directory
const ignoredTestDir = collection.get(
vscode.Uri.joinPath(workspaceFolder.uri, "vendor", "bundle", "gems", "test").toString(),
);
assert.strictEqual(ignoredTestDir, undefined, "Ignored directory should not be discovered");

cleanupWorkspace(workspaceFolder);
});

suite("isInIgnoredFolder", () => {
test("detects files in .bundle directory", () => {
const uri = vscode.Uri.file("/workspace/.bundle/gems/ruby/3.3.0/gems/rspec-core-3.12.0/spec/rspec_spec.rb");
assert.ok((controller as any).isInIgnoredFolder(uri));
});

test("detects files in vendor/bundle directory", () => {
const uri = vscode.Uri.file("/workspace/vendor/bundle/ruby/3.3.0/gems/rspec-core-3.12.0/spec/rspec_spec.rb");
assert.ok((controller as any).isInIgnoredFolder(uri));
});

test("detects files in nested .bundle paths", () => {
const uri = vscode.Uri.file("/workspace/subfolder/.bundle/ruby/3.3.0/gems/minitest-5.0.0/test/minitest_test.rb");
assert.ok((controller as any).isInIgnoredFolder(uri));
});

test("detects files in node_modules directory", () => {
const uri = vscode.Uri.file("/workspace/node_modules/@types/node/test/test.rb");
assert.ok((controller as any).isInIgnoredFolder(uri));
});

test("detects files in tmp directory", () => {
const uri = vscode.Uri.file("/workspace/tmp/cache/test_file.rb");
assert.ok((controller as any).isInIgnoredFolder(uri));
});

test("detects files in log directory", () => {
const uri = vscode.Uri.file("/workspace/log/test/test_spec.rb");
assert.ok((controller as any).isInIgnoredFolder(uri));
});

test("does not detect regular test files", () => {
const uri = vscode.Uri.file("/workspace/test/models/user_test.rb");
assert.strictEqual((controller as any).isInIgnoredFolder(uri), false);
});

test("does not detect spec files in regular paths", () => {
const uri = vscode.Uri.file("/workspace/spec/models/user_spec.rb");
assert.strictEqual((controller as any).isInIgnoredFolder(uri), false);
});

test("does not detect files with 'bundle' in name but not in ignored directory", () => {
const uri = vscode.Uri.file("/workspace/test/bundle_test.rb");
assert.strictEqual((controller as any).isInIgnoredFolder(uri), false);
});

test("does not detect files with 'vendor' in name but not in vendor/bundle", () => {
const uri = vscode.Uri.file("/workspace/test/vendor_test.rb");
assert.strictEqual((controller as any).isInIgnoredFolder(uri), false);
});

test("does not detect files in vendor directory that are not in vendor/bundle", () => {
const uri = vscode.Uri.file("/workspace/vendor/plugins/my_plugin/test/plugin_test.rb");
assert.strictEqual((controller as any).isInIgnoredFolder(uri), false);
});
});
});
35 changes: 32 additions & 3 deletions vscode/src/testController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ const asyncExec = promisify(exec);

const NESTED_TEST_DIR_PATTERN = "**/{test,spec,features}/**/";
const TEST_FILE_PATTERN = `${NESTED_TEST_DIR_PATTERN}{*_test.rb,test_*.rb,*_spec.rb,*.feature}`;
const IGNORED_FOLDERS = [".bundle", "vendor/bundle", "node_modules", "tmp", "log"];
const IGNORED_FOLDERS_EXCLUDE_PATTERN = `{${IGNORED_FOLDERS.map((folder) => `**/${folder}/**`).join(",")}}`;

// Build a regex to match paths containing any of the ignored folders
const IGNORED_FOLDERS_PATH_REGEX = new RegExp(
IGNORED_FOLDERS.map((folder) => `/${folder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`).join("|"),
);

interface CodeLensData {
type: string;
Expand Down Expand Up @@ -142,6 +149,10 @@ export class TestController {
testFileWatcher,
nestedTestDirWatcher,
testFileWatcher.onDidCreate(async (uri) => {
if (this.isInIgnoredFolder(uri)) {
return;
}

const workspace = vscode.workspace.getWorkspaceFolder(uri);

if (!workspace || !vscode.workspace.workspaceFolders) {
Expand All @@ -160,6 +171,10 @@ export class TestController {
await this.addTestItemsForFile(uri, workspace, initialCollection);
}),
testFileWatcher.onDidChange(async (uri) => {
if (this.isInIgnoredFolder(uri)) {
return;
}

const item = await this.getParentTestItem(uri);

if (item) {
Expand All @@ -172,6 +187,10 @@ export class TestController {
}
}),
nestedTestDirWatcher.onDidDelete(async (uri) => {
if (this.isInIgnoredFolder(uri)) {
return;
}

const pathParts = uri.fsPath.split(path.sep);
if (pathParts.includes(".git")) {
return;
Expand All @@ -184,6 +203,10 @@ export class TestController {
}
}),
testFileWatcher.onDidDelete(async (uri) => {
if (this.isInIgnoredFolder(uri)) {
return;
}

const item = await this.getParentTestItem(uri);

if (item) {
Expand Down Expand Up @@ -358,7 +381,8 @@ export class TestController {
Does the file path match the expected glob pattern?
[Read more](https://shopify.github.io/ruby-lsp/test_explorer.html)

Expected pattern: "**/{test,spec,features}/**/{*_test.rb,test_*.rb,*_spec.rb,*.feature}"`,
Expected pattern: "**/{test,spec,features}/**/{*_test.rb,test_*.rb,*_spec.rb,*.feature}"
Excluded paths: ${IGNORED_FOLDERS.map((p) => `"${p}"`).join(", ")}`,
);
return;
}
Expand Down Expand Up @@ -981,7 +1005,7 @@ export class TestController {
for (const workspaceFolder of workspaceFolders) {
// Check if there is at least one Ruby test file in the workspace, otherwise we don't consider it
const pattern = this.testPattern(workspaceFolder);
const files = await vscode.workspace.findFiles(pattern, undefined, 1);
const files = await vscode.workspace.findFiles(pattern, IGNORED_FOLDERS_EXCLUDE_PATTERN, 1);
if (files.length === 0) {
continue;
}
Expand All @@ -1002,7 +1026,7 @@ export class TestController {
) {
const initialCollection = item ? item.children : this.testController.items;
const pattern = this.testPattern(workspaceFolder);
const filePaths = await vscode.workspace.findFiles(pattern);
const filePaths = await vscode.workspace.findFiles(pattern, IGNORED_FOLDERS_EXCLUDE_PATTERN);
const increment = Math.floor(filePaths.length / 100);

for (const uri of filePaths) {
Expand Down Expand Up @@ -1146,6 +1170,11 @@ export class TestController {
return false;
}

private isInIgnoredFolder(uri: vscode.Uri): boolean {
const normalizedPath = uri.fsPath.split(path.sep).join("/");
return IGNORED_FOLDERS_PATH_REGEX.test(normalizedPath);
}

private testPattern(workspaceFolder: vscode.WorkspaceFolder) {
return new vscode.RelativePattern(workspaceFolder, TEST_FILE_PATTERN);
}
Expand Down