diff --git a/extensions/ql-vscode/src/databases.ts b/extensions/ql-vscode/src/databases.ts index a8cfba97a..e6651b8e0 100644 --- a/extensions/ql-vscode/src/databases.ts +++ b/extensions/ql-vscode/src/databases.ts @@ -154,67 +154,69 @@ export async function findSourceArchive( return undefined; } -async function resolveDatabase( - databasePath: string, -): Promise { - const name = basename(databasePath); - - // Look for dataset and source archive. - const datasetUri = await findDataset(databasePath); - const sourceArchiveUri = await findSourceArchive(databasePath); - - return { - kind: DatabaseKind.Database, - name, - datasetUri, - sourceArchiveUri, - }; -} - /** Gets the relative paths of all `.dbscheme` files in the given directory. */ async function getDbSchemeFiles(dbDirectory: string): Promise { return await glob("*.dbscheme", { cwd: dbDirectory }); } -async function resolveDatabaseContents( - uri: vscode.Uri, -): Promise { - if (uri.scheme !== "file") { - throw new Error( - `Database URI scheme '${uri.scheme}' not supported; only 'file' URIs are supported.`, - ); - } - const databasePath = uri.fsPath; - if (!(await pathExists(databasePath))) { - throw new InvalidDatabaseError( - `Database '${databasePath}' does not exist.`, - ); +export class DatabaseResolver { + public static async resolveDatabaseContents( + uri: vscode.Uri, + ): Promise { + if (uri.scheme !== "file") { + throw new Error( + `Database URI scheme '${uri.scheme}' not supported; only 'file' URIs are supported.`, + ); + } + const databasePath = uri.fsPath; + if (!(await pathExists(databasePath))) { + throw new InvalidDatabaseError( + `Database '${databasePath}' does not exist.`, + ); + } + + const contents = await this.resolveDatabase(databasePath); + + if (contents === undefined) { + throw new InvalidDatabaseError( + `'${databasePath}' is not a valid database.`, + ); + } + + // Look for a single dbscheme file within the database. + // This should be found in the dataset directory, regardless of the form of database. + const dbPath = contents.datasetUri.fsPath; + const dbSchemeFiles = await getDbSchemeFiles(dbPath); + if (dbSchemeFiles.length === 0) { + throw new InvalidDatabaseError( + `Database '${databasePath}' does not contain a CodeQL dbscheme under '${dbPath}'.`, + ); + } else if (dbSchemeFiles.length > 1) { + throw new InvalidDatabaseError( + `Database '${databasePath}' contains multiple CodeQL dbschemes under '${dbPath}'.`, + ); + } else { + contents.dbSchemeUri = vscode.Uri.file(resolve(dbPath, dbSchemeFiles[0])); + } + return contents; } - const contents = await resolveDatabase(databasePath); + public static async resolveDatabase( + databasePath: string, + ): Promise { + const name = basename(databasePath); - if (contents === undefined) { - throw new InvalidDatabaseError( - `'${databasePath}' is not a valid database.`, - ); - } + // Look for dataset and source archive. + const datasetUri = await findDataset(databasePath); + const sourceArchiveUri = await findSourceArchive(databasePath); - // Look for a single dbscheme file within the database. - // This should be found in the dataset directory, regardless of the form of database. - const dbPath = contents.datasetUri.fsPath; - const dbSchemeFiles = await getDbSchemeFiles(dbPath); - if (dbSchemeFiles.length === 0) { - throw new InvalidDatabaseError( - `Database '${databasePath}' does not contain a CodeQL dbscheme under '${dbPath}'.`, - ); - } else if (dbSchemeFiles.length > 1) { - throw new InvalidDatabaseError( - `Database '${databasePath}' contains multiple CodeQL dbschemes under '${dbPath}'.`, - ); - } else { - contents.dbSchemeUri = vscode.Uri.file(resolve(dbPath, dbSchemeFiles[0])); + return { + kind: DatabaseKind.Database, + name, + datasetUri, + sourceArchiveUri, + }; } - return contents; } /** An item in the list of available databases */ @@ -370,7 +372,9 @@ export class DatabaseItemImpl implements DatabaseItem { public async refresh(): Promise { try { try { - this._contents = await resolveDatabaseContents(this.databaseUri); + this._contents = await DatabaseResolver.resolveDatabaseContents( + this.databaseUri, + ); this._error = undefined; } catch (e) { this._contents = undefined; @@ -602,7 +606,7 @@ export class DatabaseManager extends DisposableObject { uri: vscode.Uri, displayName?: string, ): Promise { - const contents = await resolveDatabaseContents(uri); + const contents = await DatabaseResolver.resolveDatabaseContents(uri); // Ignore the source archive for QLTest databases by default. const isQLTestDatabase = extname(uri.fsPath) === ".testproj"; const fullOptions: FullDatabaseOptions = { diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts index 37d804b8d..068be7385 100644 --- a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts @@ -10,6 +10,7 @@ import { DatabaseContents, FullDatabaseOptions, findSourceArchive, + DatabaseResolver, } from "../../../src/databases"; import { Logger } from "../../../src/common"; import { ProgressCallback } from "../../../src/commandRunner"; @@ -21,6 +22,7 @@ import { import { testDisposeHandler } from "../test-dispose-handler"; import { QueryRunner } from "../../../src/queryRunner"; import * as helpers from "../../../src/helpers"; +import { Setting } from "../../../src/config"; describe("databases", () => { const MOCK_DB_OPTIONS: FullDatabaseOptions = { @@ -623,6 +625,98 @@ describe("databases", () => { }); }); + describe("openDatabase", () => { + let createSkeletonPacksSpy: jest.SpyInstance; + let resolveDatabaseContentsSpy: jest.SpyInstance; + let addDatabaseSourceArchiveFolderSpy: jest.SpyInstance; + let mockDbItem: DatabaseItemImpl; + + beforeEach(() => { + createSkeletonPacksSpy = jest + .spyOn(databaseManager, "createSkeletonPacks") + .mockImplementation(async () => { + /* no-op */ + }); + + resolveDatabaseContentsSpy = jest + .spyOn(DatabaseResolver, "resolveDatabaseContents") + .mockResolvedValue({} as DatabaseContents); + + addDatabaseSourceArchiveFolderSpy = jest.spyOn( + databaseManager, + "addDatabaseSourceArchiveFolder", + ); + + jest.mock("fs", () => ({ + promises: { + pathExists: jest.fn().mockResolvedValue(true), + }, + })); + + mockDbItem = createMockDB(); + }); + + it("should resolve the database contents", async () => { + await databaseManager.openDatabase( + {} as ProgressCallback, + {} as CancellationToken, + mockDbItem.databaseUri, + ); + + expect(resolveDatabaseContentsSpy).toBeCalledTimes(1); + }); + + it("should add database source archive folder", async () => { + await databaseManager.openDatabase( + {} as ProgressCallback, + {} as CancellationToken, + mockDbItem.databaseUri, + ); + + expect(addDatabaseSourceArchiveFolderSpy).toBeCalledTimes(1); + }); + + describe("when codeQL.codespacesTemplate is set to true", () => { + it("should create a skeleton QL pack", async () => { + jest.spyOn(Setting.prototype, "getValue").mockReturnValue(true); + + await databaseManager.openDatabase( + {} as ProgressCallback, + {} as CancellationToken, + mockDbItem.databaseUri, + ); + + expect(createSkeletonPacksSpy).toBeCalledTimes(1); + }); + }); + + describe("when codeQL.codespacesTemplate is set to false", () => { + it("should not create a skeleton QL pack", async () => { + jest.spyOn(Setting.prototype, "getValue").mockReturnValue(false); + + await databaseManager.openDatabase( + {} as ProgressCallback, + {} as CancellationToken, + mockDbItem.databaseUri, + ); + expect(createSkeletonPacksSpy).toBeCalledTimes(0); + }); + }); + + describe("when codeQL.codespacesTemplate is not set", () => { + it("should not create a skeleton QL pack", async () => { + jest.spyOn(Setting.prototype, "getValue").mockReturnValue(undefined); + + await databaseManager.openDatabase( + {} as ProgressCallback, + {} as CancellationToken, + mockDbItem.databaseUri, + ); + expect(createSkeletonPacksSpy).toBeCalledTimes(0); + }); + }); + }); + function createMockDB( mockDbOptions = MOCK_DB_OPTIONS, // source archive location must be a real(-ish) location since