Merge pull request #2296 from github/koesie10/create-extension-model-file

Allow creating new model file in existing data extension
This commit is contained in:
Koen Vlaswinkel
2023-04-14 11:09:20 +02:00
committed by GitHub
6 changed files with 2093 additions and 257 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1461,6 +1461,7 @@
"fs-extra": "^11.1.1",
"immutable": "^4.0.0",
"js-yaml": "^4.1.0",
"minimatch": "^9.0.0",
"minimist": "~1.2.6",
"msw": "^1.2.0",
"nanoid": "^3.2.0",

View File

@@ -53,7 +53,7 @@ export class DataExtensionsEditorModule {
return {
"codeQL.openDataExtensionsEditor": async () =>
withProgress(
async (progress) => {
async (progress, token) => {
const db = this.databaseManager.currentDatabaseItem;
if (!db) {
void showAndLogErrorMessage("No database selected");
@@ -69,7 +69,9 @@ export class DataExtensionsEditorModule {
const modelFile = await pickExtensionPackModelFile(
this.cliServer,
db,
progress,
token,
);
if (!modelFile) {
return;

View File

@@ -18,7 +18,7 @@ import {
showAndLogExceptionWithTelemetry,
} from "../helpers";
import { extLogger } from "../common";
import { readFile, writeFile } from "fs-extra";
import { outputFile, readFile } from "fs-extra";
import { load as loadYaml } from "js-yaml";
import { DatabaseItem, DatabaseManager } from "../local-databases";
import { CodeQLCliServer } from "../cli";
@@ -150,7 +150,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
): Promise<void> {
const yaml = createDataExtensionYaml(externalApiUsages, modeledMethods);
await writeFile(this.modelFilename, yaml);
await outputFile(this.modelFilename, yaml);
void extLogger.log(`Saved data extension YAML to ${this.modelFilename}`);
}

View File

@@ -1,21 +1,34 @@
import { relative, sep } from "path";
import { window } from "vscode";
import { relative, resolve, sep } from "path";
import { pathExists, readFile } from "fs-extra";
import { load as loadYaml } from "js-yaml";
import { minimatch } from "minimatch";
import { CancellationToken, window } from "vscode";
import { CodeQLCliServer } from "../cli";
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers";
import { ProgressCallback } from "../progress";
import { DatabaseItem } from "../local-databases";
import { getQlPackPath, QLPACK_FILENAMES } from "../pure/ql";
const maxStep = 3;
export async function pickExtensionPackModelFile(
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveExtensions">,
databaseItem: Pick<DatabaseItem, "name">,
progress: ProgressCallback,
token: CancellationToken,
): Promise<string | undefined> {
const extensionPackPath = await pickExtensionPack(cliServer, progress);
const extensionPackPath = await pickExtensionPack(cliServer, progress, token);
if (!extensionPackPath) {
return;
}
const modelFile = await pickModelFile(cliServer, progress, extensionPackPath);
const modelFile = await pickModelFile(
cliServer,
databaseItem,
extensionPackPath,
progress,
token,
);
if (!modelFile) {
return;
}
@@ -26,6 +39,7 @@ export async function pickExtensionPackModelFile(
async function pickExtensionPack(
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
progress: ProgressCallback,
token: CancellationToken,
): Promise<string | undefined> {
progress({
message: "Resolving extension packs...",
@@ -47,9 +61,13 @@ async function pickExtensionPack(
maxStep,
});
const extensionPackOption = await window.showQuickPick(options, {
title: "Select extension pack to use",
});
const extensionPackOption = await window.showQuickPick(
options,
{
title: "Select extension pack to use",
},
token,
);
if (!extensionPackOption) {
return undefined;
}
@@ -74,8 +92,10 @@ async function pickExtensionPack(
async function pickModelFile(
cliServer: Pick<CodeQLCliServer, "resolveExtensions">,
progress: ProgressCallback,
databaseItem: Pick<DatabaseItem, "name">,
extensionPackPath: string,
progress: ProgressCallback,
token: CancellationToken,
): Promise<string | undefined> {
// Find the existing model files in the extension pack
const additionalPacks = getOnDiskWorkspaceFolders();
@@ -92,13 +112,21 @@ async function pickModelFile(
}
}
const fileOptions: Array<{ label: string; file: string }> = [];
if (modelFiles.size === 0) {
return pickNewModelFile(databaseItem, extensionPackPath, token);
}
const fileOptions: Array<{ label: string; file: string | null }> = [];
for (const file of modelFiles) {
fileOptions.push({
label: relative(extensionPackPath, file).replaceAll(sep, "/"),
file,
});
}
fileOptions.push({
label: "Create new model file",
file: null,
});
progress({
message: "Choosing model file...",
@@ -106,13 +134,103 @@ async function pickModelFile(
maxStep,
});
const fileOption = await window.showQuickPick(fileOptions, {
title: "Select model file to use",
});
const fileOption = await window.showQuickPick(
fileOptions,
{
title: "Select model file to use",
},
token,
);
if (!fileOption) {
return;
return undefined;
}
return fileOption.file;
if (fileOption.file) {
return fileOption.file;
}
return pickNewModelFile(databaseItem, extensionPackPath, token);
}
async function pickNewModelFile(
databaseItem: Pick<DatabaseItem, "name">,
extensionPackPath: string,
token: CancellationToken,
) {
const qlpackPath = await getQlPackPath(extensionPackPath);
if (!qlpackPath) {
void showAndLogErrorMessage(
`Could not find any of ${QLPACK_FILENAMES.join(
", ",
)} in ${extensionPackPath}`,
);
return undefined;
}
const qlpack = await loadYaml(await readFile(qlpackPath, "utf8"), {
filename: qlpackPath,
});
if (typeof qlpack !== "object" || qlpack === null) {
void showAndLogErrorMessage(`Could not parse ${qlpackPath}`);
return undefined;
}
const dataExtensionPatternsValue = qlpack.dataExtensions;
if (
!(
Array.isArray(dataExtensionPatternsValue) ||
typeof dataExtensionPatternsValue === "string"
)
) {
void showAndLogErrorMessage(
`Expected 'dataExtensions' to be a string or an array in ${qlpackPath}`,
);
return undefined;
}
// The YAML allows either a string or an array of strings
const dataExtensionPatterns = Array.isArray(dataExtensionPatternsValue)
? dataExtensionPatternsValue
: [dataExtensionPatternsValue];
const filename = await window.showInputBox(
{
title: "Enter the name of the new model file",
value: `models/${databaseItem.name.replaceAll("/", ".")}.model.yml`,
validateInput: async (value: string): Promise<string | undefined> => {
if (value === "") {
return "File name must not be empty";
}
const path = resolve(extensionPackPath, value);
if (await pathExists(path)) {
return "File already exists";
}
const notInExtensionPack = relative(extensionPackPath, path).startsWith(
"..",
);
if (notInExtensionPack) {
return "File must be in the extension pack";
}
const matchesPattern = dataExtensionPatterns.some((pattern) =>
minimatch(value, pattern, { matchBase: true }),
);
if (!matchesPattern) {
return `File must match one of the patterns in 'dataExtensions' in ${qlpackPath}`;
}
return undefined;
},
},
token,
);
if (!filename) {
return undefined;
}
return resolve(extensionPackPath, filename);
}

View File

@@ -1,4 +1,8 @@
import { QuickPickItem, window } from "vscode";
import { CancellationTokenSource, QuickPickItem, window } from "vscode";
import { dump as dumpYaml } from "js-yaml";
import { outputFile } from "fs-extra";
import { join } from "path";
import { dir } from "tmp-promise";
import { pickExtensionPackModelFile } from "../../../../src/data-extensions-editor/extension-pack-picker";
import { QlpacksInfo, ResolveExtensionsResult } from "../../../../src/cli";
@@ -21,14 +25,32 @@ describe("pickExtensionPackModelFile", () => {
],
},
};
const databaseItem = {
name: "github/vscode-codeql",
};
const cancellationTokenSource = new CancellationTokenSource();
const token = cancellationTokenSource.token;
const progress = jest.fn();
let showQuickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
let showInputBoxSpy: jest.SpiedFunction<typeof window.showInputBox>;
let showAndLogErrorMessageSpy: jest.SpiedFunction<
typeof helpers.showAndLogErrorMessage
>;
beforeEach(() => {
showQuickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockRejectedValue(new Error("Unexpected call to showQuickPick"));
showInputBoxSpy = jest
.spyOn(window, "showInputBox")
.mockRejectedValue(new Error("Unexpected call to showInputBox"));
showAndLogErrorMessageSpy = jest
.spyOn(helpers, "showAndLogErrorMessage")
.mockImplementation((msg) => {
throw new Error(`Unexpected call to showAndLogErrorMessage: ${msg}`);
});
});
it("allows choosing an existing extension pack and model file", async () => {
@@ -43,9 +65,14 @@ describe("pickExtensionPackModelFile", () => {
file: "/a/b/c/my-extension-pack/models/model.yml",
} as QuickPickItem);
expect(await pickExtensionPackModelFile(cliServer, progress)).toEqual(
"/a/b/c/my-extension-pack/models/model.yml",
);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual("/a/b/c/my-extension-pack/models/model.yml");
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
@@ -61,6 +88,7 @@ describe("pickExtensionPackModelFile", () => {
{
title: expect.any(String),
},
token,
);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
@@ -68,10 +96,15 @@ describe("pickExtensionPackModelFile", () => {
label: "models/model.yml",
file: "/a/b/c/my-extension-pack/models/model.yml",
},
{
label: expect.stringMatching(/create/i),
file: null,
},
],
{
title: expect.any(String),
},
token,
);
expect(cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith([], true);
@@ -82,14 +115,121 @@ describe("pickExtensionPackModelFile", () => {
);
});
it("allows choosing an existing extension pack and creating a new model file", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
...qlPacks,
"my-extension-pack": [tmpDir.path],
},
{
models: extensions.models,
data: {
[tmpDir.path]: [
{
file: join(tmpDir.path, "models/model.yml"),
index: 0,
predicate: "sinkModel",
},
],
},
},
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce({
label: "create",
file: null,
} as QuickPickItem);
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
}),
);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(join(tmpDir.path, "models/my-model.yml"));
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: "my-extension-pack",
extensionPack: "my-extension-pack",
},
{
label: "another-extension-pack",
extensionPack: "another-extension-pack",
},
],
{
title: expect.any(String),
},
token,
);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: "models/model.yml",
file: join(tmpDir.path, "models/model.yml"),
},
{
label: expect.stringMatching(/create/i),
file: null,
},
],
{
title: expect.any(String),
},
token,
);
expect(showInputBoxSpy).toHaveBeenCalledWith(
{
title: expect.any(String),
value: "models/github.vscode-codeql.model.yml",
validateInput: expect.any(Function),
},
token,
);
expect(cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith([], true);
expect(cliServer.resolveExtensions).toHaveBeenCalledTimes(1);
expect(cliServer.resolveExtensions).toHaveBeenCalledWith(tmpDir.path, []);
});
it("allows cancelling the extension pack prompt", async () => {
const cliServer = mockCliServer(qlPacks, extensions);
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(await pickExtensionPackModelFile(cliServer, progress)).toEqual(
undefined,
);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
@@ -99,22 +239,28 @@ describe("pickExtensionPackModelFile", () => {
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(await pickExtensionPackModelFile(cliServer, progress)).toEqual(
undefined,
);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showQuickPickSpy).toHaveBeenCalledWith([], {
title: expect.any(String),
});
expect(showQuickPickSpy).toHaveBeenCalledWith(
[],
{
title: expect.any(String),
},
token,
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
it("shows an error when an extension pack resolves to more than 1 location", async () => {
const showAndLogErrorMessageSpy = jest.spyOn(
helpers,
"showAndLogErrorMessage",
);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
const cliServer = mockCliServer(
{
@@ -131,9 +277,14 @@ describe("pickExtensionPackModelFile", () => {
extensionPack: "my-extension-pack",
} as QuickPickItem);
expect(await pickExtensionPackModelFile(cliServer, progress)).toEqual(
undefined,
);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/could not be resolved to a single location/),
@@ -153,47 +304,425 @@ describe("pickExtensionPackModelFile", () => {
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(await pickExtensionPackModelFile(cliServer, progress)).toEqual(
undefined,
);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
});
it("does not show any options when there are no model files", async () => {
const cliServer = mockCliServer(qlPacks, { models: [], data: {} });
it("shows create input box when there are no model files", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
expect(await pickExtensionPackModelFile(cliServer, progress)).toEqual(
undefined,
);
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: "my-extension-pack",
extensionPack: "my-extension-pack",
},
{
label: "another-extension-pack",
extensionPack: "another-extension-pack",
},
],
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(join(tmpDir.path, "models/my-model.yml"));
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).toHaveBeenCalledWith(
{
title: expect.any(String),
value: "models/github.vscode-codeql.model.yml",
validateInput: expect.any(Function),
},
token,
);
expect(showQuickPickSpy).toHaveBeenCalledWith([], {
title: expect.any(String),
});
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
});
it("shows an error when there is no pack YAML file", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/codeql-pack\.yml/),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
});
it("shows an error when the pack YAML file is invalid", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
await outputFile(join(tmpDir.path, "codeql-pack.yml"), dumpYaml("java"));
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/Could not parse/),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
});
it("shows an error when the pack YAML does not contain dataExtensions", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/Expected 'dataExtensions' to be/),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
});
it("shows an error when the pack YAML dataExtensions is invalid", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: {
"codeql/java-all": "invalid",
},
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/Expected 'dataExtensions' to be/),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
});
it("allows cancelling the new file input box", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
});
it("validates the file input", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
const qlpackPath = join(tmpDir.path, "codeql-pack.yml");
await outputFile(
qlpackPath,
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml", "data/**/*.yml"],
}),
);
await outputFile(
join(tmpDir.path, "models", "model.yml"),
dumpYaml({
extensions: [],
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
const validateFile = showInputBoxSpy.mock.calls[0][0]?.validateInput;
expect(validateFile).toBeDefined();
if (!validateFile) {
return;
}
expect(await validateFile("")).toEqual("File name must not be empty");
expect(await validateFile("models/model.yml")).toEqual(
"File already exists",
);
expect(await validateFile("../model.yml")).toEqual(
"File must be in the extension pack",
);
expect(await validateFile("/home/user/model.yml")).toEqual(
"File must be in the extension pack",
);
expect(await validateFile("model.yml")).toEqual(
`File must match one of the patterns in 'dataExtensions' in ${qlpackPath}`,
);
expect(await validateFile("models/model.yaml")).toEqual(
`File must match one of the patterns in 'dataExtensions' in ${qlpackPath}`,
);
expect(await validateFile("models/my-model.yml")).toBeUndefined();
expect(await validateFile("models/nested/model.yml")).toBeUndefined();
expect(await validateFile("data/model.yml")).toBeUndefined();
});
it("allows the dataExtensions to be a string", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
const qlpackPath = join(tmpDir.path, "codeql-pack.yml");
await outputFile(
qlpackPath,
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: "models/**/*.yml",
}),
);
await outputFile(
join(tmpDir.path, "models", "model.yml"),
dumpYaml({
extensions: [],
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
const validateFile = showInputBoxSpy.mock.calls[0][0]?.validateInput;
expect(validateFile).toBeDefined();
if (!validateFile) {
return;
}
expect(await validateFile("models/my-model.yml")).toBeUndefined();
});
});
function mockCliServer(