Skip to content
Merged
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
110 changes: 57 additions & 53 deletions extensions/ql-vscode/src/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,67 +154,69 @@ export async function findSourceArchive(
return undefined;
}

async function resolveDatabase(
databasePath: string,
): Promise<DatabaseContents> {
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<string[]> {
return await glob("*.dbscheme", { cwd: dbDirectory });
}

async function resolveDatabaseContents(
uri: vscode.Uri,
): Promise<DatabaseContents> {
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<DatabaseContents> {
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 resolveDatabase(databasePath);
const contents = await this.resolveDatabase(databasePath);

if (contents === undefined) {
throw new InvalidDatabaseError(
`'${databasePath}' is not a valid database.`,
);
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;
}

// 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]));
public static async resolveDatabase(
databasePath: string,
): Promise<DatabaseContents> {
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,
};
}
return contents;
}

/** An item in the list of available databases */
Expand Down Expand Up @@ -370,7 +372,9 @@ export class DatabaseItemImpl implements DatabaseItem {
public async refresh(): Promise<void> {
try {
try {
this._contents = await resolveDatabaseContents(this.databaseUri);
this._contents = await DatabaseResolver.resolveDatabaseContents(
this.databaseUri,
);
this._error = undefined;
} catch (e) {
this._contents = undefined;
Expand Down Expand Up @@ -602,7 +606,7 @@ export class DatabaseManager extends DisposableObject {
uri: vscode.Uri,
displayName?: string,
): Promise<DatabaseItem> {
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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
DatabaseContents,
FullDatabaseOptions,
findSourceArchive,
DatabaseResolver,
} from "../../../src/databases";
import { Logger } from "../../../src/common";
import { ProgressCallback } from "../../../src/commandRunner";
Expand All @@ -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 = {
Expand Down Expand Up @@ -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
Expand Down