Files
vscode-codeql/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/extension-pack-picker.test.ts
Koen Vlaswinkel 951bd13881 Use ModelConfig for all model settings
This switches all places where we're retrieving some model configuration
to use the `ModelConfig` or `ModelConfigListener` types. This makes it
much easier to mock these settings in tests.

This also adds a listener to the `ModelEditorView` to send the new view
state when any of the settings is changed. This should make it easier
to test settings changes in the model editor without having to re-open
the model editor.
2023-10-06 14:29:07 +02:00

515 lines
13 KiB
TypeScript

import { Uri, workspace, WorkspaceFolder } 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 { QlpacksInfo } from "../../../../src/codeql-cli/cli";
import { pickExtensionPack } from "../../../../src/model-editor/extension-pack-picker";
import { ExtensionPack } from "../../../../src/model-editor/shared/extension-pack";
import { createMockLogger } from "../../../__mocks__/loggerMock";
import { ModelConfig } from "../../../../src/config";
import { mockedObject } from "../../utils/mocking.helpers";
describe("pickExtensionPack", () => {
let tmpDir: string;
const autoExtensionPackName = "github/vscode-codeql-java";
let autoExtensionPackPath: string;
let autoExtensionPack: ExtensionPack;
let qlPacks: QlpacksInfo;
const databaseItem = {
name: "github/vscode-codeql",
language: "java",
};
const progress = jest.fn();
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>({
getExtensionsDirectory: jest.fn().mockReturnValue(undefined),
});
});
it("selects an existing extension pack", async () => {
const cliServer = mockCliServer(qlPacks);
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
maxStep,
),
).toEqual(autoExtensionPack);
expect(cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith(
additionalPacks,
true,
);
expect(modelConfig.getExtensionsDirectory).toHaveBeenCalledWith("java");
});
it("creates a new extension pack using default extensions directory", 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,
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.getExtensionsDirectory).toHaveBeenCalledWith("java");
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 extensions directory is set in config", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const configExtensionsDir = join(
Uri.file(tmpDir.path).fsPath,
"my-custom-extensions-directory",
);
const modelConfig = mockedObject<ModelConfig>({
getExtensionsDirectory: jest.fn().mockReturnValue(configExtensionsDir),
});
const newPackDir = join(configExtensionsDir, "vscode-codeql-java");
const cliServer = mockCliServer({});
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
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.getExtensionsDirectory).toHaveBeenCalledWith("java");
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("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,
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,
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,
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,
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,
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,
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,
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,
}),
);
}