Skip to content

Commit 5cc9f83

Browse files
committed
Restructure the tree in the Test Explorer View
With this change we display the tree based on the file system not based on ql-packs. We also merge test folders whose only child is another test folder. Resolves #595
1 parent 34f683b commit 5cc9f83

4 files changed

Lines changed: 119 additions & 98 deletions

File tree

extensions/ql-vscode/src/cli.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,11 @@ export class CodeQLCliServer implements Disposable {
470470
const subcommandArgs = [
471471
testPath
472472
];
473-
return await this.runJsonCodeQlCliCommand<ResolvedTests>(['resolve', 'tests'], subcommandArgs, 'Resolving tests');
473+
return await this.runJsonCodeQlCliCommand<ResolvedTests>(
474+
['resolve', 'tests', '--strict-test-discovery'],
475+
subcommandArgs,
476+
'Resolving tests'
477+
);
474478
}
475479

476480
/**

extensions/ql-vscode/src/qltest-discovery.ts

Lines changed: 55 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as path from 'path';
2-
import { QLPackDiscovery, QLPack } from './qlpack-discovery';
2+
import { QLPackDiscovery } from './qlpack-discovery';
33
import { Discovery } from './discovery';
4-
import { EventEmitter, Event, Uri, RelativePattern, WorkspaceFolder, env, workspace } from 'vscode';
4+
import { EventEmitter, Event, Uri, RelativePattern, WorkspaceFolder, env } from 'vscode';
55
import { MultiFileSystemWatcher } from './vscode-utils/multi-file-system-watcher';
66
import { CodeQLCliServer } from './cli';
77

@@ -29,9 +29,8 @@ export abstract class QLTestNode {
2929
* A directory containing one or more QL tests or other test directories.
3030
*/
3131
export class QLTestDirectory extends QLTestNode {
32-
private _children: QLTestNode[] = [];
3332

34-
constructor(_path: string, _name: string) {
33+
constructor(_path: string, _name: string, private _children: QLTestNode[] = []) {
3534
super(_path, _name);
3635
}
3736

@@ -55,10 +54,23 @@ export class QLTestDirectory extends QLTestNode {
5554
}
5655

5756
public finish(): void {
57+
// remove empty directories
58+
this._children.filter(child =>
59+
child instanceof QLTestFile || child.children.length > 0
60+
);
5861
this._children.sort((a, b) => a.name.localeCompare(b.name, env.language));
59-
for (const child of this._children) {
62+
this._children.forEach((child, i) => {
6063
child.finish();
61-
}
64+
if (child.children?.length === 1 && child.children[0] instanceof QLTestDirectory) {
65+
// collapse children
66+
const replacement = new QLTestDirectory(
67+
child.children[0].path,
68+
child.name + ' / ' + child.children[0].name,
69+
Array.from(child.children[0].children)
70+
);
71+
this._children[i] = replacement;
72+
}
73+
});
6274
}
6375

6476
private createChildDirectory(name: string): QLTestDirectory {
@@ -96,14 +108,15 @@ export class QLTestFile extends QLTestNode {
96108
*/
97109
interface QLTestDiscoveryResults {
98110
/**
99-
* The root test directory for each QL pack that contains tests.
111+
* A directory that contains one or more QL Tests, or other QLTestDirectories.
100112
*/
101-
testDirectories: QLTestDirectory[];
113+
testDirectory: QLTestDirectory | undefined;
114+
102115
/**
103-
* The list of file system paths to watch. If any of these paths changes, the discovery results
104-
* may be out of date.
116+
* The file system path to a directory to watch. If any ql or qlref file changes in
117+
* this directory, then this signifies a change in tests.
105118
*/
106-
watchPaths: string[];
119+
watchPath: string;
107120
}
108121

109122
/**
@@ -112,7 +125,7 @@ interface QLTestDiscoveryResults {
112125
export class QLTestDiscovery extends Discovery<QLTestDiscoveryResults> {
113126
private readonly _onDidChangeTests = this.push(new EventEmitter<void>());
114127
private readonly watcher: MultiFileSystemWatcher = this.push(new MultiFileSystemWatcher());
115-
private _testDirectories: QLTestDirectory[] = [];
128+
private _testDirectory: QLTestDirectory | undefined;
116129

117130
constructor(
118131
private readonly qlPackDiscovery: QLPackDiscovery,
@@ -128,12 +141,17 @@ export class QLTestDiscovery extends Discovery<QLTestDiscoveryResults> {
128141
/**
129142
* Event to be fired when the set of discovered tests may have changed.
130143
*/
131-
public get onDidChangeTests(): Event<void> { return this._onDidChangeTests.event; }
144+
public get onDidChangeTests(): Event<void> {
145+
return this._onDidChangeTests.event;
146+
}
132147

133148
/**
134-
* The root test directory for each QL pack that contains tests.
149+
* The root directory. There is at least one test in this directory, or
150+
* in a subdirectory of this.
135151
*/
136-
public get testDirectories(): QLTestDirectory[] { return this._testDirectories; }
152+
public get testDirectory(): QLTestDirectory | undefined {
153+
return this._testDirectory;
154+
}
137155

138156
private handleDidChangeQLPacks(): void {
139157
this.refresh();
@@ -144,72 +162,49 @@ export class QLTestDiscovery extends Discovery<QLTestDiscoveryResults> {
144162
this.refresh();
145163
}
146164
}
147-
165+
static cnt = 0;
148166
protected async discover(): Promise<QLTestDiscoveryResults> {
149-
const testDirectories: QLTestDirectory[] = [];
150-
const watchPaths: string[] = [];
151-
const qlPacks = this.qlPackDiscovery.qlPacks;
152-
for (const qlPack of qlPacks) {
153-
//HACK: Assume that only QL packs whose name ends with '-tests' contain tests.
154-
if (this.isRelevantQlPack(qlPack)) {
155-
watchPaths.push(qlPack.uri.fsPath);
156-
const testPackage = await this.discoverTests(qlPack.uri.fsPath, qlPack.name);
157-
if (testPackage !== undefined) {
158-
testDirectories.push(testPackage);
159-
}
160-
}
161-
}
162-
163-
return { testDirectories, watchPaths };
167+
const timer = 'testDirectory-' + this.workspaceFolder.uri.fsPath + '-' + QLTestDiscovery.cnt++;
168+
console.time(timer);
169+
const testDirectory = await this.discoverTests();
170+
console.timeEnd(timer);
171+
return {
172+
testDirectory,
173+
watchPath: this.workspaceFolder.uri.fsPath
174+
};
164175
}
165176

166177
protected update(results: QLTestDiscoveryResults): void {
167-
this._testDirectories = results.testDirectories;
178+
this._testDirectory = results.testDirectory;
168179

169180
// Watch for changes to any `.ql` or `.qlref` file in any of the QL packs that contain tests.
170181
this.watcher.clear();
171-
results.watchPaths.forEach(watchPath => {
172-
this.watcher.addWatch(new RelativePattern(watchPath, '**/*.{ql,qlref}'));
173-
});
182+
this.watcher.addWatch(new RelativePattern(results.watchPath, '**/*.{ql,qlref}'));
174183
this._onDidChangeTests.fire();
175184
}
176185

177-
/**
178-
* Only include qlpacks suffixed with '-tests' that are contained
179-
* within the provided workspace folder.
180-
*/
181-
private isRelevantQlPack(qlPack: QLPack): boolean {
182-
return qlPack.name.endsWith('-tests')
183-
&& workspace.getWorkspaceFolder(qlPack.uri)?.index === this.workspaceFolder.index;
184-
}
185-
186186
/**
187187
* Discover all QL tests in the specified directory and its subdirectories.
188-
* @param fullPath The full path of the test directory.
189-
* @param name The display name to use for the returned `TestDirectory` object.
190188
* @returns A `QLTestDirectory` object describing the contents of the directory, or `undefined` if
191189
* no tests were found.
192190
*/
193-
private async discoverTests(fullPath: string, name: string): Promise<QLTestDirectory | undefined> {
191+
private async discoverTests(): Promise<QLTestDirectory> {
192+
const fullPath = this.workspaceFolder.uri.fsPath;
193+
const name = this.workspaceFolder.name;
194194
const resolvedTests = (await this.cliServer.resolveTests(fullPath))
195195
.filter((testPath) => !QLTestDiscovery.ignoreTestPath(testPath));
196196

197-
if (resolvedTests.length === 0) {
198-
return undefined;
197+
const rootDirectory = new QLTestDirectory(fullPath, name);
198+
for (const testPath of resolvedTests) {
199+
const relativePath = path.normalize(path.relative(fullPath, testPath));
200+
const dirName = path.dirname(relativePath);
201+
const parentDirectory = rootDirectory.createDirectory(dirName);
202+
parentDirectory.addChild(new QLTestFile(testPath, path.basename(testPath)));
199203
}
200-
else {
201-
const rootDirectory = new QLTestDirectory(fullPath, name);
202-
for (const testPath of resolvedTests) {
203-
const relativePath = path.normalize(path.relative(fullPath, testPath));
204-
const dirName = path.dirname(relativePath);
205-
const parentDirectory = rootDirectory.createDirectory(dirName);
206-
parentDirectory.addChild(new QLTestFile(testPath, path.basename(testPath)));
207-
}
208204

209-
rootDirectory.finish();
205+
rootDirectory.finish();
210206

211-
return rootDirectory;
212-
}
207+
return rootDirectory;
213208
}
214209

215210
/**

extensions/ql-vscode/src/test-adapter.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
9999

100100
this.qlPackDiscovery = this.push(new QLPackDiscovery(workspaceFolder, cliServer));
101101
this.qlTestDiscovery = this.push(new QLTestDiscovery(this.qlPackDiscovery, workspaceFolder, cliServer));
102+
// TODO: Don't run test discovery until pack discovery finishes
102103
this.qlPackDiscovery.refresh();
103104
this.qlTestDiscovery.refresh();
104105

@@ -160,22 +161,22 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
160161
private discoverTests(): void {
161162
this._tests.fire({ type: 'started' } as TestLoadStartedEvent);
162163

163-
const testDirectories = this.qlTestDiscovery.testDirectories;
164-
const children = testDirectories.map(
165-
testDirectory => QLTestAdapter.createTestSuiteInfo(testDirectory, testDirectory.name)
166-
);
167-
const testSuite: TestSuiteInfo = {
168-
type: 'suite',
169-
label: 'CodeQL',
170-
id: '.',
171-
children
172-
};
173-
164+
const testDirectory = this.qlTestDiscovery.testDirectory;
165+
let testSuite: TestSuiteInfo | undefined;
166+
if (testDirectory?.children.length) {
167+
const children = QLTestAdapter.createTestOrSuiteInfos(testDirectory.children);
168+
testSuite = {
169+
type: 'suite',
170+
label: 'CodeQL',
171+
id: testDirectory.path,
172+
children
173+
};
174+
}
174175
this._tests.fire({
175176
type: 'finished',
176-
suite: children.length > 0 ? testSuite : undefined
177+
suite: testSuite
177178
} as TestLoadFinishedEvent);
178-
}
179+
}
179180

180181
public async run(tests: string[]): Promise<void> {
181182
if (this.runningTask !== undefined) {
Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,61 @@
11
import 'vscode-test';
22
import 'mocha';
3-
import * as path from 'path';
4-
import { Uri, workspace } from 'vscode';
3+
import { Uri, WorkspaceFolder } from 'vscode';
54
import { expect } from 'chai';
65

76
import { QLTestDiscovery } from '../../qltest-discovery';
87

98
describe('qltest-discovery', () => {
10-
describe('isRelevantQlPack', () => {
11-
it('should check if a qlpack is relevant', () => {
12-
const qlTestDiscover: any = new QLTestDiscovery(
9+
describe('discoverTests', () => {
10+
it('should run discovery', async () => {
11+
const baseUri = Uri.parse('file:/a/b');
12+
const baseDir = baseUri.fsPath;
13+
const cDir = Uri.parse('file:/a/b/c').fsPath;
14+
const dFile = Uri.parse('file:/a/b/c/d.ql').fsPath;
15+
const eFile = Uri.parse('file:/a/b/c/e.ql').fsPath;
16+
const hDir = Uri.parse('file:/a/b/c/f/g/h').fsPath;
17+
const iFile = Uri.parse('file:/a/b/c/f/g/h/i.ql').fsPath;
18+
const qlTestDiscover = new QLTestDiscovery(
1319
{ onDidChangeQLPacks: () => ({}) } as any,
14-
{ index: 0 } as any,
15-
{} as any
20+
{
21+
uri: baseUri,
22+
name: 'My tests'
23+
} as unknown as WorkspaceFolder,
24+
{
25+
resolveTests() {
26+
return [
27+
Uri.parse('file:/a/b/c/d.ql').fsPath,
28+
Uri.parse('file:/a/b/c/e.ql').fsPath,
29+
Uri.parse('file:/a/b/c/f/g/h/i.ql').fsPath
30+
];
31+
}
32+
} as any
1633
);
1734

18-
const uri = workspace.workspaceFolders![0].uri;
19-
expect(qlTestDiscover.isRelevantQlPack({
20-
name: '-hucairz',
21-
uri
22-
})).to.be.false;
35+
const result = await (qlTestDiscover as any).discover();
36+
expect(result.watchPath).to.eq(baseDir);
37+
expect(result.testDirectory.path).to.eq(baseDir);
38+
expect(result.testDirectory.name).to.eq('My tests');
2339

24-
expect(qlTestDiscover.isRelevantQlPack({
25-
name: '-tests',
26-
uri: Uri.file('/a/b/')
27-
})).to.be.false;
40+
let children = result.testDirectory.children;
41+
expect(children[0].path).to.eq(cDir);
42+
expect(children[0].name).to.eq('c');
43+
expect(children.length).to.eq(1);
2844

29-
expect(qlTestDiscover.isRelevantQlPack({
30-
name: '-tests',
31-
uri
32-
})).to.be.true;
45+
children = children[0].children;
46+
expect(children[0].path).to.eq(dFile);
47+
expect(children[0].name).to.eq('d.ql');
48+
expect(children[1].path).to.eq(eFile);
49+
expect(children[1].name).to.eq('e.ql');
3350

34-
expect(qlTestDiscover.isRelevantQlPack({
35-
name: '-tests',
36-
uri: Uri.file(path.join(uri.fsPath, 'other'))
37-
})).to.be.true;
51+
// A merged foler
52+
expect(children[2].path).to.eq(hDir);
53+
expect(children[2].name).to.eq('f / g / h');
54+
expect(children.length).to.eq(3);
55+
56+
children = children[2].children;
57+
expect(children[0].path).to.eq(iFile);
58+
expect(children[0].name).to.eq('i.ql');
3859
});
3960
});
4061
});

0 commit comments

Comments
 (0)