Skip to content

Commit 0eaaaab

Browse files
committed
Add unit tests for add/remove database
In order to do this, needed to move `databases.test.ts` to the `minimal-workspace` test folder because these tests require that there be some kind of workspace open in order to check on workspace folders. Unfortunately, during tests vscode does not allow you to convert from a single root workspace to multi-root and so several of the workspace functions needed to be stubbed out.
1 parent 9fca57b commit 0eaaaab

5 files changed

Lines changed: 334 additions & 205 deletions

File tree

extensions/ql-vscode/src/databases.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export interface DatabaseOptions {
3838
dateAdded?: number | undefined;
3939
}
4040

41-
interface FullDatabaseOptions extends DatabaseOptions {
41+
export interface FullDatabaseOptions extends DatabaseOptions {
4242
ignoreSourceArchive: boolean;
4343
dateAdded: number | undefined;
4444
}
@@ -674,16 +674,19 @@ export class DatabaseManager extends DisposableObject {
674674
}
675675

676676
public removeDatabaseItem(item: DatabaseItem) {
677-
if (this._currentDatabaseItem == item)
677+
if (this._currentDatabaseItem == item) {
678678
this._currentDatabaseItem = undefined;
679+
}
679680
const index = this.databaseItems.findIndex(searchItem => searchItem === item);
680681
if (index >= 0) {
681682
this._databaseItems.splice(index, 1);
682683
}
683684
this.updatePersistedDatabaseList();
684685

685686
// Delete folder from workspace, if it is still there
686-
const folderIndex = (vscode.workspace.workspaceFolders || []).findIndex(folder => item.belongsToSourceArchiveExplorerUri(folder.uri));
687+
const folderIndex = (vscode.workspace.workspaceFolders || []).findIndex(
688+
folder => item.belongsToSourceArchiveExplorerUri(folder.uri)
689+
);
687690
if (folderIndex >= 0) {
688691
logger.log(`Removing workspace folder at index ${folderIndex}`);
689692
vscode.workspace.updateWorkspaceFolders(folderIndex, 1);
@@ -692,9 +695,9 @@ export class DatabaseManager extends DisposableObject {
692695
// Delete folder from file system only if it is controlled by the extension
693696
if (this.isExtensionControlledLocation(item.databaseUri)) {
694697
logger.log('Deleting database from filesystem.');
695-
fs.remove(item.databaseUri.path).then(
696-
() => logger.log(`Deleted '${item.databaseUri.path}'`),
697-
e => logger.log(`Failed to delete '${item.databaseUri.path}'. Reason: ${e.message}`));
698+
fs.remove(item.databaseUri.fsPath).then(
699+
() => logger.log(`Deleted '${item.databaseUri.fsPath}'`),
700+
e => logger.log(`Failed to delete '${item.databaseUri.fsPath}'. Reason: ${e.message}`));
698701
}
699702

700703
// note that we use undefined as the item in order to reset the entire tree
@@ -715,7 +718,7 @@ export class DatabaseManager extends DisposableObject {
715718

716719
private isExtensionControlledLocation(uri: vscode.Uri) {
717720
const storagePath = this.ctx.storagePath || this.ctx.globalStoragePath;
718-
return uri.path.startsWith(storagePath);
721+
return uri.fsPath.startsWith(storagePath);
719722
}
720723
}
721724

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
import 'vscode-test';
2+
import 'mocha';
3+
import * as sinon from 'sinon';
4+
import * as tmp from 'tmp';
5+
import * as fs from 'fs-extra';
6+
import * as path from 'path';
7+
import { expect } from 'chai';
8+
import { ExtensionContext, Uri, workspace } from 'vscode';
9+
10+
import {
11+
DatabaseEventKind,
12+
DatabaseManager,
13+
DatabaseItemImpl,
14+
DatabaseContents,
15+
isLikelyDbLanguageFolder,
16+
FullDatabaseOptions
17+
} from '../../databases';
18+
import { QueryServerConfig } from '../../config';
19+
import { Logger } from '../../logging';
20+
import { encodeArchiveBasePath, encodeSourceArchiveUri } from '../../archive-filesystem-provider';
21+
22+
describe('databases', () => {
23+
24+
const MOCK_DB_OPTIONS: FullDatabaseOptions = {
25+
dateAdded: 123,
26+
ignoreSourceArchive: false
27+
};
28+
29+
let databaseManager: DatabaseManager;
30+
let updateSpy: sinon.SinonSpy;
31+
let getSpy: sinon.SinonStub;
32+
let dbChangedHandler: sinon.SinonSpy;
33+
34+
let sandbox: sinon.SinonSandbox;
35+
let dir: tmp.DirResult;
36+
37+
38+
39+
beforeEach(() => {
40+
dir = tmp.dirSync();
41+
sandbox = sinon.createSandbox();
42+
updateSpy = sandbox.spy();
43+
getSpy = sandbox.stub();
44+
dbChangedHandler = sandbox.spy();
45+
databaseManager = new DatabaseManager(
46+
{
47+
workspaceState: {
48+
update: updateSpy,
49+
get: getSpy
50+
},
51+
// pretend like databases added in the temp dir are controlled by the extension
52+
// so that they are deleted upon removal
53+
storagePath: dir.name
54+
} as unknown as ExtensionContext,
55+
{} as QueryServerConfig,
56+
{} as Logger,
57+
);
58+
59+
// Unfortunately, during a test it is not possible to convert from
60+
// a single root workspace to multi-root, so must stub out relevant
61+
// functions
62+
sandbox.stub(workspace, 'updateWorkspaceFolders');
63+
sandbox.spy(workspace, 'onDidChangeWorkspaceFolders');
64+
});
65+
66+
afterEach(async () => {
67+
dir.removeCallback();
68+
sandbox.restore();
69+
});
70+
71+
it('should fire events when adding and removing a db item', () => {
72+
const mockDbItem = createMockDB();
73+
const spy = sinon.spy();
74+
databaseManager.onDidChangeDatabaseItem(spy);
75+
(databaseManager as any).addDatabaseItem(mockDbItem);
76+
77+
expect((databaseManager as any)._databaseItems).to.deep.eq([mockDbItem]);
78+
expect(updateSpy).to.have.been.calledWith('databaseList', [{
79+
options: MOCK_DB_OPTIONS,
80+
uri: dbLocationUri().toString(true)
81+
}]);
82+
expect(spy).to.have.been.calledWith({
83+
item: undefined,
84+
kind: DatabaseEventKind.Add
85+
});
86+
87+
sinon.reset();
88+
89+
// now remove the item
90+
databaseManager.removeDatabaseItem(mockDbItem);
91+
expect((databaseManager as any)._databaseItems).to.deep.eq([]);
92+
expect(updateSpy).to.have.been.calledWith('databaseList', []);
93+
expect(spy).to.have.been.calledWith({
94+
item: undefined,
95+
kind: DatabaseEventKind.Remove
96+
});
97+
});
98+
99+
it('should rename a db item and emit an event', () => {
100+
const mockDbItem = createMockDB();
101+
const spy = sinon.spy();
102+
databaseManager.onDidChangeDatabaseItem(spy);
103+
(databaseManager as any).addDatabaseItem(mockDbItem);
104+
sinon.restore();
105+
106+
databaseManager.renameDatabaseItem(mockDbItem, 'new name');
107+
108+
expect(mockDbItem.name).to.eq('new name');
109+
expect(updateSpy).to.have.been.calledWith('databaseList', [{
110+
options: { ...MOCK_DB_OPTIONS, displayName: 'new name' },
111+
uri: dbLocationUri().toString(true)
112+
}]);
113+
114+
expect(spy).to.have.been.calledWith({
115+
item: undefined,
116+
kind: DatabaseEventKind.Rename
117+
});
118+
});
119+
120+
describe('add / remove database items', () => {
121+
it('should add a database item', async () => {
122+
const spy = sandbox.spy();
123+
databaseManager.onDidChangeDatabaseItem(spy);
124+
const mockDbItem = createMockDB();
125+
126+
await (databaseManager as any).addDatabaseItem(mockDbItem);
127+
128+
expect(databaseManager.databaseItems).to.deep.eq([mockDbItem]);
129+
expect(updateSpy).to.have.been.calledWith('databaseList', [{
130+
uri: dbLocationUri().toString(true),
131+
options: MOCK_DB_OPTIONS
132+
}]);
133+
134+
const mockEvent = {
135+
item: undefined,
136+
kind: DatabaseEventKind.Add
137+
};
138+
expect(spy).to.have.been.calledWith(mockEvent);
139+
});
140+
141+
it('should add a database item source archive', async function() {
142+
const mockDbItem = createMockDB();
143+
mockDbItem.name = 'xxx';
144+
await (databaseManager as any).addDatabaseSourceArchiveFolder(mockDbItem);
145+
146+
// workspace folders should be updated. We can only check the mocks since
147+
// when running as a test, we are not allowed to update the workspace folders
148+
expect(workspace.updateWorkspaceFolders).to.have.been.calledWith(1, 0, {
149+
name: '[xxx source archive]',
150+
// must use a matcher here since vscode URIs with the same path
151+
// are not always equal due to internal state.
152+
uri: sinon.match.has('fsPath', encodeArchiveBasePath(sourceLocationUri().fsPath).fsPath)
153+
});
154+
});
155+
156+
it('should remove a database item', async () => {
157+
const mockDbItem = createMockDB();
158+
sandbox.stub(fs, 'remove').resolves();
159+
160+
// pretend that this item is the first workspace folder in the list
161+
sandbox.stub(mockDbItem, 'belongsToSourceArchiveExplorerUri').returns(true);
162+
163+
await (databaseManager as any).addDatabaseItem(mockDbItem);
164+
updateSpy.resetHistory();
165+
166+
await databaseManager.removeDatabaseItem(mockDbItem);
167+
168+
expect(databaseManager.databaseItems).to.deep.eq([]);
169+
expect(updateSpy).to.have.been.calledWith('databaseList', []);
170+
// should remove the folder
171+
expect(workspace.updateWorkspaceFolders).to.have.been.calledWith(0, 1);
172+
173+
// should also delete the db contents
174+
expect(fs.remove).to.have.been.calledWith(mockDbItem.databaseUri.fsPath);
175+
});
176+
177+
it('should remove a database item outside of the extension controlled area', async () => {
178+
const mockDbItem = createMockDB();
179+
sandbox.stub(fs, 'remove').resolves();
180+
181+
// pretend that this item is the first workspace folder in the list
182+
sandbox.stub(mockDbItem, 'belongsToSourceArchiveExplorerUri').returns(true);
183+
184+
await (databaseManager as any).addDatabaseItem(mockDbItem);
185+
updateSpy.resetHistory();
186+
187+
// pretend that the database location is not controlled by the extension
188+
(databaseManager as any).ctx.storagePath = 'hucairz';
189+
190+
await databaseManager.removeDatabaseItem(mockDbItem);
191+
192+
expect(databaseManager.databaseItems).to.deep.eq([]);
193+
expect(updateSpy).to.have.been.calledWith('databaseList', []);
194+
// should remove the folder
195+
expect(workspace.updateWorkspaceFolders).to.have.been.calledWith(0, 1);
196+
197+
// should NOT delete the db contents
198+
expect(fs.remove).not.to.have.been.called;
199+
});
200+
});
201+
202+
describe('resolveSourceFile', () => {
203+
it('should fail to resolve when not a uri', () => {
204+
const db = createMockDB(Uri.parse('file:/sourceArchive-uri/'));
205+
(db as any)._contents.sourceArchiveUri = undefined;
206+
expect(() => db.resolveSourceFile('abc')).to.throw('Scheme is missing');
207+
});
208+
209+
it('should fail to resolve when not a file uri', () => {
210+
const db = createMockDB(Uri.parse('file:/sourceArchive-uri/'));
211+
(db as any)._contents.sourceArchiveUri = undefined;
212+
expect(() => db.resolveSourceFile('http://abc')).to.throw('Invalid uri scheme');
213+
});
214+
215+
describe('no source archive', () => {
216+
it('should resolve undefined', () => {
217+
const db = createMockDB(Uri.parse('file:/sourceArchive-uri/'));
218+
(db as any)._contents.sourceArchiveUri = undefined;
219+
const resolved = db.resolveSourceFile(undefined);
220+
expect(resolved.toString(true)).to.eq(dbLocationUri().toString(true));
221+
});
222+
223+
it('should resolve an empty file', () => {
224+
const db = createMockDB(Uri.parse('file:/sourceArchive-uri/'));
225+
(db as any)._contents.sourceArchiveUri = undefined;
226+
const resolved = db.resolveSourceFile('file:');
227+
expect(resolved.toString()).to.eq('file:///');
228+
});
229+
});
230+
231+
describe('zipped source archive', () => {
232+
it('should encode a source archive url', () => {
233+
const db = createMockDB(encodeSourceArchiveUri({
234+
sourceArchiveZipPath: 'sourceArchive-uri',
235+
pathWithinSourceArchive: 'def'
236+
}));
237+
const resolved = db.resolveSourceFile(Uri.file('abc').toString());
238+
239+
// must recreate an encoded archive uri instead of typing out the
240+
// text since the uris will be different on windows and ubuntu.
241+
expect(resolved.toString()).to.eq(encodeSourceArchiveUri({
242+
sourceArchiveZipPath: 'sourceArchive-uri',
243+
pathWithinSourceArchive: 'def/abc'
244+
}).toString());
245+
});
246+
247+
it('should encode a source archive url with trailing slash', () => {
248+
const db = createMockDB(encodeSourceArchiveUri({
249+
sourceArchiveZipPath: 'sourceArchive-uri',
250+
pathWithinSourceArchive: 'def/'
251+
}));
252+
const resolved = db.resolveSourceFile(Uri.file('abc').toString());
253+
254+
// must recreate an encoded archive uri instead of typing out the
255+
// text since the uris will be different on windows and ubuntu.
256+
expect(resolved.toString()).to.eq(encodeSourceArchiveUri({
257+
sourceArchiveZipPath: 'sourceArchive-uri',
258+
pathWithinSourceArchive: 'def/abc'
259+
}).toString());
260+
});
261+
262+
it('should encode an empty source archive url', () => {
263+
const db = createMockDB(encodeSourceArchiveUri({
264+
sourceArchiveZipPath: 'sourceArchive-uri',
265+
pathWithinSourceArchive: 'def'
266+
}));
267+
const resolved = db.resolveSourceFile('file:');
268+
expect(resolved.toString()).to.eq('codeql-zip-archive://1-18/sourceArchive-uri/def/');
269+
});
270+
});
271+
272+
it('should handle an empty file', () => {
273+
const db = createMockDB(Uri.parse('file:/sourceArchive-uri/'));
274+
const resolved = db.resolveSourceFile('');
275+
expect(resolved.toString()).to.eq('file:///sourceArchive-uri/');
276+
});
277+
});
278+
279+
it('should find likely db language folders', () => {
280+
expect(isLikelyDbLanguageFolder('db-javascript')).to.be.true;
281+
expect(isLikelyDbLanguageFolder('dbnot-a-db')).to.be.false;
282+
});
283+
284+
function createMockDB(
285+
// source archive location must be a real(-ish) location since
286+
// tests will add this to the workspace location
287+
sourceArchiveUri = sourceLocationUri(),
288+
databaseUri = dbLocationUri()
289+
): DatabaseItemImpl {
290+
291+
return new DatabaseItemImpl(
292+
databaseUri,
293+
{
294+
sourceArchiveUri
295+
} as DatabaseContents,
296+
MOCK_DB_OPTIONS,
297+
dbChangedHandler
298+
);
299+
}
300+
301+
function sourceLocationUri() {
302+
return Uri.file(path.join(dir.name, 'src.zip'));
303+
}
304+
305+
function dbLocationUri() {
306+
return Uri.file(path.join(dir.name, 'db'));
307+
}
308+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
import { runTestsInDirectory } from '../index-template';
2+
3+
import * as sinonChai from 'sinon-chai';
4+
import * as chai from 'chai';
5+
import * as chaiAsPromised from 'chai-as-promised';
6+
chai.use(chaiAsPromised);
7+
chai.use(sinonChai);
8+
9+
210
export function run(): Promise<void> {
311
return runTestsInDirectory(__dirname);
412
}

0 commit comments

Comments
 (0)