Files
vscode-codeql/extensions/ql-vscode/test/vscode-tests/minimal-workspace/local-databases.test.ts
Elena Tanasoiu 4a8ba1377d Don't offer to create skeleton pack again
When running Create Query in the codespaces-codeql repo, it successfully
creates codeql-custom-queries-xxx as a subfolder of the first workspace
folder, and then adds a database.  After the database gets added, we get
prompted with this message:

```
We've noticed you don't have a CodeQL pack available to analyze this
database. Can we set up a query pack for you?
```

which would try to create another QL pack.

Since we're no longer pushing QL packs as top level folders in the
workspace when we use the new "Create Query" flow, we also need to adapt
the original flow to take into account subfolders.

Just as a reminder, the original flow is:
- Be in the codespace template
- Download a database from GitHub
- The extension will offer to create a QL pack for the database

The new flow:
- Run the "Create Query" command
- Choose a language
- Create a QL pack
- Download a database for it

In the new flow the last step of downloading a database would trigger
the extension to offer to create a QL pack.

Let's fix this by detecting subfolders as well and exiting early.
2023-04-14 09:55:18 +00:00

820 lines
26 KiB
TypeScript

import * as tmp from "tmp";
import * as fs from "fs-extra";
import { join } from "path";
import { CancellationToken, ExtensionContext, Uri, workspace } from "vscode";
import {
DatabaseContentsWithDbScheme,
DatabaseEventKind,
DatabaseItemImpl,
DatabaseManager,
DatabaseResolver,
findSourceArchive,
FullDatabaseOptions,
} from "../../../src/local-databases";
import { Logger } from "../../../src/common";
import { ProgressCallback } from "../../../src/progress";
import { CodeQLCliServer, DbInfo } from "../../../src/cli";
import {
encodeArchiveBasePath,
encodeSourceArchiveUri,
} from "../../../src/archive-filesystem-provider";
import { testDisposeHandler } from "../test-dispose-handler";
import { QueryRunner } from "../../../src/queryRunner";
import * as helpers from "../../../src/helpers";
import { Setting } from "../../../src/config";
import { QlPackGenerator } from "../../../src/qlpack-generator";
import { mockedObject } from "../utils/mocking.helpers";
import { createMockApp } from "../../__mocks__/appMock";
import {
createMockDB,
dbLocationUri,
mockDbOptions,
sourceLocationUri,
} from "../../factories/databases/databases";
describe("local databases", () => {
let databaseManager: DatabaseManager;
let extensionContext: ExtensionContext;
let updateSpy: jest.Mock<Promise<void>, []>;
let registerSpy: jest.Mock<Promise<void>, []>;
let deregisterSpy: jest.Mock<Promise<void>, []>;
let resolveDatabaseSpy: jest.Mock<Promise<DbInfo>, []>;
let packAddSpy: jest.Mock<any, []>;
let logSpy: jest.Mock<any, []>;
let showBinaryChoiceDialogSpy: jest.SpiedFunction<
typeof helpers.showBinaryChoiceDialog
>;
let dir: tmp.DirResult;
let extensionContextStoragePath: string;
beforeEach(() => {
dir = tmp.dirSync();
updateSpy = jest.fn(() => Promise.resolve(undefined));
registerSpy = jest.fn(() => Promise.resolve(undefined));
deregisterSpy = jest.fn(() => Promise.resolve(undefined));
resolveDatabaseSpy = jest.fn(() => Promise.resolve({} as DbInfo));
packAddSpy = jest.fn();
logSpy = jest.fn(() => {
/* */
});
showBinaryChoiceDialogSpy = jest
.spyOn(helpers, "showBinaryChoiceDialog")
.mockResolvedValue(true);
extensionContextStoragePath = dir.name;
extensionContext = mockedObject<ExtensionContext>(
{
workspaceState: {
update: updateSpy,
get: () => [],
},
},
{
dynamicProperties: {
// pretend like databases added in the temp dir are controlled by the extension
// so that they are deleted upon removal
storagePath: () => extensionContextStoragePath,
storageUri: () => Uri.parse(extensionContextStoragePath),
},
},
);
databaseManager = new DatabaseManager(
extensionContext,
createMockApp({}),
mockedObject<QueryRunner>({
registerDatabase: registerSpy,
deregisterDatabase: deregisterSpy,
onStart: () => {
/**/
},
}),
mockedObject<CodeQLCliServer>({
resolveDatabase: resolveDatabaseSpy,
packAdd: packAddSpy,
}),
mockedObject<Logger>({
log: logSpy,
}),
);
// Unfortunately, during a test it is not possible to convert from
// a single root workspace to multi-root, so must stub out relevant
// functions
jest.spyOn(workspace, "updateWorkspaceFolders").mockReturnValue(true);
});
afterEach(async () => {
dir.removeCallback();
databaseManager.dispose(testDisposeHandler);
});
it("should fire events when adding and removing a db item", async () => {
const mockDbItem = createMockDB(dir);
const onDidChangeDatabaseItem = jest.fn();
databaseManager.onDidChangeDatabaseItem(onDidChangeDatabaseItem);
await (databaseManager as any).addDatabaseItem(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem,
);
expect((databaseManager as any)._databaseItems).toEqual([mockDbItem]);
expect(updateSpy).toBeCalledWith("databaseList", [
{
options: mockDbOptions(),
uri: dbLocationUri(dir).toString(true),
},
]);
expect(onDidChangeDatabaseItem).toBeCalledWith({
item: undefined,
kind: DatabaseEventKind.Add,
});
updateSpy.mockClear();
onDidChangeDatabaseItem.mockClear();
// now remove the item
await databaseManager.removeDatabaseItem(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem,
);
expect((databaseManager as any)._databaseItems).toEqual([]);
expect(updateSpy).toBeCalledWith("databaseList", []);
expect(onDidChangeDatabaseItem).toBeCalledWith({
item: undefined,
kind: DatabaseEventKind.Remove,
});
});
describe("renameDatabaseItem", () => {
it("should rename a db item and emit an event", async () => {
const mockDbItem = createMockDB(dir);
const onDidChangeDatabaseItem = jest.fn();
databaseManager.onDidChangeDatabaseItem(onDidChangeDatabaseItem);
await (databaseManager as any).addDatabaseItem(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem,
);
await databaseManager.renameDatabaseItem(mockDbItem, "new name");
expect(mockDbItem.name).toBe("new name");
expect(updateSpy).toBeCalledWith("databaseList", [
{
options: { ...mockDbOptions(), displayName: "new name" },
uri: dbLocationUri(dir).toString(true),
},
]);
expect(onDidChangeDatabaseItem).toBeCalledWith({
item: undefined,
kind: DatabaseEventKind.Rename,
});
});
});
describe("add / remove database items", () => {
it("should add a database item", async () => {
const onDidChangeDatabaseItem = jest.fn();
databaseManager.onDidChangeDatabaseItem(onDidChangeDatabaseItem);
const mockDbItem = createMockDB(dir);
await (databaseManager as any).addDatabaseItem(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem,
);
expect(databaseManager.databaseItems).toEqual([mockDbItem]);
expect(updateSpy).toBeCalledWith("databaseList", [
{
uri: dbLocationUri(dir).toString(true),
options: mockDbOptions(),
},
]);
const mockEvent = {
item: undefined,
kind: DatabaseEventKind.Add,
};
expect(onDidChangeDatabaseItem).toBeCalledWith(mockEvent);
});
it("should add a database item source archive", async () => {
const mockDbItem = createMockDB(dir);
mockDbItem.name = "xxx";
await databaseManager.addDatabaseSourceArchiveFolder(mockDbItem);
// workspace folders should be updated. We can only check the mocks since
// when running as a test, we are not allowed to update the workspace folders
expect(workspace.updateWorkspaceFolders).toHaveBeenCalledWith(1, 0, {
name: "[xxx source archive]",
// must use a matcher here since vscode URIs with the same path
// are not always equal due to internal state.
uri: expect.objectContaining({
fsPath: encodeArchiveBasePath(sourceLocationUri(dir).fsPath).fsPath,
}),
});
});
it("should remove a database item", async () => {
const mockDbItem = createMockDB(dir);
await fs.ensureDir(mockDbItem.databaseUri.fsPath);
// pretend that this item is the first workspace folder in the list
jest
.spyOn(mockDbItem, "belongsToSourceArchiveExplorerUri")
.mockReturnValue(true);
await (databaseManager as any).addDatabaseItem(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem,
);
updateSpy.mockClear();
await databaseManager.removeDatabaseItem(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem,
);
expect(databaseManager.databaseItems).toEqual([]);
expect(updateSpy).toBeCalledWith("databaseList", []);
// should remove the folder
expect(workspace.updateWorkspaceFolders).toBeCalledWith(0, 1);
// should also delete the db contents
await expect(fs.pathExists(mockDbItem.databaseUri.fsPath)).resolves.toBe(
false,
);
});
it("should remove a database item outside of the extension controlled area", async () => {
const mockDbItem = createMockDB(dir);
await fs.ensureDir(mockDbItem.databaseUri.fsPath);
// pretend that this item is the first workspace folder in the list
jest
.spyOn(mockDbItem, "belongsToSourceArchiveExplorerUri")
.mockReturnValue(true);
await (databaseManager as any).addDatabaseItem(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem,
);
updateSpy.mockClear();
// pretend that the database location is not controlled by the extension
(databaseManager as any).ctx.storagePath = "hucairz";
extensionContextStoragePath = "hucairz";
await databaseManager.removeDatabaseItem(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem,
);
expect(databaseManager.databaseItems).toEqual([]);
expect(updateSpy).toBeCalledWith("databaseList", []);
// should remove the folder
expect(workspace.updateWorkspaceFolders).toBeCalledWith(0, 1);
// should NOT delete the db contents
await expect(fs.pathExists(mockDbItem.databaseUri.fsPath)).resolves.toBe(
true,
);
});
it("should register and deregister a database when adding and removing it", async () => {
// similar test as above, but also check the call to sendRequestSpy to make sure they send the
// registration messages.
const mockDbItem = createMockDB(dir);
await (databaseManager as any).addDatabaseItem(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem,
);
// Should have registered this database
expect(registerSpy).toBeCalledWith({}, {}, mockDbItem);
await databaseManager.removeDatabaseItem(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem,
);
// Should have deregistered this database
expect(deregisterSpy).toBeCalledWith({}, {}, mockDbItem);
});
});
describe("resolveSourceFile", () => {
it("should fail to resolve when not a uri", () => {
const db = createMockDB(
dir,
mockDbOptions(),
Uri.parse("file:/sourceArchive-uri/"),
);
(db as any)._contents.sourceArchiveUri = undefined;
expect(() => db.resolveSourceFile("abc")).toThrowError(
"Scheme is missing",
);
});
it("should fail to resolve when not a file uri", () => {
const db = createMockDB(
dir,
mockDbOptions(),
Uri.parse("file:/sourceArchive-uri/"),
);
(db as any)._contents.sourceArchiveUri = undefined;
expect(() => db.resolveSourceFile("http://abc")).toThrowError(
"Invalid uri scheme",
);
});
describe("no source archive", () => {
it("should resolve undefined", () => {
const db = createMockDB(
dir,
mockDbOptions(),
Uri.parse("file:/sourceArchive-uri/"),
);
(db as any)._contents.sourceArchiveUri = undefined;
const resolved = db.resolveSourceFile(undefined);
expect(resolved.toString(true)).toBe(dbLocationUri(dir).toString(true));
});
it("should resolve an empty file", () => {
const db = createMockDB(
dir,
mockDbOptions(),
Uri.parse("file:/sourceArchive-uri/"),
);
(db as any)._contents.sourceArchiveUri = undefined;
const resolved = db.resolveSourceFile("file:");
expect(resolved.toString()).toBe("file:///");
});
});
describe("zipped source archive", () => {
it("should encode a source archive url", () => {
const db = createMockDB(
dir,
mockDbOptions(),
encodeSourceArchiveUri({
sourceArchiveZipPath: "sourceArchive-uri",
pathWithinSourceArchive: "def",
}),
);
const resolved = db.resolveSourceFile(Uri.file("abc").toString());
// must recreate an encoded archive uri instead of typing out the
// text since the uris will be different on windows and ubuntu.
expect(resolved.toString()).toBe(
encodeSourceArchiveUri({
sourceArchiveZipPath: "sourceArchive-uri",
pathWithinSourceArchive: "def/abc",
}).toString(),
);
});
it("should encode a source archive url with trailing slash", () => {
const db = createMockDB(
dir,
mockDbOptions(),
encodeSourceArchiveUri({
sourceArchiveZipPath: "sourceArchive-uri",
pathWithinSourceArchive: "def/",
}),
);
const resolved = db.resolveSourceFile(Uri.file("abc").toString());
// must recreate an encoded archive uri instead of typing out the
// text since the uris will be different on windows and ubuntu.
expect(resolved.toString()).toBe(
encodeSourceArchiveUri({
sourceArchiveZipPath: "sourceArchive-uri",
pathWithinSourceArchive: "def/abc",
}).toString(),
);
});
it("should encode an empty source archive url", () => {
const db = createMockDB(
dir,
mockDbOptions(),
encodeSourceArchiveUri({
sourceArchiveZipPath: "sourceArchive-uri",
pathWithinSourceArchive: "def",
}),
);
const resolved = db.resolveSourceFile("file:");
expect(resolved.toString()).toBe(
"codeql-zip-archive://1-18/sourceArchive-uri/def/",
);
});
});
it("should handle an empty file", () => {
const db = createMockDB(
dir,
mockDbOptions(),
Uri.parse("file:/sourceArchive-uri/"),
);
const resolved = db.resolveSourceFile("");
expect(resolved.toString()).toBe("file:///sourceArchive-uri/");
});
});
describe("getPrimaryLanguage", () => {
it("should get the primary language", async () => {
resolveDatabaseSpy.mockResolvedValue({
languages: ["python"],
} as unknown as DbInfo);
const result = await (databaseManager as any).getPrimaryLanguage(
"hucairz",
);
expect(result).toBe("python");
});
it("should handle missing the primary language", async () => {
resolveDatabaseSpy.mockResolvedValue({
languages: [],
} as unknown as DbInfo);
const result = await (databaseManager as any).getPrimaryLanguage(
"hucairz",
);
expect(result).toBe("");
});
});
describe("isAffectedByTest", () => {
let directoryPath: string;
let projectPath: string;
let qlFilePath: string;
beforeEach(async () => {
directoryPath = join(dir.name, "dir");
await fs.ensureDir(directoryPath);
projectPath = join(directoryPath, "dir.testproj");
await fs.writeFile(projectPath, "");
qlFilePath = join(directoryPath, "test.ql");
await fs.writeFile(qlFilePath, "");
});
it("should return true for testproj database in test directory", async () => {
const db = createMockDB(
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(projectPath),
);
expect(await db.isAffectedByTest(directoryPath)).toBe(true);
});
it("should return false for non-existent test directory", async () => {
const db = createMockDB(
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(join(dir.name, "non-existent/non-existent.testproj")),
);
expect(await db.isAffectedByTest(join(dir.name, "non-existent"))).toBe(
false,
);
});
it("should return false for non-testproj database in test directory", async () => {
const anotherProjectPath = join(directoryPath, "dir.proj");
await fs.writeFile(anotherProjectPath, "");
const db = createMockDB(
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(anotherProjectPath),
);
expect(await db.isAffectedByTest(directoryPath)).toBe(false);
});
it("should return false for testproj database outside test directory", async () => {
const anotherProjectDir = join(dir.name, "other");
await fs.ensureDir(anotherProjectDir);
const anotherProjectPath = join(anotherProjectDir, "other.testproj");
await fs.writeFile(anotherProjectPath, "");
const db = createMockDB(
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(anotherProjectPath),
);
expect(await db.isAffectedByTest(directoryPath)).toBe(false);
});
it("should return false for testproj database for prefix directory", async () => {
const db = createMockDB(
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(projectPath),
);
// /d is a prefix of /dir/dir.testproj, but
// /dir/dir.testproj is not under /d
expect(await db.isAffectedByTest(join(directoryPath, "d"))).toBe(false);
});
it("should return true for testproj database for test file", async () => {
const db = createMockDB(
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(projectPath),
);
expect(await db.isAffectedByTest(qlFilePath)).toBe(true);
});
it("should return false for non-existent test file", async () => {
const otherTestFile = join(directoryPath, "other-test.ql");
const db = createMockDB(
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(projectPath),
);
expect(await db.isAffectedByTest(otherTestFile)).toBe(false);
});
it("should return false for non-testproj database for test file", async () => {
const anotherProjectPath = join(directoryPath, "dir.proj");
await fs.writeFile(anotherProjectPath, "");
const db = createMockDB(
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(anotherProjectPath),
);
expect(await db.isAffectedByTest(qlFilePath)).toBe(false);
});
it("should return false for testproj database not matching test file", async () => {
const otherTestFile = join(dir.name, "test.ql");
await fs.writeFile(otherTestFile, "");
const db = createMockDB(
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(projectPath),
);
expect(await db.isAffectedByTest(otherTestFile)).toBe(false);
});
});
describe("findSourceArchive", () => {
["src", "output/src_archive"].forEach((name) => {
it(`should find source folder in ${name}`, async () => {
const uri = Uri.file(join(dir.name, name));
fs.createFileSync(join(uri.fsPath, "hucairz.txt"));
const srcUri = await findSourceArchive(dir.name);
expect(srcUri!.fsPath).toBe(uri.fsPath);
});
it(`should find source archive in ${name}.zip`, async () => {
const uri = Uri.file(join(dir.name, `${name}.zip`));
fs.createFileSync(uri.fsPath);
const srcUri = await findSourceArchive(dir.name);
expect(srcUri!.fsPath).toBe(uri.fsPath);
});
it(`should prioritize ${name}.zip over ${name}`, async () => {
const uri = Uri.file(join(dir.name, `${name}.zip`));
fs.createFileSync(uri.fsPath);
const uriFolder = Uri.file(join(dir.name, name));
fs.createFileSync(join(uriFolder.fsPath, "hucairz.txt"));
const srcUri = await findSourceArchive(dir.name);
expect(srcUri!.fsPath).toBe(uri.fsPath);
});
});
it("should prioritize src over output/src_archive", async () => {
const uriSrc = Uri.file(join(dir.name, "src.zip"));
fs.createFileSync(uriSrc.fsPath);
const uriSrcArchive = Uri.file(join(dir.name, "src.zip"));
fs.createFileSync(uriSrcArchive.fsPath);
const resultUri = await findSourceArchive(dir.name);
expect(resultUri!.fsPath).toBe(uriSrc.fsPath);
});
});
describe("createSkeletonPacks", () => {
let mockDbItem: DatabaseItemImpl;
let language: string;
let generateSpy: jest.SpyInstance;
beforeEach(() => {
language = "ruby";
const options: FullDatabaseOptions = {
dateAdded: 123,
ignoreSourceArchive: false,
language,
};
mockDbItem = createMockDB(dir, options);
generateSpy = jest
.spyOn(QlPackGenerator.prototype, "generate")
.mockImplementation(() => Promise.resolve());
});
describe("when the language is set", () => {
it("should offer the user to set up a skeleton QL pack", async () => {
await (databaseManager as any).createSkeletonPacks(mockDbItem);
expect(showBinaryChoiceDialogSpy).toBeCalledTimes(1);
});
it("should return early if the user refuses help", async () => {
showBinaryChoiceDialogSpy = jest
.spyOn(helpers, "showBinaryChoiceDialog")
.mockResolvedValue(false);
await (databaseManager as any).createSkeletonPacks(mockDbItem);
expect(generateSpy).not.toBeCalled();
});
it("should create the skeleton QL pack for the user", async () => {
await (databaseManager as any).createSkeletonPacks(mockDbItem);
expect(generateSpy).toBeCalled();
});
});
describe("when the language is not set", () => {
it("should fail gracefully", async () => {
mockDbItem = createMockDB(dir);
await (databaseManager as any).createSkeletonPacks(mockDbItem);
expect(logSpy).toHaveBeenCalledWith(
"Could not create skeleton QL pack because the selected database's language is not set.",
);
});
});
describe("when the databaseItem is not set", () => {
it("should fail gracefully", async () => {
await (databaseManager as any).createSkeletonPacks(undefined);
expect(logSpy).toHaveBeenCalledWith(
"Could not create QL pack because no database is selected. Please add a database.",
);
});
});
describe("when the QL pack already exists", () => {
beforeEach(() => {
fs.mkdirSync(join(dir.name, `codeql-custom-queries-${language}`));
});
it("should exit early", async () => {
showBinaryChoiceDialogSpy = jest
.spyOn(helpers, "showBinaryChoiceDialog")
.mockResolvedValue(false);
await (databaseManager as any).createSkeletonPacks(mockDbItem);
expect(generateSpy).not.toBeCalled();
});
});
});
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 DatabaseContentsWithDbScheme);
addDatabaseSourceArchiveFolderSpy = jest.spyOn(
databaseManager,
"addDatabaseSourceArchiveFolder",
);
jest.mock("fs", () => ({
promises: {
pathExists: jest.fn().mockResolvedValue(true),
},
}));
mockDbItem = createMockDB(dir);
});
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", () => {
describe("when we add the tutorial database to the codespace", () => {
it("should not offer to create a skeleton QL pack", async () => {
jest.spyOn(Setting.prototype, "getValue").mockReturnValue(true);
const isTutorialDatabase = true;
await databaseManager.openDatabase(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem.databaseUri,
"CodeQL Tutorial Database",
isTutorialDatabase,
);
expect(createSkeletonPacksSpy).toBeCalledTimes(0);
});
});
describe("when we add a new database that isn't the tutorial one", () => {
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);
});
});
});
});