Files
vscode-codeql/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/extension-pack-picker.test.ts
Koen Vlaswinkel 5fbd912abd Add codeQL.model.packLocation setting
This adds the `codeQL.model.packLocation` setting, which allows users to
specify the location of the CodeQL extension pack. This setting replaces
the `codeQL.model.extensionsDirectory` setting, which has been removed.

The pack location supports variable substitutions and supports both
absolute and relative paths. The default value is
`.github/codeql/extensions/${name}-${language}` which matches the
previous defaults.
2024-04-05 15:04:29 +02:00

680 lines
17 KiB
TypeScript

import type { WorkspaceFolder } from "vscode";
import { CancellationTokenSource, Uri, workspace } from "vscode";
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
import { outputFile, readFile } from "fs-extra";
import { join } from "path";
import { dir } from "tmp-promise";
import type { QlpacksInfo } from "../../../../src/codeql-cli/cli";
import { pickExtensionPack } from "../../../../src/model-editor/extension-pack-picker";
import type { ExtensionPack } from "../../../../src/model-editor/shared/extension-pack";
import { createMockLogger } from "../../../__mocks__/loggerMock";
import type { ModelConfig } from "../../../../src/config";
import { mockedObject } from "../../utils/mocking.helpers";
import type { DatabaseItem } from "../../../../src/databases/local-databases";
describe("pickExtensionPack", () => {
let tmpDir: string;
const autoExtensionPackName = "github/vscode-codeql-java";
let autoExtensionPackPath: string;
let autoExtensionPack: ExtensionPack;
let qlPacks: QlpacksInfo;
const databaseItem: Pick<DatabaseItem, "name" | "language" | "origin"> = {
name: "github/vscode-codeql",
language: "java",
origin: {
type: "github",
repository: "github/vscode-codeql",
databaseId: 123578,
databaseCreatedAt: "2021-01-01T00:00:00Z",
commitOid: "1234567890abcdef",
},
};
const progress = jest.fn();
const token = new CancellationTokenSource().token;
let workspaceFoldersSpy: jest.SpyInstance;
let additionalPacks: string[];
let workspaceFolder: WorkspaceFolder;
let modelConfig: ModelConfig;
const logger = createMockLogger();
const maxStep = 4;
beforeEach(async () => {
tmpDir = (
await dir({
unsafeCleanup: true,
})
).path;
// Uri.file(...).fsPath normalizes the filenames so we can properly compare them on Windows
autoExtensionPackPath = Uri.file(join(tmpDir, "vscode-codeql-java")).fsPath;
qlPacks = {
"github/vscode-codeql-java": [autoExtensionPackPath],
};
autoExtensionPack = await createMockExtensionPack(
autoExtensionPackPath,
autoExtensionPackName,
);
workspaceFolder = {
uri: Uri.file(tmpDir),
name: "codeql-custom-queries-java",
index: 0,
};
additionalPacks = [
Uri.file(tmpDir).fsPath,
`${Uri.file(tmpDir).fsPath}/.github`,
];
workspaceFoldersSpy = jest
.spyOn(workspace, "workspaceFolders", "get")
.mockReturnValue([workspaceFolder]);
modelConfig = mockedObject<ModelConfig>({
getPackLocation: jest
.fn()
.mockImplementation(
(language, { name }) =>
`.github/codeql/extensions/${name}-${language}`,
),
});
});
it("selects an existing extension pack", async () => {
const cliServer = mockCliServer(qlPacks);
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
token,
maxStep,
),
).toEqual(autoExtensionPack);
expect(cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith(
additionalPacks,
true,
);
expect(modelConfig.getPackLocation).toHaveBeenCalledWith("java", {
database: "github/vscode-codeql",
language: "java",
name: "vscode-codeql",
owner: "github",
});
});
it("creates a new extension pack using default pack location", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
workspaceFoldersSpy.mockReturnValue([
{
uri: Uri.file("/b/a/c"),
name: "my-workspace",
index: 0,
},
{
uri: Uri.file("/a/b/c"),
name: "codeql-custom-queries-csharp",
index: 1,
},
{
uri: Uri.joinPath(Uri.file(tmpDir.path), "codeql-custom-queries-java"),
name: "codeql-custom-queries-java",
index: 2,
},
]);
jest
.spyOn(workspace, "workspaceFile", "get")
.mockReturnValue(
Uri.joinPath(Uri.file(tmpDir.path), "workspace.code-workspace"),
);
jest.spyOn(workspace, "updateWorkspaceFolders").mockReturnValue(true);
const newPackDir = join(
Uri.file(tmpDir.path).fsPath,
".github",
"codeql",
"extensions",
"vscode-codeql-java",
);
const cliServer = mockCliServer({});
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
token,
maxStep,
),
).toEqual({
path: newPackDir,
yamlPath: join(newPackDir, "codeql-pack.yml"),
name: autoExtensionPackName,
version: "0.0.0",
language: "java",
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(modelConfig.getPackLocation).toHaveBeenCalledWith("java", {
database: "github/vscode-codeql",
language: "java",
name: "vscode-codeql",
owner: "github",
});
expect(
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
).toEqual({
name: autoExtensionPackName,
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
});
it("creates a new extension pack when absolute custom pack location is set in config", async () => {
const packLocation = join(Uri.file(tmpDir).fsPath, "java/ql/lib");
const modelConfig = mockedObject<ModelConfig>({
getPackLocation: jest.fn().mockReturnValue(packLocation),
});
const cliServer = mockCliServer({});
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
token,
maxStep,
),
).toEqual({
path: packLocation,
yamlPath: join(packLocation, "codeql-pack.yml"),
name: autoExtensionPackName,
version: "0.0.0",
language: "java",
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(modelConfig.getPackLocation).toHaveBeenCalledWith("java", {
database: "github/vscode-codeql",
language: "java",
name: "vscode-codeql",
owner: "github",
});
expect(
loadYaml(await readFile(join(packLocation, "codeql-pack.yml"), "utf8")),
).toEqual({
name: autoExtensionPackName,
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
});
it("creates a new extension pack when relative custom pack location is set in config", async () => {
const packLocation = join(Uri.file(tmpDir).fsPath, "java/ql/lib");
const modelConfig = mockedObject<ModelConfig>({
getPackLocation: jest
.fn()
.mockImplementation((language) => `${language}/ql/lib`),
});
const cliServer = mockCliServer({});
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
token,
maxStep,
),
).toEqual({
path: packLocation,
yamlPath: join(packLocation, "codeql-pack.yml"),
name: autoExtensionPackName,
version: "0.0.0",
language: "java",
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(modelConfig.getPackLocation).toHaveBeenCalledWith("java", {
database: "github/vscode-codeql",
language: "java",
name: "vscode-codeql",
owner: "github",
});
expect(
loadYaml(await readFile(join(packLocation, "codeql-pack.yml"), "utf8")),
).toEqual({
name: autoExtensionPackName,
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
});
it("creates a new extension pack with non-github origin database", async () => {
const databaseItem: Pick<DatabaseItem, "name" | "language" | "origin"> = {
name: "vscode-codeql",
language: "java",
origin: {
type: "archive",
path: "/path/to/codeql-database.zip",
},
};
const tmpDir = await dir({
unsafeCleanup: true,
});
workspaceFoldersSpy.mockReturnValue([
{
uri: Uri.joinPath(Uri.file(tmpDir.path), "codeql-custom-queries-java"),
name: "codeql-custom-queries-java",
index: 2,
},
]);
jest
.spyOn(workspace, "workspaceFile", "get")
.mockReturnValue(
Uri.joinPath(Uri.file(tmpDir.path), "workspace.code-workspace"),
);
jest.spyOn(workspace, "updateWorkspaceFolders").mockReturnValue(true);
const newPackDir = join(
Uri.file(tmpDir.path).fsPath,
".github",
"codeql",
"extensions",
"vscode-codeql-java",
);
const cliServer = mockCliServer({});
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
token,
maxStep,
),
).toEqual({
path: newPackDir,
yamlPath: join(newPackDir, "codeql-pack.yml"),
name: "pack/vscode-codeql-java",
version: "0.0.0",
language: "java",
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(modelConfig.getPackLocation).toHaveBeenCalledWith("java", {
database: "vscode-codeql",
language: "java",
name: "vscode-codeql",
owner: "",
});
expect(
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
).toEqual({
name: "pack/vscode-codeql-java",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
});
it("shows an error when an extension pack resolves to more than 1 location", async () => {
const cliServer = mockCliServer({
"github/vscode-codeql-java": [
"/a/b/c/my-extension-pack",
"/a/b/c/my-extension-pack2",
],
});
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
token,
maxStep,
),
).toEqual(undefined);
expect(logger.showErrorMessage).toHaveBeenCalledTimes(1);
expect(logger.showErrorMessage).toHaveBeenCalledWith(
expect.stringMatching(/resolves to multiple paths/),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
});
it("shows an error when there is no pack YAML file", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer({
"github/vscode-codeql-java": [tmpDir.path],
});
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
token,
maxStep,
),
).toEqual(undefined);
expect(logger.showErrorMessage).toHaveBeenCalledTimes(1);
expect(logger.showErrorMessage).toHaveBeenCalledWith(
"Could not read extension pack github/vscode-codeql-java",
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
});
it("shows an error when the pack YAML file is invalid", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer({
"github/vscode-codeql-java": [tmpDir.path],
});
await outputFile(join(tmpDir.path, "codeql-pack.yml"), dumpYaml("java"));
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
token,
maxStep,
),
).toEqual(undefined);
expect(logger.showErrorMessage).toHaveBeenCalledTimes(1);
expect(logger.showErrorMessage).toHaveBeenCalledWith(
"Could not read extension pack github/vscode-codeql-java",
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
});
it("shows an error when the pack YAML does not contain name", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer({
"github/vscode-codeql-java": [tmpDir.path],
});
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
}),
);
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
token,
maxStep,
),
).toEqual(undefined);
expect(logger.showErrorMessage).toHaveBeenCalledTimes(1);
expect(logger.showErrorMessage).toHaveBeenCalledWith(
"Could not read extension pack github/vscode-codeql-java",
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
});
it("shows an error when the pack YAML does not contain dataExtensions", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer({
"github/vscode-codeql-java": [tmpDir.path],
});
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: autoExtensionPackName,
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
}),
);
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
token,
maxStep,
),
).toEqual(undefined);
expect(logger.showErrorMessage).toHaveBeenCalledTimes(1);
expect(logger.showErrorMessage).toHaveBeenCalledWith(
"Could not read extension pack github/vscode-codeql-java",
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
});
it("shows an error when the pack YAML dataExtensions is invalid", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer({
"github/vscode-codeql-java": [tmpDir.path],
});
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: autoExtensionPackName,
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: {
"codeql/java-all": "invalid",
},
}),
);
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
token,
maxStep,
),
).toEqual(undefined);
expect(logger.showErrorMessage).toHaveBeenCalledTimes(1);
expect(logger.showErrorMessage).toHaveBeenCalledWith(
"Could not read extension pack github/vscode-codeql-java",
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
});
it("allows the dataExtensions to be a string", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer({
"github/vscode-codeql-java": [tmpDir.path],
});
const qlpackPath = join(tmpDir.path, "codeql-pack.yml");
await outputFile(
qlpackPath,
dumpYaml({
name: autoExtensionPackName,
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: "models/**/*.yml",
}),
);
await outputFile(
join(tmpDir.path, "models", "model.yml"),
dumpYaml({
extensions: [],
}),
);
const extensionPack = {
path: tmpDir.path,
yamlPath: qlpackPath,
name: autoExtensionPackName,
version: "0.0.0",
language: "java",
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
};
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
token,
maxStep,
),
).toEqual(extensionPack);
});
});
function mockCliServer(qlpacks: QlpacksInfo) {
return {
resolveQlpacks: jest.fn().mockResolvedValue(qlpacks),
};
}
async function createMockExtensionPack(
path: string,
name: string,
data: Partial<ExtensionPack> = {},
): Promise<ExtensionPack> {
const extensionPack: ExtensionPack = {
path,
yamlPath: join(path, "codeql-pack.yml"),
name,
version: "0.0.0",
language: "java",
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
...data,
};
await writeExtensionPackToDisk(extensionPack);
return extensionPack;
}
async function writeExtensionPackToDisk(
extensionPack: ExtensionPack,
): Promise<void> {
await outputFile(
extensionPack.yamlPath,
dumpYaml({
name: extensionPack.name,
version: extensionPack.version,
library: true,
extensionTargets: extensionPack.extensionTargets,
dataExtensions: extensionPack.dataExtensions,
}),
);
}