Merge remote-tracking branch 'origin/main' into koesie10/improve-error-message

This commit is contained in:
Koen Vlaswinkel
2024-01-29 14:47:36 +01:00
67 changed files with 1331 additions and 602 deletions

View File

@@ -11,6 +11,7 @@ on:
- extensions/ql-vscode/src/language-support/** - extensions/ql-vscode/src/language-support/**
- extensions/ql-vscode/src/query-server/** - extensions/ql-vscode/src/query-server/**
- extensions/ql-vscode/supported_cli_versions.json - extensions/ql-vscode/supported_cli_versions.json
- extensions/ql-vscode/src/variant-analysis/run-remote-query.ts
jobs: jobs:
find-nightly: find-nightly:

46
.github/workflows/e2e-tests.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Run E2E Playwright tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
e2e-test:
name: E2E Test
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: extensions/ql-vscode/.nvmrc
cache: 'npm'
cache-dependency-path: extensions/ql-vscode/package-lock.json
- name: Install dependencies
working-directory: extensions/ql-vscode
run: npm ci
- name: Start containers
working-directory: extensions/ql-vscode/test/e2e
run: docker-compose -f "docker-compose.yml" up -d --build
- name: Install Playwright Browsers
working-directory: extensions/ql-vscode
run: npx playwright install --with-deps
- name: Run Playwright tests
working-directory: extensions/ql-vscode/test/e2e
run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: extensions/ql-vscode/playwright-report/
retention-days: 30
- name: Stop containers
working-directory: extensions/ql-vscode/test/e2e
if: always()
run: docker-compose -f "docker-compose.yml" down -v

4
.gitignore vendored
View File

@@ -19,3 +19,7 @@ artifacts/
# CodeQL metadata # CodeQL metadata
.cache/ .cache/
.codeql/ .codeql/
# E2E Reports
**/playwright-report/**
**/test-results/**

View File

@@ -28,6 +28,7 @@ const baseConfig = {
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:import/recommended", "plugin:import/recommended",
"plugin:import/typescript", "plugin:import/typescript",
"plugin:deprecation/recommended",
], ],
rules: { rules: {
"@typescript-eslint/await-thenable": "error", "@typescript-eslint/await-thenable": "error",

View File

@@ -1,5 +1,6 @@
import * as React from "react"; import * as React from "react";
import { addons, types } from "@storybook/manager-api"; import { addons } from "@storybook/manager-api";
import { Addon_TypesEnum } from "@storybook/types";
import { ThemeSelector } from "./ThemeSelector"; import { ThemeSelector } from "./ThemeSelector";
const ADDON_ID = "vscode-theme-addon"; const ADDON_ID = "vscode-theme-addon";
@@ -7,7 +8,7 @@ const ADDON_ID = "vscode-theme-addon";
addons.register(ADDON_ID, () => { addons.register(ADDON_ID, () => {
addons.add(ADDON_ID, { addons.add(ADDON_ID, {
title: "VSCode Themes", title: "VSCode Themes",
type: types.TOOL, type: Addon_TypesEnum.TOOL,
match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)), match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)),
render: () => <ThemeSelector />, render: () => <ThemeSelector />,
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -1952,7 +1952,7 @@
"source-map": "^0.7.4", "source-map": "^0.7.4",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"stream-json": "^1.7.3", "stream-json": "^1.7.3",
"styled-components": "^6.0.2", "styled-components": "^6.1.8",
"tmp": "^0.2.1", "tmp": "^0.2.1",
"tmp-promise": "^3.0.2", "tmp-promise": "^3.0.2",
"tree-kill": "^1.2.2", "tree-kill": "^1.2.2",
@@ -1960,7 +1960,7 @@
"vscode-jsonrpc": "^8.0.2", "vscode-jsonrpc": "^8.0.2",
"vscode-languageclient": "^8.0.2", "vscode-languageclient": "^8.0.2",
"yauzl": "^2.10.0", "yauzl": "^2.10.0",
"zip-a-folder": "^3.1.3" "zip-a-folder": "^3.1.6"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.18.13", "@babel/core": "^7.18.13",
@@ -1971,6 +1971,7 @@
"@faker-js/faker": "^8.0.2", "@faker-js/faker": "^8.0.2",
"@github/markdownlint-github": "^0.6.0", "@github/markdownlint-github": "^0.6.0",
"@octokit/plugin-throttling": "^8.0.0", "@octokit/plugin-throttling": "^8.0.0",
"@playwright/test": "^1.40.1",
"@storybook/addon-a11y": "^7.6.9", "@storybook/addon-a11y": "^7.6.9",
"@storybook/addon-actions": "^7.1.0", "@storybook/addon-actions": "^7.1.0",
"@storybook/addon-essentials": "^7.1.0", "@storybook/addon-essentials": "^7.1.0",
@@ -1999,7 +2000,7 @@
"@types/node": "18.15.*", "@types/node": "18.15.*",
"@types/node-fetch": "^2.5.2", "@types/node-fetch": "^2.5.2",
"@types/react": "^18.0.28", "@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.2.18",
"@types/sarif": "^2.1.2", "@types/sarif": "^2.1.2",
"@types/semver": "^7.2.0", "@types/semver": "^7.2.0",
"@types/stream-json": "^1.7.1", "@types/stream-json": "^1.7.1",
@@ -2023,6 +2024,7 @@
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.6.1", "eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-deprecation": "^2.0.0",
"eslint-plugin-etc": "^2.0.2", "eslint-plugin-etc": "^2.0.2",
"eslint-plugin-github": "^4.4.1", "eslint-plugin-github": "^4.4.1",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
@@ -2042,13 +2044,13 @@
"jest-environment-jsdom": "^29.0.3", "jest-environment-jsdom": "^29.0.3",
"jest-runner-vscode": "^3.0.1", "jest-runner-vscode": "^3.0.1",
"lint-staged": "^15.0.2", "lint-staged": "^15.0.2",
"markdownlint-cli2": "^0.11.0", "markdownlint-cli2": "^0.12.1",
"markdownlint-cli2-formatter-pretty": "^0.0.5", "markdownlint-cli2-formatter-pretty": "^0.0.5",
"mini-css-extract-plugin": "^2.6.1", "mini-css-extract-plugin": "^2.6.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"prettier": "^3.0.0", "prettier": "^3.2.4",
"storybook": "^7.6.7", "storybook": "^7.6.10",
"tar-stream": "^3.0.0", "tar-stream": "^3.0.0",
"through2": "^4.0.2", "through2": "^4.0.2",
"ts-jest": "^29.0.1", "ts-jest": "^29.0.1",

View File

@@ -14,7 +14,8 @@ function ignoreFile(file: string): boolean {
) || ) ||
basename(file) === "jest.config.ts" || basename(file) === "jest.config.ts" ||
basename(file) === "index.tsx" || basename(file) === "index.tsx" ||
basename(file) === "index.ts" basename(file) === "index.ts" ||
basename(file) === "playwright.config.ts"
); );
} }

View File

@@ -45,8 +45,9 @@ export async function getVersionInformation(
vscodeVersion: string, vscodeVersion: string,
): Promise<VersionInformation> { ): Promise<VersionInformation> {
const vsCodePackageJson = await getVsCodePackageJson(vscodeVersion); const vsCodePackageJson = await getVsCodePackageJson(vscodeVersion);
const electronVersion = minVersion(vsCodePackageJson.devDependencies.electron) const electronVersion = minVersion(
?.version; vsCodePackageJson.devDependencies.electron,
)?.version;
if (!electronVersion) { if (!electronVersion) {
throw new Error("Could not find Electron version"); throw new Error("Could not find Electron version");
} }

View File

@@ -1739,6 +1739,15 @@ export class CliVersionConstraint {
*/ */
public static CLI_VERSION_WITH_TRIM_CACHE = new SemVer("2.15.1"); public static CLI_VERSION_WITH_TRIM_CACHE = new SemVer("2.15.1");
public static CLI_VERSION_WITHOUT_MRVA_EXTENSIBLE_PREDICATE_HACK = new SemVer(
"2.16.1",
);
/**
* CLI version where there is support for multiple queries on the pack create command.
*/
public static CLI_VERSION_WITH_MULTI_QUERY_PACK_CREATE = new SemVer("2.16.1");
constructor(private readonly cli: CodeQLCliServer) { constructor(private readonly cli: CodeQLCliServer) {
/**/ /**/
} }
@@ -1781,6 +1790,19 @@ export class CliVersionConstraint {
); );
} }
async preservesExtensiblePredicatesInMrvaPack() {
// Negated, because we _stopped_ preserving these in 2.16.1.
return !(await this.isVersionAtLeast(
CliVersionConstraint.CLI_VERSION_WITHOUT_MRVA_EXTENSIBLE_PREDICATE_HACK,
));
}
async supportsPackCreateWithMultipleQueries() {
return this.isVersionAtLeast(
CliVersionConstraint.CLI_VERSION_WITH_MULTI_QUERY_PACK_CREATE,
);
}
async supportsMrvaPackCreate(): Promise<boolean> { async supportsMrvaPackCreate(): Promise<boolean> {
return (await this.cli.getFeatures()).mrvaPackCreate === true; return (await this.cli.getFeatures()).mrvaPackCreate === true;
} }

View File

@@ -1,4 +1,4 @@
import { join } from "path"; import { dirname, join, parse } from "path";
import { pathExists } from "fs-extra"; import { pathExists } from "fs-extra";
export const QLPACK_FILENAMES = ["qlpack.yml", "codeql-pack.yml"]; export const QLPACK_FILENAMES = ["qlpack.yml", "codeql-pack.yml"];
@@ -8,7 +8,13 @@ export const QLPACK_LOCK_FILENAMES = [
]; ];
export const FALLBACK_QLPACK_FILENAME = QLPACK_FILENAMES[0]; export const FALLBACK_QLPACK_FILENAME = QLPACK_FILENAMES[0];
export async function getQlPackPath( /**
* Gets the path to the QL pack file (a qlpack.yml or
* codeql-pack.yml).
* @param packRoot The root of the pack.
* @returns The path to the qlpack file, or undefined if it doesn't exist.
*/
export async function getQlPackFilePath(
packRoot: string, packRoot: string,
): Promise<string | undefined> { ): Promise<string | undefined> {
for (const filename of QLPACK_FILENAMES) { for (const filename of QLPACK_FILENAMES) {
@@ -21,3 +27,28 @@ export async function getQlPackPath(
return undefined; return undefined;
} }
/**
* Recursively find the directory containing qlpack.yml or codeql-pack.yml. If
* no such directory is found, the directory containing the query file is returned.
* @param queryFile The query file to start from.
* @returns The path to the pack root.
*/
export async function findPackRoot(queryFile: string): Promise<string> {
let dir = dirname(queryFile);
while (!(await getQlPackFilePath(dir))) {
dir = dirname(dir);
if (isFileSystemRoot(dir)) {
// there is no qlpack.yml or codeql-pack.yml in this directory or any parent directory.
// just use the query file's directory as the pack root.
return dirname(queryFile);
}
}
return dir;
}
function isFileSystemRoot(dir: string): boolean {
const pathObj = parse(dir);
return pathObj.root === dir && pathObj.base === "";
}

View File

@@ -1,11 +1,12 @@
export type DeepReadonly<T> = T extends Array<infer R> export type DeepReadonly<T> =
? DeepReadonlyArray<R> T extends Array<infer R>
: // eslint-disable-next-line @typescript-eslint/ban-types ? DeepReadonlyArray<R>
T extends Function : // eslint-disable-next-line @typescript-eslint/ban-types
? T T extends Function
: T extends object ? T
? DeepReadonlyObject<T> : T extends object
: T; ? DeepReadonlyObject<T>
: T;
interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {} interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

View File

@@ -716,12 +716,17 @@ const LLM_GENERATION_DEV_ENDPOINT = new Setting(
); );
const EXTENSIONS_DIRECTORY = new Setting("extensionsDirectory", MODEL_SETTING); const EXTENSIONS_DIRECTORY = new Setting("extensionsDirectory", MODEL_SETTING);
const ENABLE_RUBY = new Setting("enableRuby", MODEL_SETTING); const ENABLE_RUBY = new Setting("enableRuby", MODEL_SETTING);
const ENABLE_ACCESS_PATH_SUGGESTIONS = new Setting(
"enableAccessPathSuggestions",
MODEL_SETTING,
);
export interface ModelConfig { export interface ModelConfig {
flowGeneration: boolean; flowGeneration: boolean;
llmGeneration: boolean; llmGeneration: boolean;
getExtensionsDirectory(languageId: string): string | undefined; getExtensionsDirectory(languageId: string): string | undefined;
enableRuby: boolean; enableRuby: boolean;
enableAccessPathSuggestions: boolean;
} }
export class ModelConfigListener extends ConfigListener implements ModelConfig { export class ModelConfigListener extends ConfigListener implements ModelConfig {
@@ -762,6 +767,10 @@ export class ModelConfigListener extends ConfigListener implements ModelConfig {
public get enableRuby(): boolean { public get enableRuby(): boolean {
return !!ENABLE_RUBY.getValue<boolean>(); return !!ENABLE_RUBY.getValue<boolean>();
} }
public get enableAccessPathSuggestions(): boolean {
return !!ENABLE_ACCESS_PATH_SUGGESTIONS.getValue<boolean>();
}
} }
const GITHUB_DATABASE_SETTING = new Setting("githubDatabase", ROOT_SETTING); const GITHUB_DATABASE_SETTING = new Setting("githubDatabase", ROOT_SETTING);

View File

@@ -235,7 +235,7 @@ async function chooseDatabaseDir(byFolder: boolean): Promise<Uri | undefined> {
return getFirst(chosen); return getFirst(chosen);
} }
interface DatabaseSelectionQuickPickItem extends QuickPickItem { export interface DatabaseSelectionQuickPickItem extends QuickPickItem {
databaseKind: "new" | "existing"; databaseKind: "new" | "existing";
} }
@@ -243,7 +243,7 @@ export interface DatabaseQuickPickItem extends QuickPickItem {
databaseItem: DatabaseItem; databaseItem: DatabaseItem;
} }
interface DatabaseImportQuickPickItems extends QuickPickItem { export interface DatabaseImportQuickPickItems extends QuickPickItem {
importType: "URL" | "github" | "archive" | "folder"; importType: "URL" | "github" | "archive" | "folder";
} }

View File

@@ -3,7 +3,7 @@ import { glob } from "glob";
import { basename } from "path"; import { basename } from "path";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { readFile } from "fs-extra"; import { readFile } from "fs-extra";
import { getQlPackPath } from "../common/ql"; import { getQlPackFilePath } from "../common/ql";
import type { CodeQLCliServer, QlpacksInfo } from "../codeql-cli/cli"; import type { CodeQLCliServer, QlpacksInfo } from "../codeql-cli/cli";
import { extLogger } from "../common/logging/vscode"; import { extLogger } from "../common/logging/vscode";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders"; import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
@@ -31,7 +31,7 @@ async function findDbschemePack(
): Promise<{ name: string; isLibraryPack: boolean }> { ): Promise<{ name: string; isLibraryPack: boolean }> {
for (const { packDir, packName } of packs) { for (const { packDir, packName } of packs) {
if (packDir !== undefined) { if (packDir !== undefined) {
const qlpackPath = await getQlPackPath(packDir); const qlpackPath = await getQlPackFilePath(packDir);
if (qlpackPath !== undefined) { if (qlpackPath !== undefined) {
const qlpack = load(await readFile(qlpackPath, "utf8")) as { const qlpack = load(await readFile(qlpackPath, "utf8")) as {

View File

@@ -215,8 +215,9 @@ function getCommands(
"codeQL.restartLegacyQueryServerOnConfigChange": restartQueryServer, "codeQL.restartLegacyQueryServerOnConfigChange": restartQueryServer,
"codeQL.restartQueryServerOnExternalConfigChange": restartQueryServer, "codeQL.restartQueryServerOnExternalConfigChange": restartQueryServer,
"codeQL.copyVersion": async () => { "codeQL.copyVersion": async () => {
const text = `CodeQL extension version: ${extension?.packageJSON const text = `CodeQL extension version: ${
.version} \nCodeQL CLI version: ${await getCliVersion()} \nPlatform: ${platform()} ${arch()}`; extension?.packageJSON.version
} \nCodeQL CLI version: ${await getCliVersion()} \nPlatform: ${platform()} ${arch()}`;
await env.clipboard.writeText(text); await env.clipboard.writeText(text);
void showAndLogInformationMessage(extLogger, text); void showAndLogInformationMessage(extLogger, text);
}, },

View File

@@ -11,7 +11,7 @@ import { getPrimaryDbscheme, getQlPackForDbscheme } from "../databases/qlpack";
import type { ProgressCallback } from "../common/vscode/progress"; import type { ProgressCallback } from "../common/vscode/progress";
import { UserCancellationException } from "../common/vscode/progress"; import { UserCancellationException } from "../common/vscode/progress";
import { getErrorMessage } from "../common/helpers-pure"; import { getErrorMessage } from "../common/helpers-pure";
import { FALLBACK_QLPACK_FILENAME, getQlPackPath } from "../common/ql"; import { FALLBACK_QLPACK_FILENAME, getQlPackFilePath } from "../common/ql";
import type { App } from "../common/app"; import type { App } from "../common/app";
import type { ExtensionApp } from "../common/vscode/vscode-app"; import type { ExtensionApp } from "../common/vscode/vscode-app";
@@ -119,7 +119,7 @@ export async function displayQuickQuery(
const dbscheme = await getPrimaryDbscheme(datasetFolder); const dbscheme = await getPrimaryDbscheme(datasetFolder);
const qlpack = (await getQlPackForDbscheme(cliServer, dbscheme)) const qlpack = (await getQlPackForDbscheme(cliServer, dbscheme))
.dbschemePack; .dbschemePack;
const qlPackFile = await getQlPackPath(queriesDir); const qlPackFile = await getQlPackFilePath(queriesDir);
const qlFile = join(queriesDir, QUICK_QUERY_QUERY_NAME); const qlFile = join(queriesDir, QUICK_QUERY_QUERY_NAME);
const shouldRewrite = await checkShouldRewrite(qlPackFile, qlpack); const shouldRewrite = await checkShouldRewrite(qlPackFile, qlpack);

View File

@@ -36,7 +36,7 @@ import { redactableError } from "../common/errors";
import type { App } from "../common/app"; import type { App } from "../common/app";
import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item"; import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
import { containsPath, pathsEqual } from "../common/files"; import { containsPath, pathsEqual } from "../common/files";
import { getQlPackPath } from "../common/ql"; import { getQlPackFilePath } from "../common/ql";
import { getQlPackLanguage } from "../common/qlpack-language"; import { getQlPackLanguage } from "../common/qlpack-language";
type QueryLanguagesToDatabaseMap = Record<string, string>; type QueryLanguagesToDatabaseMap = Record<string, string>;
@@ -111,7 +111,7 @@ export class SkeletonQueryWizard {
// Try to detect if there is already a qlpack in this location. We will assume that // Try to detect if there is already a qlpack in this location. We will assume that
// the user hasn't changed the language of the qlpack. // the user hasn't changed the language of the qlpack.
const qlPackPath = await getQlPackPath(this.qlPackStoragePath); const qlPackPath = await getQlPackFilePath(this.qlPackStoragePath);
// If we are creating or using a qlpack in the user's selected folder, we will also // If we are creating or using a qlpack in the user's selected folder, we will also
// create the query in that folder // create the query in that folder
@@ -248,7 +248,7 @@ export class SkeletonQueryWizard {
const matchingQueryPackPath = matchingQueryPacks[0]; const matchingQueryPackPath = matchingQueryPacks[0];
const qlPackPath = await getQlPackPath(matchingQueryPackPath); const qlPackPath = await getQlPackFilePath(matchingQueryPackPath);
if (!qlPackPath) { if (!qlPackPath) {
return undefined; return undefined;
} }

View File

@@ -9,7 +9,7 @@ import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import type { ProgressCallback } from "../common/vscode/progress"; import type { ProgressCallback } from "../common/vscode/progress";
import { UserCancellationException } from "../common/vscode/progress"; import { UserCancellationException } from "../common/vscode/progress";
import type { DatabaseItem } from "../databases/local-databases"; import type { DatabaseItem } from "../databases/local-databases";
import { getQlPackPath, QLPACK_FILENAMES } from "../common/ql"; import { getQlPackFilePath, QLPACK_FILENAMES } from "../common/ql";
import { getErrorMessage } from "../common/helpers-pure"; import { getErrorMessage } from "../common/helpers-pure";
import type { ExtensionPack } from "./shared/extension-pack"; import type { ExtensionPack } from "./shared/extension-pack";
import type { NotificationLogger } from "../common/logging"; import type { NotificationLogger } from "../common/logging";
@@ -208,7 +208,7 @@ async function readExtensionPack(
path: string, path: string,
language: string, language: string,
): Promise<ExtensionPack> { ): Promise<ExtensionPack> {
const qlpackPath = await getQlPackPath(path); const qlpackPath = await getQlPackFilePath(path);
if (!qlpackPath) { if (!qlpackPath) {
throw new Error( throw new Error(
`Could not find any of ${QLPACK_FILENAMES.join(", ")} in ${path}`, `Could not find any of ${QLPACK_FILENAMES.join(", ")} in ${path}`,

View File

@@ -1,6 +1,7 @@
import type { Meta, StoryFn } from "@storybook/react"; import type { Meta, StoryFn } from "@storybook/react";
import { faker } from "@faker-js/faker"; import { faker } from "@faker-js/faker";
import { customAlphabet } from "nanoid";
import { VariantAnalysisContainer } from "../../view/variant-analysis/VariantAnalysisContainer"; import { VariantAnalysisContainer } from "../../view/variant-analysis/VariantAnalysisContainer";
import { VariantAnalysisAnalyzedRepos } from "../../view/variant-analysis/VariantAnalysisAnalyzedRepos"; import { VariantAnalysisAnalyzedRepos } from "../../view/variant-analysis/VariantAnalysisAnalyzedRepos";
@@ -125,11 +126,10 @@ Example.args = {
}; };
faker.seed(42); faker.seed(42);
const uniqueStore = {};
const manyScannedRepos = Array.from({ length: 1000 }, (_, i) => { const manyScannedRepos = Array.from({ length: 1000 }, (_, i) => {
const mockedScannedRepo = createMockScannedRepo(); const mockedScannedRepo = createMockScannedRepo();
const nanoid = customAlphabet("123456789");
return { return {
...mockedScannedRepo, ...mockedScannedRepo,
analysisStatus: VariantAnalysisRepoStatus.Succeeded, analysisStatus: VariantAnalysisRepoStatus.Succeeded,
@@ -137,12 +137,8 @@ const manyScannedRepos = Array.from({ length: 1000 }, (_, i) => {
repository: { repository: {
...mockedScannedRepo.repository, ...mockedScannedRepo.repository,
// We need to ensure the ID is unique for React keys // We need to ensure the ID is unique for React keys
id: faker.helpers.unique(faker.number.int, [], { id: parseInt(nanoid()),
store: uniqueStore, fullName: `octodemo/${nanoid()}`,
}),
fullName: `octodemo/${faker.helpers.unique(faker.word.sample, [], {
store: uniqueStore,
})}`,
}, },
}; };
}); });

View File

@@ -0,0 +1,20 @@
import type { QueryLanguage } from "../common/query-language";
/**
* Details about the original QL pack that is used for triggering
* a variant analysis.
*/
export interface QlPackDetails {
// The absolute paths of the query files.
queryFiles: string[];
// The absolute path to the QL pack that is used for triggering a variant analysis.
// If there is no query pack, this is the same as the directory of the query files.
qlPackRootPath: string;
// The absolute path to the QL pack file (a qlpack.yml or codeql-pack.yml) or undefined if
// it doesn't exist.
qlPackFilePath: string | undefined;
language: QueryLanguage;
}

View File

@@ -1,6 +1,6 @@
import type { CancellationToken } from "vscode"; import type { CancellationToken } from "vscode";
import { Uri, window } from "vscode"; import { Uri, window } from "vscode";
import { relative, join, sep, dirname, parse, basename } from "path"; import { join, sep, basename, relative } from "path";
import { dump, load } from "js-yaml"; import { dump, load } from "js-yaml";
import { copy, writeFile, readFile, mkdirp } from "fs-extra"; import { copy, writeFile, readFile, mkdirp } from "fs-extra";
import type { DirectoryResult } from "tmp-promise"; import type { DirectoryResult } from "tmp-promise";
@@ -29,27 +29,20 @@ import {
import type { Repository } from "./shared/repository"; import type { Repository } from "./shared/repository";
import type { DbManager } from "../databases/db-manager"; import type { DbManager } from "../databases/db-manager";
import { import {
getQlPackPath, getQlPackFilePath,
FALLBACK_QLPACK_FILENAME, FALLBACK_QLPACK_FILENAME,
QLPACK_FILENAMES, QLPACK_FILENAMES,
QLPACK_LOCK_FILENAMES, QLPACK_LOCK_FILENAMES,
} from "../common/ql"; } from "../common/ql";
import type { QueryLanguage } from "../common/query-language";
import { tryGetQueryMetadata } from "../codeql-cli/query-metadata";
import { askForLanguage, findLanguage } from "../codeql-cli/query-language";
import type { QlPackFile } from "../packaging/qlpack-file"; import type { QlPackFile } from "../packaging/qlpack-file";
import { expandShortPaths } from "../common/short-paths"; import { expandShortPaths } from "../common/short-paths";
import type { QlPackDetails } from "./ql-pack-details";
/** /**
* Well-known names for the query pack used by the server. * Well-known names for the query pack used by the server.
*/ */
const QUERY_PACK_NAME = "codeql-remote/query"; const QUERY_PACK_NAME = "codeql-remote/query";
interface GeneratedQueryPack {
base64Pack: string;
language: string;
}
/** /**
* Two possibilities: * Two possibilities:
* 1. There is no qlpack.yml (or codeql-pack.yml) in this directory. Assume this is a lone query and generate a synthetic qlpack for it. * 1. There is no qlpack.yml (or codeql-pack.yml) in this directory. Assume this is a lone query and generate a synthetic qlpack for it.
@@ -59,45 +52,30 @@ interface GeneratedQueryPack {
*/ */
async function generateQueryPack( async function generateQueryPack(
cliServer: CodeQLCliServer, cliServer: CodeQLCliServer,
queryFile: string, qlPackDetails: QlPackDetails,
tmpDir: RemoteQueryTempDir, tmpDir: RemoteQueryTempDir,
): Promise<GeneratedQueryPack> { ): Promise<string> {
const originalPackRoot = await findPackRoot(queryFile);
const packRelativePath = relative(originalPackRoot, queryFile);
const workspaceFolders = getOnDiskWorkspaceFolders(); const workspaceFolders = getOnDiskWorkspaceFolders();
const extensionPacks = await getExtensionPacksToInject( const extensionPacks = await getExtensionPacksToInject(
cliServer, cliServer,
workspaceFolders, workspaceFolders,
); );
const mustSynthesizePack = const mustSynthesizePack = qlPackDetails.qlPackFilePath === undefined;
(await getQlPackPath(originalPackRoot)) === undefined;
const cliSupportsMrvaPackCreate = const cliSupportsMrvaPackCreate =
await cliServer.cliConstraints.supportsMrvaPackCreate(); await cliServer.cliConstraints.supportsMrvaPackCreate();
const language: QueryLanguage | undefined = mustSynthesizePack let targetPackPath: string;
? await askForLanguage(cliServer) // open popup to ask for language if not already hardcoded
: await findLanguage(cliServer, Uri.file(queryFile));
if (!language) {
throw new UserCancellationException("Could not determine language");
}
let queryPackDir: string;
let needsInstall: boolean; let needsInstall: boolean;
if (mustSynthesizePack) { if (mustSynthesizePack) {
// This section applies whether or not the CLI supports MRVA pack creation directly. // This section applies whether or not the CLI supports MRVA pack creation directly.
queryPackDir = tmpDir.queryPackDir; targetPackPath = tmpDir.queryPackDir;
// Synthesize a query pack for the query. // Synthesize a query pack for the query.
// copy only the query file to the query pack directory // copy only the query file to the query pack directory
// and generate a synthetic query pack // and generate a synthetic query pack
await createNewQueryPack( await createNewQueryPack(qlPackDetails, targetPackPath);
queryFile,
queryPackDir,
language,
packRelativePath,
);
// Clear the cliServer cache so that the previous qlpack text is purged from the CLI. // Clear the cliServer cache so that the previous qlpack text is purged from the CLI.
await cliServer.clearCache(); await cliServer.clearCache();
@@ -105,14 +83,8 @@ async function generateQueryPack(
needsInstall = true; needsInstall = true;
} else if (!cliSupportsMrvaPackCreate) { } else if (!cliSupportsMrvaPackCreate) {
// We need to copy the query pack to a temporary directory and then fix it up to work with MRVA. // We need to copy the query pack to a temporary directory and then fix it up to work with MRVA.
queryPackDir = tmpDir.queryPackDir; targetPackPath = tmpDir.queryPackDir;
await copyExistingQueryPack( await copyExistingQueryPack(cliServer, qlPackDetails, targetPackPath);
cliServer,
originalPackRoot,
queryFile,
queryPackDir,
packRelativePath,
);
// We should already have all the dependencies available, but these older versions of the CLI // We should already have all the dependencies available, but these older versions of the CLI
// have a bug where they will not search `--additional-packs` during validation in `codeql pack bundle`. // have a bug where they will not search `--additional-packs` during validation in `codeql pack bundle`.
@@ -120,14 +92,14 @@ async function generateQueryPack(
needsInstall = true; needsInstall = true;
} else { } else {
// The CLI supports creating a MRVA query pack directly from the source pack. // The CLI supports creating a MRVA query pack directly from the source pack.
queryPackDir = originalPackRoot; targetPackPath = qlPackDetails.qlPackRootPath;
// We expect any dependencies to be available already. // We expect any dependencies to be available already.
needsInstall = false; needsInstall = false;
} }
if (needsInstall) { if (needsInstall) {
// Install the dependencies of the synthesized query pack. // Install the dependencies of the synthesized query pack.
await cliServer.packInstall(queryPackDir, { await cliServer.packInstall(targetPackPath, {
workspaceFolders, workspaceFolders,
}); });
@@ -137,10 +109,23 @@ async function generateQueryPack(
let precompilationOpts: string[]; let precompilationOpts: string[];
if (cliSupportsMrvaPackCreate) { if (cliSupportsMrvaPackCreate) {
if (
qlPackDetails.queryFiles.length > 1 &&
!(await cliServer.cliConstraints.supportsPackCreateWithMultipleQueries())
) {
throw new Error(
`Installed CLI version does not allow creating a MRVA pack with multiple queries`,
);
}
const queryOpts = qlPackDetails.queryFiles.flatMap((q) => [
"--query",
join(targetPackPath, relative(qlPackDetails.qlPackRootPath, q)),
]);
precompilationOpts = [ precompilationOpts = [
"--mrva", "--mrva",
"--query", ...queryOpts,
join(queryPackDir, packRelativePath),
// We need to specify the extension packs as dependencies so that they are included in the MRVA pack. // We need to specify the extension packs as dependencies so that they are included in the MRVA pack.
// The version range doesn't matter, since they'll always be found by source lookup. // The version range doesn't matter, since they'll always be found by source lookup.
...extensionPacks.map((p) => `--extension-pack=${p}@*`), ...extensionPacks.map((p) => `--extension-pack=${p}@*`),
@@ -149,7 +134,7 @@ async function generateQueryPack(
if (await cliServer.cliConstraints.usesGlobalCompilationCache()) { if (await cliServer.cliConstraints.usesGlobalCompilationCache()) {
precompilationOpts = ["--qlx"]; precompilationOpts = ["--qlx"];
} else { } else {
const cache = join(originalPackRoot, ".cache"); const cache = join(qlPackDetails.qlPackRootPath, ".cache");
precompilationOpts = [ precompilationOpts = [
"--qlx", "--qlx",
"--no-default-compilation-cache", "--no-default-compilation-cache",
@@ -158,60 +143,61 @@ async function generateQueryPack(
} }
if (extensionPacks.length > 0) { if (extensionPacks.length > 0) {
await addExtensionPacksAsDependencies(queryPackDir, extensionPacks); await addExtensionPacksAsDependencies(targetPackPath, extensionPacks);
} }
} }
const bundlePath = tmpDir.bundleFile; const bundlePath = tmpDir.bundleFile;
void extLogger.log( void extLogger.log(
`Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`, `Compiling and bundling query pack from ${targetPackPath} to ${bundlePath}. (This may take a while.)`,
); );
await cliServer.packBundle( await cliServer.packBundle(
queryPackDir, targetPackPath,
workspaceFolders, workspaceFolders,
bundlePath, bundlePath,
tmpDir.compiledPackDir, tmpDir.compiledPackDir,
precompilationOpts, precompilationOpts,
); );
const base64Pack = (await readFile(bundlePath)).toString("base64"); const base64Pack = (await readFile(bundlePath)).toString("base64");
return { return base64Pack;
base64Pack,
language,
};
} }
async function createNewQueryPack( async function createNewQueryPack(
queryFile: string, qlPackDetails: QlPackDetails,
queryPackDir: string, targetPackPath: string,
language: string | undefined,
packRelativePath: string,
) { ) {
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`); for (const queryFile of qlPackDetails.queryFiles) {
const targetQueryFileName = join(queryPackDir, packRelativePath); void extLogger.log(`Copying ${queryFile} to ${targetPackPath}`);
await copy(queryFile, targetQueryFileName); const relativeQueryPath = relative(qlPackDetails.qlPackRootPath, queryFile);
const targetQueryFileName = join(targetPackPath, relativeQueryPath);
await copy(queryFile, targetQueryFileName);
}
void extLogger.log("Generating synthetic query pack"); void extLogger.log("Generating synthetic query pack");
const syntheticQueryPack = { const syntheticQueryPack = {
name: QUERY_PACK_NAME, name: QUERY_PACK_NAME,
version: "0.0.0", version: "0.0.0",
dependencies: { dependencies: {
[`codeql/${language}-all`]: "*", [`codeql/${qlPackDetails.language}-all`]: "*",
}, },
defaultSuite: generateDefaultSuite(packRelativePath), defaultSuite: generateDefaultSuite(qlPackDetails),
}; };
await writeFile( await writeFile(
join(queryPackDir, FALLBACK_QLPACK_FILENAME), join(targetPackPath, FALLBACK_QLPACK_FILENAME),
dump(syntheticQueryPack), dump(syntheticQueryPack),
); );
} }
async function copyExistingQueryPack( async function copyExistingQueryPack(
cliServer: CodeQLCliServer, cliServer: CodeQLCliServer,
originalPackRoot: string, qlPackDetails: QlPackDetails,
queryFile: string, targetPackPath: string,
queryPackDir: string,
packRelativePath: string,
) { ) {
const toCopy = await cliServer.packPacklist(originalPackRoot, false); const toCopy = await cliServer.packPacklist(
qlPackDetails.qlPackRootPath,
false,
);
// Also include query files that contain extensible predicates. These query files are not // Also include query files that contain extensible predicates. These query files are not
// needed for the query to run, but they are needed for the query pack to pass deep validation // needed for the query to run, but they are needed for the query pack to pass deep validation
@@ -219,19 +205,20 @@ async function copyExistingQueryPack(
if ( if (
await cliServer.cliConstraints.supportsGenerateExtensiblePredicateMetadata() await cliServer.cliConstraints.supportsGenerateExtensiblePredicateMetadata()
) { ) {
const metadata = const metadata = await cliServer.generateExtensiblePredicateMetadata(
await cliServer.generateExtensiblePredicateMetadata(originalPackRoot); qlPackDetails.qlPackRootPath,
);
metadata.extensible_predicates.forEach((predicate) => { metadata.extensible_predicates.forEach((predicate) => {
if (predicate.path.endsWith(".ql")) { if (predicate.path.endsWith(".ql")) {
toCopy.push(join(originalPackRoot, predicate.path)); toCopy.push(join(qlPackDetails.qlPackRootPath, predicate.path));
} }
}); });
} }
[ [
// also copy the lock file (either new name or old name) and the query file itself. These are not included in the packlist. // also copy the lock file (either new name or old name) and the query file itself. These are not included in the packlist.
...QLPACK_LOCK_FILENAMES.map((f) => join(originalPackRoot, f)), ...QLPACK_LOCK_FILENAMES.map((f) => join(qlPackDetails.qlPackRootPath, f)),
queryFile, ...qlPackDetails.queryFiles,
].forEach((absolutePath) => { ].forEach((absolutePath) => {
if (absolutePath) { if (absolutePath) {
toCopy.push(absolutePath); toCopy.push(absolutePath);
@@ -239,7 +226,7 @@ async function copyExistingQueryPack(
}); });
let copiedCount = 0; let copiedCount = 0;
await copy(originalPackRoot, queryPackDir, { await copy(qlPackDetails.qlPackRootPath, targetPackPath, {
filter: (file: string) => filter: (file: string) =>
// copy file if it is in the packlist, or it is a parent directory of a file in the packlist // copy file if it is in the packlist, or it is a parent directory of a file in the packlist
!!toCopy.find((f) => { !!toCopy.find((f) => {
@@ -254,29 +241,9 @@ async function copyExistingQueryPack(
}), }),
}); });
void extLogger.log(`Copied ${copiedCount} files to ${queryPackDir}`); void extLogger.log(`Copied ${copiedCount} files to ${targetPackPath}`);
await fixPackFile(queryPackDir, packRelativePath); await fixPackFile(targetPackPath, qlPackDetails);
}
async function findPackRoot(queryFile: string): Promise<string> {
// recursively find the directory containing qlpack.yml or codeql-pack.yml
let dir = dirname(queryFile);
while (!(await getQlPackPath(dir))) {
dir = dirname(dir);
if (isFileSystemRoot(dir)) {
// there is no qlpack.yml or codeql-pack.yml in this directory or any parent directory.
// just use the query file's directory as the pack root.
return dirname(queryFile);
}
}
return dir;
}
function isFileSystemRoot(dir: string): boolean {
const pathObj = parse(dir);
return pathObj.root === dir && pathObj.base === "";
} }
interface RemoteQueryTempDir { interface RemoteQueryTempDir {
@@ -319,35 +286,26 @@ interface PreparedRemoteQuery {
actionBranch: string; actionBranch: string;
base64Pack: string; base64Pack: string;
repoSelection: RepositorySelection; repoSelection: RepositorySelection;
queryFile: string;
queryMetadata: QueryMetadata | undefined;
controllerRepo: Repository; controllerRepo: Repository;
queryStartTime: number; queryStartTime: number;
language: string;
} }
export async function prepareRemoteQueryRun( export async function prepareRemoteQueryRun(
cliServer: CodeQLCliServer, cliServer: CodeQLCliServer,
credentials: Credentials, credentials: Credentials,
uris: Uri[], qlPackDetails: QlPackDetails,
progress: ProgressCallback, progress: ProgressCallback,
token: CancellationToken, token: CancellationToken,
dbManager: DbManager, dbManager: DbManager,
): Promise<PreparedRemoteQuery> { ): Promise<PreparedRemoteQuery> {
if (uris.length !== 1) { for (const queryFile of qlPackDetails.queryFiles) {
// For now we only support a single file, but we're aiming if (!queryFile.endsWith(".ql")) {
// to support multiple files in the near future. throw new UserCancellationException(
throw Error("Exactly one query file must be selected."); `Not a CodeQL query file: ${queryFile}`,
);
}
} }
const uri = uris[0];
if (!uri.fsPath.endsWith(".ql")) {
throw new UserCancellationException("Not a CodeQL query file.");
}
const queryFile = uri.fsPath;
progress({ progress({
maxStep: 4, maxStep: 4,
step: 1, step: 1,
@@ -379,16 +337,14 @@ export async function prepareRemoteQueryRun(
const tempDir = await createRemoteQueriesTempDirectory(); const tempDir = await createRemoteQueriesTempDirectory();
let pack: GeneratedQueryPack; let base64Pack: string;
try { try {
pack = await generateQueryPack(cliServer, queryFile, tempDir); base64Pack = await generateQueryPack(cliServer, qlPackDetails, tempDir);
} finally { } finally {
await tempDir.remoteQueryDir.cleanup(); await tempDir.remoteQueryDir.cleanup();
} }
const { base64Pack, language } = pack;
if (token.isCancellationRequested) { if (token.isCancellationRequested) {
throw new UserCancellationException("Cancelled"); throw new UserCancellationException("Cancelled");
} }
@@ -401,17 +357,13 @@ export async function prepareRemoteQueryRun(
const actionBranch = getActionBranch(); const actionBranch = getActionBranch();
const queryStartTime = Date.now(); const queryStartTime = Date.now();
const queryMetadata = await tryGetQueryMetadata(cliServer, queryFile);
return { return {
actionBranch, actionBranch,
base64Pack, base64Pack,
repoSelection, repoSelection,
queryFile,
queryMetadata,
controllerRepo, controllerRepo,
queryStartTime, queryStartTime,
language,
}; };
} }
@@ -426,26 +378,26 @@ export async function prepareRemoteQueryRun(
* - Removes any `${workspace}` version references from the qlpack.yml or codeql-pack.yml file. Converts them * - Removes any `${workspace}` version references from the qlpack.yml or codeql-pack.yml file. Converts them
* to `*` versions. * to `*` versions.
* *
* @param queryPackDir The directory containing the query pack * @param targetPackPath The path to the directory containing the target pack
* @param packRelativePath The relative path to the query pack from the root of the query pack * @param qlPackDetails The details of the original QL pack
*/ */
async function fixPackFile( async function fixPackFile(
queryPackDir: string, targetPackPath: string,
packRelativePath: string, qlPackDetails: QlPackDetails,
): Promise<void> { ): Promise<void> {
const packPath = await getQlPackPath(queryPackDir); const packPath = await getQlPackFilePath(targetPackPath);
// This should not happen since we create the pack ourselves. // This should not happen since we create the pack ourselves.
if (!packPath) { if (!packPath) {
throw new Error( throw new Error(
`Could not find ${QLPACK_FILENAMES.join( `Could not find ${QLPACK_FILENAMES.join(
" or ", " or ",
)} file in '${queryPackDir}'`, )} file in '${targetPackPath}'`,
); );
} }
const qlpack = load(await readFile(packPath, "utf8")) as QlPackFile; const qlpack = load(await readFile(packPath, "utf8")) as QlPackFile;
updateDefaultSuite(qlpack, packRelativePath); updateDefaultSuite(qlpack, qlPackDetails);
removeWorkspaceRefs(qlpack); removeWorkspaceRefs(qlpack);
await writeFile(packPath, dump(qlpack)); await writeFile(packPath, dump(qlpack));
@@ -483,7 +435,7 @@ async function addExtensionPacksAsDependencies(
queryPackDir: string, queryPackDir: string,
extensionPacks: string[], extensionPacks: string[],
): Promise<void> { ): Promise<void> {
const qlpackFile = await getQlPackPath(queryPackDir); const qlpackFile = await getQlPackFilePath(queryPackDir);
if (!qlpackFile) { if (!qlpackFile) {
throw new Error( throw new Error(
`Could not find ${QLPACK_FILENAMES.join( `Could not find ${QLPACK_FILENAMES.join(
@@ -509,19 +461,23 @@ async function addExtensionPacksAsDependencies(
await writeFile(qlpackFile, dump(syntheticQueryPack)); await writeFile(qlpackFile, dump(syntheticQueryPack));
} }
function updateDefaultSuite(qlpack: QlPackFile, packRelativePath: string) { function updateDefaultSuite(qlpack: QlPackFile, qlPackDetails: QlPackDetails) {
delete qlpack.defaultSuiteFile; delete qlpack.defaultSuiteFile;
qlpack.defaultSuite = generateDefaultSuite(packRelativePath); qlpack.defaultSuite = generateDefaultSuite(qlPackDetails);
} }
function generateDefaultSuite(packRelativePath: string) { function generateDefaultSuite(qlPackDetails: QlPackDetails) {
const queries = qlPackDetails.queryFiles.map((query) => {
const relativePath = relative(qlPackDetails.qlPackRootPath, query);
return {
query: relativePath.replace(/\\/g, "/"),
};
});
return [ return [
{ {
description: "Query suite for variant analysis", description: "Query suite for variant analysis",
}, },
{ ...queries,
query: packRelativePath.replace(/\\/g, "/"),
},
]; ];
} }

View File

@@ -146,8 +146,8 @@ export function filterAndSortRepositoriesWithResults<
filterSortState.repositoryIds.length > 0 filterSortState.repositoryIds.length > 0
) { ) {
return repositories return repositories
.filter( .filter((repo) =>
(repo) => filterSortState.repositoryIds?.includes(repo.repository.id), filterSortState.repositoryIds?.includes(repo.repository.id),
) )
.sort(compareWithResults(filterSortState)); .sort(compareWithResults(filterSortState));
} }

View File

@@ -159,6 +159,7 @@ export interface VariantAnalysisSubmission {
// unclear what it will look like in the future. // unclear what it will look like in the future.
export interface VariantAnalysisQueries { export interface VariantAnalysisQueries {
language: QueryLanguage; language: QueryLanguage;
count: number;
} }
export async function isVariantAnalysisComplete( export async function isVariantAnalysisComplete(

View File

@@ -22,6 +22,7 @@ import { DisposableObject } from "../common/disposable-object";
import { VariantAnalysisMonitor } from "./variant-analysis-monitor"; import { VariantAnalysisMonitor } from "./variant-analysis-monitor";
import type { import type {
VariantAnalysis, VariantAnalysis,
VariantAnalysisQueries,
VariantAnalysisRepositoryTask, VariantAnalysisRepositoryTask,
VariantAnalysisScannedRepository, VariantAnalysisScannedRepository,
VariantAnalysisScannedRepositoryResult, VariantAnalysisScannedRepositoryResult,
@@ -87,7 +88,10 @@ import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
import { RequestError } from "@octokit/request-error"; import { RequestError } from "@octokit/request-error";
import { handleRequestError } from "./custom-errors"; import { handleRequestError } from "./custom-errors";
import { createMultiSelectionCommand } from "../common/vscode/selection-commands"; import { createMultiSelectionCommand } from "../common/vscode/selection-commands";
import { askForLanguage } from "../codeql-cli/query-language"; import { askForLanguage, findLanguage } from "../codeql-cli/query-language";
import type { QlPackDetails } from "./ql-pack-details";
import { findPackRoot, getQlPackFilePath } from "../common/ql";
import { tryGetQueryMetadata } from "../codeql-cli/query-metadata";
const maxRetryCount = 3; const maxRetryCount = 3;
@@ -191,26 +195,22 @@ export class VariantAnalysisManager
throw new Error("Please select a .ql file to run as a variant analysis"); throw new Error("Please select a .ql file to run as a variant analysis");
} }
await this.runVariantAnalysisCommand(fileUri); await this.runVariantAnalysisCommand([fileUri]);
} }
private async runVariantAnalysisFromContextEditor(uri: Uri) { private async runVariantAnalysisFromContextEditor(uri: Uri) {
await this.runVariantAnalysisCommand(uri); await this.runVariantAnalysisCommand([uri]);
} }
private async runVariantAnalysisFromExplorer(fileURIs: Uri[]): Promise<void> { private async runVariantAnalysisFromExplorer(fileURIs: Uri[]): Promise<void> {
if (fileURIs.length !== 1) { return this.runVariantAnalysisCommand(fileURIs);
throw new Error("Can only run a single query at a time");
}
return this.runVariantAnalysisCommand(fileURIs[0]);
} }
private async runVariantAnalysisFromQueriesPanel( private async runVariantAnalysisFromQueriesPanel(
queryTreeViewItem: QueryTreeViewItem, queryTreeViewItem: QueryTreeViewItem,
): Promise<void> { ): Promise<void> {
if (queryTreeViewItem.path !== undefined) { if (queryTreeViewItem.path !== undefined) {
await this.runVariantAnalysisCommand(Uri.file(queryTreeViewItem.path)); await this.runVariantAnalysisCommand([Uri.file(queryTreeViewItem.path)]);
} }
} }
@@ -223,6 +223,9 @@ export class VariantAnalysisManager
}); });
const language = await askForLanguage(this.cliServer); const language = await askForLanguage(this.cliServer);
if (!language) {
return;
}
progress({ progress({
maxStep: 8, maxStep: 8,
@@ -263,8 +266,18 @@ export class VariantAnalysisManager
return; return;
} }
const qlPackFilePath = await getQlPackFilePath(packDir);
// Build up details to pass to the functions that run the variant analysis.
const qlPackDetails: QlPackDetails = {
queryFiles: problemQueries,
qlPackRootPath: packDir,
qlPackFilePath,
language,
};
await this.runVariantAnalysis( await this.runVariantAnalysis(
problemQueries.map((q) => Uri.file(q)), qlPackDetails,
(p) => (p) =>
progress({ progress({
...p, ...p,
@@ -294,10 +307,43 @@ export class VariantAnalysisManager
return problemQueries; return problemQueries;
} }
private async runVariantAnalysisCommand(uri: Uri): Promise<void> { private async runVariantAnalysisCommand(queryFiles: Uri[]): Promise<void> {
if (queryFiles.length === 0) {
throw new Error("Please select a .ql file to run as a variant analysis");
}
const qlPackRootPath = await findPackRoot(queryFiles[0].fsPath);
const qlPackFilePath = await getQlPackFilePath(qlPackRootPath);
// Make sure that all remaining queries have the same pack root
for (let i = 1; i < queryFiles.length; i++) {
const packRoot = await findPackRoot(queryFiles[i].fsPath);
if (packRoot !== qlPackRootPath) {
throw new Error(
"Please select queries that all belong to the same query pack",
);
}
}
// Open popup to ask for language if not already hardcoded
const language = qlPackFilePath
? await findLanguage(this.cliServer, queryFiles[0])
: await askForLanguage(this.cliServer);
if (!language) {
throw new UserCancellationException("Could not determine query language");
}
const qlPackDetails: QlPackDetails = {
queryFiles: queryFiles.map((uri) => uri.fsPath),
qlPackRootPath,
qlPackFilePath,
language,
};
return withProgress( return withProgress(
async (progress, token) => { async (progress, token) => {
await this.runVariantAnalysis([uri], progress, token); await this.runVariantAnalysis(qlPackDetails, progress, token);
}, },
{ {
title: "Run Variant Analysis", title: "Run Variant Analysis",
@@ -307,7 +353,7 @@ export class VariantAnalysisManager
} }
public async runVariantAnalysis( public async runVariantAnalysis(
uris: Uri[], qlPackDetails: QlPackDetails,
progress: ProgressCallback, progress: ProgressCallback,
token: CancellationToken, token: CancellationToken,
): Promise<void> { ): Promise<void> {
@@ -323,35 +369,43 @@ export class VariantAnalysisManager
actionBranch, actionBranch,
base64Pack, base64Pack,
repoSelection, repoSelection,
queryFile,
queryMetadata,
controllerRepo, controllerRepo,
queryStartTime, queryStartTime,
language,
} = await prepareRemoteQueryRun( } = await prepareRemoteQueryRun(
this.cliServer, this.cliServer,
this.app.credentials, this.app.credentials,
uris, qlPackDetails,
progress, progress,
token, token,
this.dbManager, this.dbManager,
); );
const queryName = getQueryName(queryMetadata, queryFile); // For now we get the metadata for the first query in the pack.
const variantAnalysisLanguage = parseVariantAnalysisQueryLanguage(language); // and use that in the submission and query history. In the future
// we'll need to consider how to handle having multiple queries.
const firstQueryFile = qlPackDetails.queryFiles[0];
const queryMetadata = await tryGetQueryMetadata(
this.cliServer,
firstQueryFile,
);
const queryName = getQueryName(queryMetadata, firstQueryFile);
const variantAnalysisLanguage = parseVariantAnalysisQueryLanguage(
qlPackDetails.language,
);
if (variantAnalysisLanguage === undefined) { if (variantAnalysisLanguage === undefined) {
throw new UserCancellationException( throw new UserCancellationException(
`Found unsupported language: ${language}`, `Found unsupported language: ${qlPackDetails.language}`,
); );
} }
const queryText = await readFile(queryFile, "utf8"); const queryText = await readFile(firstQueryFile, "utf8");
const queries = const queries: VariantAnalysisQueries | undefined =
uris.length === 1 qlPackDetails.queryFiles.length === 1
? undefined ? undefined
: { : {
language: variantAnalysisLanguage, language: qlPackDetails.language,
count: qlPackDetails.queryFiles.length,
}; };
const variantAnalysisSubmission: VariantAnalysisSubmission = { const variantAnalysisSubmission: VariantAnalysisSubmission = {
@@ -360,7 +414,7 @@ export class VariantAnalysisManager
controllerRepoId: controllerRepo.id, controllerRepoId: controllerRepo.id,
query: { query: {
name: queryName, name: queryName,
filePath: queryFile, filePath: firstQueryFile,
pack: base64Pack, pack: base64Pack,
language: variantAnalysisLanguage, language: variantAnalysisLanguage,
text: queryText, text: queryText,

View File

@@ -218,9 +218,15 @@ export class VariantAnalysisView
} }
private getTitle(variantAnalysis: VariantAnalysis | undefined): string { private getTitle(variantAnalysis: VariantAnalysis | undefined): string {
return variantAnalysis if (!variantAnalysis) {
? `${variantAnalysis.query.name} - Variant Analysis Results` return `Variant Analysis ${this.variantAnalysisId} - Results`;
: `Variant Analysis ${this.variantAnalysisId} - Results`; }
if (variantAnalysis.queries) {
return `Variant Analysis using multiple queries - Results`;
} else {
return `${variantAnalysis.query.name} - Variant Analysis Results`;
}
} }
private async showDataFlows(dataFlows: DataFlowPaths): Promise<void> { private async showDataFlows(dataFlows: DataFlowPaths): Promise<void> {

View File

@@ -25,7 +25,7 @@ const Message = styled.div`
padding: 1.5rem; padding: 1.5rem;
`; `;
export function Compare(_: Record<string, never>): JSX.Element { export function Compare(_: Record<string, never>): React.JSX.Element {
const [queryInfo, setQueryInfo] = const [queryInfo, setQueryInfo] =
useState<SetComparisonQueryInfoMessage | null>(null); useState<SetComparisonQueryInfoMessage | null>(null);
const [comparison, setComparison] = useState<SetComparisonsMessage | null>( const [comparison, setComparison] = useState<SetComparisonsMessage | null>(

View File

@@ -32,7 +32,7 @@ export const DataFlowPaths = ({
dataFlowPaths, dataFlowPaths,
}: { }: {
dataFlowPaths: DataFlowPathsDomainModel; dataFlowPaths: DataFlowPathsDomainModel;
}): JSX.Element => { }): React.JSX.Element => {
const [selectedCodeFlow, setSelectedCodeFlow] = useState( const [selectedCodeFlow, setSelectedCodeFlow] = useState(
dataFlowPaths.codeFlows[0], dataFlowPaths.codeFlows[0],
); );

View File

@@ -9,7 +9,7 @@ export type DataFlowPathsViewProps = {
export function DataFlowPathsView({ export function DataFlowPathsView({
dataFlowPaths: initialDataFlowPaths, dataFlowPaths: initialDataFlowPaths,
}: DataFlowPathsViewProps): JSX.Element { }: DataFlowPathsViewProps): React.JSX.Element {
const [dataFlowPaths, setDataFlowPaths] = useState< const [dataFlowPaths, setDataFlowPaths] = useState<
DataFlowPathsDomainModel | undefined DataFlowPathsDomainModel | undefined
>(initialDataFlowPaths); >(initialDataFlowPaths);

View File

@@ -64,7 +64,7 @@ export const MethodModeling = ({
method, method,
isModelingInProgress, isModelingInProgress,
onChange, onChange,
}: MethodModelingProps): JSX.Element => { }: MethodModelingProps): React.JSX.Element => {
return ( return (
<Container> <Container>
<Title> <Title>

View File

@@ -39,7 +39,7 @@ export const MethodModelingInputs = ({
modelingStatus, modelingStatus,
isModelingInProgress, isModelingInProgress,
onChange, onChange,
}: MethodModelingInputsProps): JSX.Element => { }: MethodModelingInputsProps): React.JSX.Element => {
const inputProps = { const inputProps = {
language, language,
method, method,

View File

@@ -16,7 +16,9 @@ type Props = {
initialViewState?: MethodModelingPanelViewState; initialViewState?: MethodModelingPanelViewState;
}; };
export function MethodModelingView({ initialViewState }: Props): JSX.Element { export function MethodModelingView({
initialViewState,
}: Props): React.JSX.Element {
const [viewState, setViewState] = useState< const [viewState, setViewState] = useState<
MethodModelingPanelViewState | undefined MethodModelingPanelViewState | undefined
>(initialViewState); >(initialViewState);

View File

@@ -22,7 +22,7 @@ const TypeMethodName = (method: Method) => {
); );
}; };
export const MethodName = (method: Method): JSX.Element => { export const MethodName = (method: Method): React.JSX.Element => {
return ( return (
<Name> <Name>
{method.packageName && <>{method.packageName}.</>} {method.packageName && <>{method.packageName}.</>}

View File

@@ -85,7 +85,7 @@ export function ModelEditor({
initialMethods = [], initialMethods = [],
initialModeledMethods = {}, initialModeledMethods = {},
initialHideModeledMethods = INITIAL_HIDE_MODELED_METHODS_VALUE, initialHideModeledMethods = INITIAL_HIDE_MODELED_METHODS_VALUE,
}: Props): JSX.Element { }: Props): React.JSX.Element {
const [viewState, setViewState] = useState<ModelEditorViewState | undefined>( const [viewState, setViewState] = useState<ModelEditorViewState | undefined>(
initialViewState, initialViewState,
); );

View File

@@ -27,7 +27,7 @@ export const ModelInputDropdown = ({
modeledMethod, modeledMethod,
modelingStatus, modelingStatus,
onChange, onChange,
}: Props): JSX.Element => { }: Props): React.JSX.Element => {
const options = useMemo(() => { const options = useMemo(() => {
const modelsAsDataLanguage = getModelsAsDataLanguage(language); const modelsAsDataLanguage = getModelsAsDataLanguage(language);

View File

@@ -27,7 +27,7 @@ export const ModelOutputDropdown = ({
modeledMethod, modeledMethod,
modelingStatus, modelingStatus,
onChange, onChange,
}: Props): JSX.Element => { }: Props): React.JSX.Element => {
const options = useMemo(() => { const options = useMemo(() => {
const modelsAsDataLanguage = getModelsAsDataLanguage(language); const modelsAsDataLanguage = getModelsAsDataLanguage(language);

View File

@@ -31,7 +31,7 @@ export const ModelTypeDropdown = ({
modeledMethod, modeledMethod,
modelingStatus, modelingStatus,
onChange, onChange,
}: Props): JSX.Element => { }: Props): React.JSX.Element => {
const options = useMemo(() => { const options = useMemo(() => {
const baseOptions: Array<{ value: ModeledMethodType; label: string }> = [ const baseOptions: Array<{ value: ModeledMethodType; label: string }> = [
{ value: "none", label: "Unmodeled" }, { value: "none", label: "Unmodeled" },

View File

@@ -15,12 +15,16 @@ type Props = {
"aria-label"?: string; "aria-label"?: string;
}; };
const stopClickPropagation = (e: React.MouseEvent) => {
e.stopPropagation();
};
export const ModelTypeTextbox = ({ export const ModelTypeTextbox = ({
modeledMethod, modeledMethod,
typeInfo, typeInfo,
onChange, onChange,
...props ...props
}: Props): JSX.Element => { }: Props): React.JSX.Element => {
const [value, setValue] = useState<string | undefined>( const [value, setValue] = useState<string | undefined>(
modeledMethod[typeInfo], modeledMethod[typeInfo],
); );
@@ -48,5 +52,12 @@ export const ModelTypeTextbox = ({
500, 500,
); );
return <VSCodeTextField value={value} onInput={handleChange} {...props} />; return (
<VSCodeTextField
value={value}
onInput={handleChange}
onClick={stopClickPropagation}
{...props}
/>
);
}; };

View File

@@ -6,7 +6,7 @@ interface Props {
showRawResults: () => void; showRawResults: () => void;
} }
export function AlertTableNoResults(props: Props): JSX.Element { export function AlertTableNoResults(props: Props): React.JSX.Element {
if (props.nonemptyRawResults) { if (props.nonemptyRawResults) {
return ( return (
<span> <span>

View File

@@ -2,7 +2,9 @@ interface Props {
numTruncatedResults: number; numTruncatedResults: number;
} }
export function AlertTableTruncatedMessage(props: Props): JSX.Element | null { export function AlertTableTruncatedMessage(
props: Props,
): React.JSX.Element | null {
if (props.numTruncatedResults === 0) { if (props.numTruncatedResults === 0) {
return null; return null;
} }

View File

@@ -18,7 +18,7 @@ const Container = styled.span`
text-align: center; text-align: center;
`; `;
export function EmptyQueryResultsMessage(): JSX.Element { export function EmptyQueryResultsMessage(): React.JSX.Element {
return ( return (
<Root> <Root>
<Container> <Container>

View File

@@ -10,7 +10,7 @@ interface Props {
handleCheckboxChanged: (event: React.ChangeEvent<HTMLInputElement>) => void; handleCheckboxChanged: (event: React.ChangeEvent<HTMLInputElement>) => void;
} }
export function ProblemsViewCheckbox(props: Props): JSX.Element | null { export function ProblemsViewCheckbox(props: Props): React.JSX.Element | null {
const { selectedTable, problemsViewSelected, handleCheckboxChanged } = props; const { selectedTable, problemsViewSelected, handleCheckboxChanged } = props;
if (selectedTable !== ALERTS_TABLE_NAME) { if (selectedTable !== ALERTS_TABLE_NAME) {

View File

@@ -13,7 +13,7 @@ export default function RawTableValue({
value, value,
databaseUri, databaseUri,
onSelected, onSelected,
}: Props): JSX.Element { }: Props): React.JSX.Element {
switch (value.type) { switch (value.type) {
case "boolean": case "boolean":
return <span>{value.value.toString()}</span>; return <span>{value.value.toString()}</span>;

View File

@@ -14,7 +14,7 @@ function getResultCount(resultSet: ResultSet): number {
} }
} }
export function ResultCount(props: Props): JSX.Element | null { export function ResultCount(props: Props): React.JSX.Element | null {
if (!props.resultSet) { if (!props.resultSet) {
return null; return null;
} }

View File

@@ -23,7 +23,7 @@ export function ClickableLocation({
label, label,
databaseUri, databaseUri,
onClick: onClick, onClick: onClick,
}: Props): JSX.Element { }: Props): React.JSX.Element {
const handleClick = useCallback( const handleClick = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();

View File

@@ -22,7 +22,7 @@ export function Location({
databaseUri, databaseUri,
title, title,
onClick, onClick,
}: Props): JSX.Element { }: Props): React.JSX.Element {
const displayLabel = useMemo(() => convertNonPrintableChars(label), [label]); const displayLabel = useMemo(() => convertNonPrintableChars(label), [label]);
if (loc === undefined) { if (loc === undefined) {

View File

@@ -51,7 +51,7 @@ export function VariantAnalysis({
variantAnalysis: initialVariantAnalysis, variantAnalysis: initialVariantAnalysis,
repoStates: initialRepoStates = [], repoStates: initialRepoStates = [],
repoResults: initialRepoResults = [], repoResults: initialRepoResults = [],
}: VariantAnalysisProps): JSX.Element { }: VariantAnalysisProps): React.JSX.Element {
const [variantAnalysis, setVariantAnalysis] = useState< const [variantAnalysis, setVariantAnalysis] = useState<
VariantAnalysisDomainModel | undefined VariantAnalysisDomainModel | undefined
>(initialVariantAnalysis); >(initialVariantAnalysis);

View File

@@ -21,6 +21,7 @@ import {
defaultFilterSortState, defaultFilterSortState,
filterAndSortRepositoriesWithResults, filterAndSortRepositoriesWithResults,
} from "../../variant-analysis/shared/variant-analysis-filter-sort"; } from "../../variant-analysis/shared/variant-analysis-filter-sort";
import { ViewTitle } from "../common";
type VariantAnalysisHeaderProps = { type VariantAnalysisHeaderProps = {
variantAnalysis: VariantAnalysis; variantAnalysis: VariantAnalysis;
@@ -50,6 +51,29 @@ const Row = styled.div`
align-items: center; align-items: center;
`; `;
const QueryInfo = ({
variantAnalysis,
onOpenQueryFileClick,
onViewQueryTextClick,
}: {
variantAnalysis: VariantAnalysis;
onOpenQueryFileClick: () => void;
onViewQueryTextClick: () => void;
}) => {
if (variantAnalysis.queries) {
return <ViewTitle>{variantAnalysis.queries?.count} queries</ViewTitle>;
} else {
return (
<QueryDetails
queryName={variantAnalysis.query.name}
queryFileName={basename(variantAnalysis.query.filePath)}
onOpenQueryFileClick={onOpenQueryFileClick}
onViewQueryTextClick={onViewQueryTextClick}
/>
);
}
};
export const VariantAnalysisHeader = ({ export const VariantAnalysisHeader = ({
variantAnalysis, variantAnalysis,
repositoryStates, repositoryStates,
@@ -117,9 +141,8 @@ export const VariantAnalysisHeader = ({
return ( return (
<Container> <Container>
<Row> <Row>
<QueryDetails <QueryInfo
queryName={variantAnalysis.query.name} variantAnalysis={variantAnalysis}
queryFileName={basename(variantAnalysis.query.filePath)}
onOpenQueryFileClick={onOpenQueryFileClick} onOpenQueryFileClick={onOpenQueryFileClick}
onViewQueryTextClick={onViewQueryTextClick} onViewQueryTextClick={onViewQueryTextClick}
/> />

View File

@@ -1,3 +1,3 @@
export type WebviewDefinition = { export type WebviewDefinition = {
component: JSX.Element; component: React.JSX.Element;
}; };

View File

@@ -1,5 +1,5 @@
[ [
"v2.16.0", "v2.16.1",
"v2.15.5", "v2.15.5",
"v2.14.6", "v2.14.6",
"v2.13.5", "v2.13.5",

View File

@@ -0,0 +1,20 @@
## VS Code CodeQL E2E Tests
When running the tests locally on a mac a different processor has to be emulated, which makes everythign VERY slow. Therefore we need to add higher timeouts in the test, so that they pass locally.
### How to use locally
Setup
- install playwright if you haven't yet (`npx playwright install`)
- go to the e2e test folder on your terminal
- make sure docker is running
- run `docker-compose build`
- run `docker-compose up`
Run tests
- run `npx playwright test --ui` from the e2e test folder to follow the test while it's running. This UI has a 'locator' tool with which elements on the test screen can be found
- use `npx playwright test --debug` to follow the test in real time and interact with the interface, e.g. press enter or input into fields, stop and start
During the test elements are created in the docker volume, e.g. the downloaded database or query data. This might interfer with other tests or when running a test twice. If that happens restart your docker volume by using `docker-compose down -v` and `docker-compose up`. Sometimes already existing queries from former runs change the input the extension needs.

View File

@@ -0,0 +1,55 @@
version: "3.8"
services:
code-server:
build:
context: docker
dockerfile: Dockerfile
platform: linux/amd64
container_name: code-server
user: "1000"
volumes:
- local-data:/home/coder/.local/share/code-server
- local-user-data:/home/coder/.local/share/code-server/User
- ./docker/config/config.yaml:/home/coder/.config/code-server/config.yaml
- ./docker/User/settings.json:/home/coder/.local/share/code-server/User/settings.json
- project-data:/home/coder/project
ports:
- 8080:8080
restart: unless-stopped
depends_on:
code-server-init:
condition: service_completed_successfully
code-server-init:
build:
context: docker
dockerfile: Dockerfile
platform: linux/amd64
user: "1000"
volumes:
- local-data:/home/coder/.local/share/code-server
- local-user-data:/home/coder/.local/share/code-server/User
- ./docker/config/config.yaml:/home/coder/.config/code-server/config.yaml
- ./docker/User/settings.json:/home/coder/.local/share/code-server/User/settings.json
- project-data:/home/coder/project
entrypoint: |
/usr/bin/entrypoint.sh --install-extension GitHub.vscode-codeql
restart: "no"
depends_on:
- files-init
files-init:
image: alpine:3.19.0
restart: "no"
# Since we're not running the code-server container using the same user as our host user,
# we need to set the permissions on the mounted volumes to match the user inside the container.
entrypoint: |
/bin/sh -c "chown 1000:1000 /home/coder/.local/share/code-server /home/coder/.local/share/code-server/User /home/coder/project"
volumes:
- local-data:/home/coder/.local/share/code-server
- local-user-data:/home/coder/.local/share/code-server/User
- project-data:/home/coder/project
volumes:
local-data:
local-user-data:
project-data:

View File

@@ -0,0 +1,16 @@
FROM codercom/code-server:4.20.0
USER root
RUN apt-get update \
&& apt-get install -y \
unzip \
&& rm -rf /var/lib/apt/lists/*
RUN wget -q -O /tmp/codeql.zip https://github.com/github/codeql-cli-binaries/releases/download/v2.15.5/codeql-linux64.zip \
&& unzip -q /tmp/codeql.zip -d /opt \
&& rm -rf /tmp/codeql.zip
ENV PATH="/opt/codeql:${PATH}"
USER 1000

View File

@@ -0,0 +1,6 @@
{
"workbench.startupEditor": "none",
"security.workspace.trust.enabled": false,
"codeQL.cli.executablePath": "/opt/codeql/codeql",
"codeQL.telemetry.enableTelemetry": false
}

View File

@@ -0,0 +1,6 @@
bind-addr: 127.0.0.1:8080
auth: none
cert: false
disable-workspace-trust: true
disable-telemetry: true
disable-update-check: true

View File

@@ -0,0 +1,36 @@
import { defineConfig, devices } from "@playwright/test";
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: ".",
timeout: 5 * 60 * 1000,
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:8080",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
});

View File

@@ -0,0 +1,79 @@
import { test, expect } from "@playwright/test";
test("run query and open it from history", async ({ page }) => {
await page.goto("/?folder=/home/coder/project");
await page.getByRole("tab", { name: "CodeQL" }).locator("a").click();
// decline extension telemetry
await page.getByRole("button", { name: "No", exact: true }).click({
timeout: 60000,
});
await page.keyboard.press("Control+Shift+P");
await page.keyboard.type("Create Query");
await page.keyboard.press("Enter");
await page.getByLabel("JavaScript, javascript").locator("a").click({
timeout: 60000,
});
// select folder for first query
await page
.getByText(
"Results0 SelectedPress 'Enter' to confirm your input or 'Escape' to cancelOK",
)
.press("Enter");
// download database
await page
.getByRole("button", { name: "Download database" })
.click({ timeout: 60000 });
await page.getByPlaceholder("https://github.com/<owner>/<").press("Enter");
await page
.locator("#list_id_3_0")
.getByText("javascript")
.click({ timeout: 60000 });
await page.keyboard.press("Control+Shift+P");
await page.keyboard.type("Run Query on selected");
await page.keyboard.press("Enter");
// check if results page is visible
await expect(page.getByText("CodeQL Query Results")).toBeVisible({
timeout: 600000,
});
// wait for query history item to be finished
await expect(
page
.locator("#list_id_6_0")
.getByLabel("Hello world on d3/d3 -")
.locator("div")
.first(),
).toBeVisible({ timeout: 60000 });
// close results page and open query from history
await page
.getByLabel("CodeQL Query Results, Editor Group")
.getByLabel("Close (Ctrl+F4)")
.click();
await expect(
page
.frameLocator(".webview")
.frameLocator('iframe[title="CodeQL Query Results"]')
.getByText("#selectalerts32 resultsShow"),
).not.toBeVisible();
await page
.locator("#list_id_6_0")
.getByLabel("Hello world on d3/d3 -")
.locator("div")
.first()
.click();
await expect(
page.getByLabel("CodeQL Query Results", { exact: true }).locator("div"),
).toBeVisible({ timeout: 60000 });
});

View File

@@ -115,7 +115,7 @@ describe("Releases API consumer", () => {
await expect( await expect(
consumer.getLatestRelease(new Range("5.*.*")), consumer.getLatestRelease(new Range("5.*.*")),
).rejects.toThrowError(); ).rejects.toThrow();
}); });
it("picked release passes additional compatibility test if an additional compatibility test is specified", async () => { it("picked release passes additional compatibility test if an additional compatibility test is specified", async () => {
@@ -140,7 +140,7 @@ describe("Releases API consumer", () => {
(asset) => asset.name === "otherExampleAsset.txt", (asset) => asset.name === "otherExampleAsset.txt",
), ),
), ),
).rejects.toThrowError(); ).rejects.toThrow();
}); });
it("picked release is the most recent prerelease when includePrereleases is set", async () => { it("picked release is the most recent prerelease when includePrereleases is set", async () => {

View File

@@ -2,9 +2,9 @@ import { join } from "path";
import { dirSync } from "tmp-promise"; import { dirSync } from "tmp-promise";
import type { DirResult } from "tmp"; import type { DirResult } from "tmp";
import { writeFile } from "fs-extra"; import { writeFile } from "fs-extra";
import { getQlPackPath } from "../../../src/common/ql"; import { getQlPackFilePath } from "../../../src/common/ql";
describe("getQlPackPath", () => { describe("getQlPackFilePath", () => {
let tmpDir: DirResult; let tmpDir: DirResult;
beforeEach(() => { beforeEach(() => {
@@ -22,14 +22,14 @@ describe("getQlPackPath", () => {
it("should find a qlpack.yml when it exists", async () => { it("should find a qlpack.yml when it exists", async () => {
await writeFile(join(tmpDir.name, "qlpack.yml"), "name: test"); await writeFile(join(tmpDir.name, "qlpack.yml"), "name: test");
const result = await getQlPackPath(tmpDir.name); const result = await getQlPackFilePath(tmpDir.name);
expect(result).toEqual(join(tmpDir.name, "qlpack.yml")); expect(result).toEqual(join(tmpDir.name, "qlpack.yml"));
}); });
it("should find a codeql-pack.yml when it exists", async () => { it("should find a codeql-pack.yml when it exists", async () => {
await writeFile(join(tmpDir.name, "codeql-pack.yml"), "name: test"); await writeFile(join(tmpDir.name, "codeql-pack.yml"), "name: test");
const result = await getQlPackPath(tmpDir.name); const result = await getQlPackFilePath(tmpDir.name);
expect(result).toEqual(join(tmpDir.name, "codeql-pack.yml")); expect(result).toEqual(join(tmpDir.name, "codeql-pack.yml"));
}); });
@@ -37,12 +37,12 @@ describe("getQlPackPath", () => {
await writeFile(join(tmpDir.name, "qlpack.yml"), "name: test"); await writeFile(join(tmpDir.name, "qlpack.yml"), "name: test");
await writeFile(join(tmpDir.name, "codeql-pack.yml"), "name: test"); await writeFile(join(tmpDir.name, "codeql-pack.yml"), "name: test");
const result = await getQlPackPath(tmpDir.name); const result = await getQlPackFilePath(tmpDir.name);
expect(result).toEqual(join(tmpDir.name, "qlpack.yml")); expect(result).toEqual(join(tmpDir.name, "qlpack.yml"));
}); });
it("should find nothing when it doesn't exist", async () => { it("should find nothing when it doesn't exist", async () => {
const result = await getQlPackPath(tmpDir.name); const result = await getQlPackFilePath(tmpDir.name);
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
}); });
}); });

View File

@@ -154,7 +154,7 @@ describe(VariantAnalysisResultsManager.name, () => {
async function* generateInParts() { async function* generateInParts() {
const partLength = fileContents.length / 5; const partLength = fileContents.length / 5;
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
yield fileContents.slice(i * partLength, (i + 1) * partLength); yield fileContents.subarray(i * partLength, (i + 1) * partLength);
} }
} }

View File

@@ -1,4 +1,4 @@
import { CancellationTokenSource, commands, Uri, window } from "vscode"; import { CancellationTokenSource, commands, window, Uri } from "vscode";
import { extLogger } from "../../../../src/common/logging/vscode"; import { extLogger } from "../../../../src/common/logging/vscode";
import { setRemoteControllerRepo } from "../../../../src/config"; import { setRemoteControllerRepo } from "../../../../src/config";
import * as ghApiClient from "../../../../src/variant-analysis/gh-api/gh-api-client"; import * as ghApiClient from "../../../../src/variant-analysis/gh-api/gh-api-client";
@@ -26,6 +26,7 @@ import type { ExtensionPackMetadata } from "../../../../src/model-editor/extensi
import type { QlPackLockFile } from "../../../../src/packaging/qlpack-lock-file"; import type { QlPackLockFile } from "../../../../src/packaging/qlpack-lock-file";
//import { expect } from "@jest/globals"; //import { expect } from "@jest/globals";
import "../../../matchers/toExistInCodeQLPack"; import "../../../matchers/toExistInCodeQLPack";
import type { QlPackDetails } from "../../../../src/variant-analysis/ql-pack-details";
describe("Variant Analysis Manager", () => { describe("Variant Analysis Manager", () => {
let cli: CodeQLCliServer; let cli: CodeQLCliServer;
@@ -99,10 +100,18 @@ describe("Variant Analysis Manager", () => {
}); });
it("should run a variant analysis that is part of a qlpack", async () => { it("should run a variant analysis that is part of a qlpack", async () => {
const fileUri = getFile("data-remote-qlpack/in-pack.ql"); const filePath = getFileOrDir("data-remote-qlpack/in-pack.ql");
const qlPackRootPath = getFileOrDir("data-remote-qlpack");
const qlPackFilePath = getFileOrDir("data-remote-qlpack/qlpack.yml");
const qlPackDetails: QlPackDetails = {
queryFiles: [filePath],
qlPackRootPath,
qlPackFilePath,
language: QueryLanguage.Javascript,
};
await variantAnalysisManager.runVariantAnalysis( await variantAnalysisManager.runVariantAnalysis(
[fileUri], qlPackDetails,
progress, progress,
cancellationTokenSource.token, cancellationTokenSource.token,
); );
@@ -120,10 +129,17 @@ describe("Variant Analysis Manager", () => {
}); });
it("should run a remote query that is not part of a qlpack", async () => { it("should run a remote query that is not part of a qlpack", async () => {
const fileUri = getFile("data-remote-no-qlpack/in-pack.ql"); const filePath = getFileOrDir("data-remote-no-qlpack/in-pack.ql");
const qlPackRootPath = getFileOrDir("data-remote-no-qlpack");
const qlPackDetails: QlPackDetails = {
queryFiles: [filePath],
qlPackRootPath,
qlPackFilePath: undefined,
language: QueryLanguage.Javascript,
};
await variantAnalysisManager.runVariantAnalysis( await variantAnalysisManager.runVariantAnalysis(
[fileUri], qlPackDetails,
progress, progress,
cancellationTokenSource.token, cancellationTokenSource.token,
); );
@@ -141,10 +157,22 @@ describe("Variant Analysis Manager", () => {
}); });
it("should run a remote query that is nested inside a qlpack", async () => { it("should run a remote query that is nested inside a qlpack", async () => {
const fileUri = getFile("data-remote-qlpack-nested/subfolder/in-pack.ql"); const filePath = getFileOrDir(
"data-remote-qlpack-nested/subfolder/in-pack.ql",
);
const qlPackRootPath = getFileOrDir("data-remote-qlpack-nested");
const qlPackFilePath = getFileOrDir(
"data-remote-qlpack-nested/codeql-pack.yml",
);
const qlPackDetails: QlPackDetails = {
queryFiles: [filePath],
qlPackRootPath,
qlPackFilePath,
language: QueryLanguage.Javascript,
};
await variantAnalysisManager.runVariantAnalysis( await variantAnalysisManager.runVariantAnalysis(
[fileUri], qlPackDetails,
progress, progress,
cancellationTokenSource.token, cancellationTokenSource.token,
); );
@@ -162,10 +190,17 @@ describe("Variant Analysis Manager", () => {
}); });
it("should cancel a run before uploading", async () => { it("should cancel a run before uploading", async () => {
const fileUri = getFile("data-remote-no-qlpack/in-pack.ql"); const filePath = getFileOrDir("data-remote-no-qlpack/in-pack.ql");
const qlPackRootPath = getFileOrDir("data-remote-no-qlpack");
const qlPackDetails: QlPackDetails = {
queryFiles: [filePath],
qlPackRootPath,
qlPackFilePath: undefined,
language: QueryLanguage.Javascript,
};
const promise = variantAnalysisManager.runVariantAnalysis( const promise = variantAnalysisManager.runVariantAnalysis(
[fileUri], qlPackDetails,
progress, progress,
cancellationTokenSource.token, cancellationTokenSource.token,
); );
@@ -202,6 +237,8 @@ describe("Variant Analysis Manager", () => {
it("should run a remote query that is part of a qlpack", async () => { it("should run a remote query that is part of a qlpack", async () => {
await doVariantAnalysisTest({ await doVariantAnalysisTest({
queryPath: "data-remote-qlpack/in-pack.ql", queryPath: "data-remote-qlpack/in-pack.ql",
qlPackRootPath: "data-remote-qlpack",
qlPackFilePath: "data-remote-qlpack/qlpack.yml",
expectedPackName: "github/remote-query-pack", expectedPackName: "github/remote-query-pack",
filesThatExist: ["in-pack.ql", "lib.qll"], filesThatExist: ["in-pack.ql", "lib.qll"],
filesThatDoNotExist: [], filesThatDoNotExist: [],
@@ -212,6 +249,8 @@ describe("Variant Analysis Manager", () => {
it("should run a remote query that is not part of a qlpack", async () => { it("should run a remote query that is not part of a qlpack", async () => {
await doVariantAnalysisTest({ await doVariantAnalysisTest({
queryPath: "data-remote-no-qlpack/in-pack.ql", queryPath: "data-remote-no-qlpack/in-pack.ql",
qlPackRootPath: "data-remote-no-qlpack",
qlPackFilePath: undefined,
expectedPackName: "codeql-remote/query", expectedPackName: "codeql-remote/query",
filesThatExist: ["in-pack.ql"], filesThatExist: ["in-pack.ql"],
filesThatDoNotExist: ["lib.qll", "not-in-pack.ql"], filesThatDoNotExist: ["lib.qll", "not-in-pack.ql"],
@@ -222,6 +261,8 @@ describe("Variant Analysis Manager", () => {
it("should run a remote query that is nested inside a qlpack", async () => { it("should run a remote query that is nested inside a qlpack", async () => {
await doVariantAnalysisTest({ await doVariantAnalysisTest({
queryPath: "data-remote-qlpack-nested/subfolder/in-pack.ql", queryPath: "data-remote-qlpack-nested/subfolder/in-pack.ql",
qlPackRootPath: "data-remote-qlpack-nested",
qlPackFilePath: "data-remote-qlpack-nested/codeql-pack.yml",
expectedPackName: "github/remote-query-pack", expectedPackName: "github/remote-query-pack",
filesThatExist: ["subfolder/in-pack.ql", "otherfolder/lib.qll"], filesThatExist: ["subfolder/in-pack.ql", "otherfolder/lib.qll"],
filesThatDoNotExist: ["subfolder/not-in-pack.ql"], filesThatDoNotExist: ["subfolder/not-in-pack.ql"],
@@ -239,6 +280,8 @@ describe("Variant Analysis Manager", () => {
await cli.setUseExtensionPacks(true); await cli.setUseExtensionPacks(true);
await doVariantAnalysisTest({ await doVariantAnalysisTest({
queryPath: "data-remote-qlpack-nested/subfolder/in-pack.ql", queryPath: "data-remote-qlpack-nested/subfolder/in-pack.ql",
qlPackRootPath: "data-remote-qlpack-nested",
qlPackFilePath: "data-remote-qlpack-nested/codeql-pack.yml",
expectedPackName: "github/remote-query-pack", expectedPackName: "github/remote-query-pack",
filesThatExist: [ filesThatExist: [
"subfolder/in-pack.ql", "subfolder/in-pack.ql",
@@ -275,16 +318,23 @@ describe("Variant Analysis Manager", () => {
const queryToRun = const queryToRun =
"Security/CWE/CWE-020/ExternalAPIsUsedWithUntrustedData.ql"; "Security/CWE/CWE-020/ExternalAPIsUsedWithUntrustedData.ql";
const extraQuery = "Telemetry/ExtractorInformation.ql";
// Recent versions of the CLI don't preserve queries with extensible predicates in MRVA packs,
// because all the necessary info is in the `.packinfo` file.
const extraQueries =
(await cli.cliConstraints.preservesExtensiblePredicatesInMrvaPack())
? ["Telemetry/ExtractorInformation.ql"]
: [];
const qlPackRootPath = join(process.env.TEST_CODEQL_PATH, "java/ql/src");
const queryPath = join(qlPackRootPath, queryToRun);
const qlPackFilePath = join(qlPackRootPath, "qlpack.yml");
await doVariantAnalysisTest({ await doVariantAnalysisTest({
queryPath: join( queryPath,
process.env.TEST_CODEQL_PATH, qlPackRootPath,
"java/ql/src", qlPackFilePath,
queryToRun,
),
expectedPackName: "codeql/java-queries", expectedPackName: "codeql/java-queries",
filesThatExist: [queryToRun, extraQuery], filesThatExist: [queryToRun, ...extraQueries],
filesThatDoNotExist: [], filesThatDoNotExist: [],
qlxFilesThatExist: [], qlxFilesThatExist: [],
dependenciesToCheck: ["codeql/java-all"], dependenciesToCheck: ["codeql/java-all"],
@@ -295,6 +345,8 @@ describe("Variant Analysis Manager", () => {
async function doVariantAnalysisTest({ async function doVariantAnalysisTest({
queryPath, queryPath,
qlPackRootPath,
qlPackFilePath,
expectedPackName, expectedPackName,
filesThatExist, filesThatExist,
qlxFilesThatExist, qlxFilesThatExist,
@@ -306,6 +358,8 @@ describe("Variant Analysis Manager", () => {
checkVersion = true, checkVersion = true,
}: { }: {
queryPath: string; queryPath: string;
qlPackRootPath: string;
qlPackFilePath: string | undefined;
expectedPackName: string; expectedPackName: string;
filesThatExist: string[]; filesThatExist: string[];
qlxFilesThatExist: string[]; qlxFilesThatExist: string[];
@@ -313,9 +367,16 @@ describe("Variant Analysis Manager", () => {
dependenciesToCheck?: string[]; dependenciesToCheck?: string[];
checkVersion?: boolean; checkVersion?: boolean;
}) { }) {
const fileUri = getFile(queryPath); const filePath = getFileOrDir(queryPath);
const qlPackDetails: QlPackDetails = {
queryFiles: [filePath],
qlPackRootPath: getFileOrDir(qlPackRootPath),
qlPackFilePath: qlPackFilePath && getFileOrDir(qlPackFilePath),
language: QueryLanguage.Javascript,
};
await variantAnalysisManager.runVariantAnalysis( await variantAnalysisManager.runVariantAnalysis(
[fileUri], qlPackDetails,
progress, progress,
cancellationTokenSource.token, cancellationTokenSource.token,
); );
@@ -324,7 +385,7 @@ describe("Variant Analysis Manager", () => {
expect(executeCommandSpy).toHaveBeenCalledWith( expect(executeCommandSpy).toHaveBeenCalledWith(
"codeQL.monitorNewVariantAnalysis", "codeQL.monitorNewVariantAnalysis",
expect.objectContaining({ expect.objectContaining({
query: expect.objectContaining({ filePath: fileUri.fsPath }), query: expect.objectContaining({ filePath }),
}), }),
); );
@@ -390,16 +451,19 @@ describe("Variant Analysis Manager", () => {
); );
} }
function getFile(file: string): Uri { function getFileOrDir(path: string): string {
if (isAbsolute(file)) { // Use `Uri.file(path).fsPath` to make sure the path is in the correct format for the OS (i.e. forward/backward slashes).
return Uri.file(file); if (isAbsolute(path)) {
return Uri.file(path).fsPath;
} else { } else {
return Uri.file(join(baseDir, file)); return Uri.file(join(baseDir, path)).fsPath;
} }
} }
}); });
describe("runVariantAnalysisFromPublishedPack", () => { describe("runVariantAnalysisFromPublishedPack", () => {
// Temporarily disabling this until we add a way to receive multiple queries in the
// runVariantAnalysis function.
it("should download pack for correct language and identify problem queries", async () => { it("should download pack for correct language and identify problem queries", async () => {
const showQuickPickSpy = jest const showQuickPickSpy = jest
.spyOn(window, "showQuickPick") .spyOn(window, "showQuickPick")
@@ -419,15 +483,17 @@ describe("Variant Analysis Manager", () => {
expect(showQuickPickSpy).toHaveBeenCalledTimes(1); expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(runVariantAnalysisMock).toHaveBeenCalledTimes(1); expect(runVariantAnalysisMock).toHaveBeenCalledTimes(1);
const queries: Uri[] = runVariantAnalysisMock.mock.calls[0][0]; console.log(runVariantAnalysisMock.mock.calls[0][0]);
const queries: string[] =
runVariantAnalysisMock.mock.calls[0][0].queryFiles;
// Should include queries. Just check that at least one known query exists. // Should include queries. Just check that at least one known query exists.
// It doesn't particularly matter which query we check for. // It doesn't particularly matter which query we check for.
expect( expect(
queries.find((q) => q.fsPath.includes("PostMessageStar.ql")), queries.find((q) => q.includes("PostMessageStar.ql")),
).toBeDefined(); ).toBeDefined();
// Should not include non-problem queries. // Should not include non-problem queries.
expect( expect(
queries.find((q) => q.fsPath.includes("LinesOfCode.ql")), queries.find((q) => q.includes("LinesOfCode.ql")),
).not.toBeDefined(); ).not.toBeDefined();
}); });
}); });

View File

@@ -86,6 +86,14 @@ describe("Variant Analysis Submission Integration", () => {
it("shows the error message", async () => { it("shows the error message", async () => {
await showQlDocument("query.ql"); await showQlDocument("query.ql");
// Select target language for your query
quickPickSpy.mockResolvedValueOnce(
mockedQuickPickItem({
label: "JavaScript",
language: "javascript",
}),
);
await commandManager.execute("codeQL.runVariantAnalysis"); await commandManager.execute("codeQL.runVariantAnalysis");
expect(showErrorMessageSpy).toHaveBeenCalledWith( expect(showErrorMessageSpy).toHaveBeenCalledWith(

View File

@@ -296,9 +296,7 @@ describe("local databases", () => {
Uri.parse("file:/sourceArchive-uri/"), Uri.parse("file:/sourceArchive-uri/"),
); );
(db as any).contents.sourceArchiveUri = undefined; (db as any).contents.sourceArchiveUri = undefined;
expect(() => db.resolveSourceFile("abc")).toThrowError( expect(() => db.resolveSourceFile("abc")).toThrow("Scheme is missing");
"Scheme is missing",
);
}); });
it("should fail to resolve when not a file uri", () => { it("should fail to resolve when not a file uri", () => {
@@ -308,7 +306,7 @@ describe("local databases", () => {
Uri.parse("file:/sourceArchive-uri/"), Uri.parse("file:/sourceArchive-uri/"),
); );
(db as any).contents.sourceArchiveUri = undefined; (db as any).contents.sourceArchiveUri = undefined;
expect(() => db.resolveSourceFile("http://abc")).toThrowError( expect(() => db.resolveSourceFile("http://abc")).toThrow(
"Invalid uri scheme", "Invalid uri scheme",
); );
}); });

View File

@@ -123,7 +123,7 @@ describe("listDatabases", () => {
it("throws an error", async () => { it("throws an error", async () => {
await expect( await expect(
listDatabases(owner, repo, credentials, config), listDatabases(owner, repo, credentials, config),
).rejects.toThrowError("Not found"); ).rejects.toThrow("Not found");
}); });
}); });
@@ -150,7 +150,7 @@ describe("listDatabases", () => {
it("throws an error", async () => { it("throws an error", async () => {
await expect( await expect(
listDatabases(owner, repo, credentials, config), listDatabases(owner, repo, credentials, config),
).rejects.toThrowError("Internal server error"); ).rejects.toThrow("Internal server error");
}); });
}); });
}); });
@@ -199,7 +199,7 @@ describe("listDatabases", () => {
it("throws an error", async () => { it("throws an error", async () => {
await expect( await expect(
listDatabases(owner, repo, credentials, config), listDatabases(owner, repo, credentials, config),
).rejects.toThrowError("Internal server error"); ).rejects.toThrow("Internal server error");
expect(mockListCodeqlDatabases).not.toHaveBeenCalled(); expect(mockListCodeqlDatabases).not.toHaveBeenCalled();
}); });
}); });
@@ -270,7 +270,7 @@ describe("listDatabases", () => {
it("throws an error", async () => { it("throws an error", async () => {
await expect( await expect(
listDatabases(owner, repo, credentials, config), listDatabases(owner, repo, credentials, config),
).rejects.toThrowError("Not found"); ).rejects.toThrow("Not found");
}); });
}); });
@@ -297,7 +297,7 @@ describe("listDatabases", () => {
it("throws an error", async () => { it("throws an error", async () => {
await expect( await expect(
listDatabases(owner, repo, credentials, config), listDatabases(owner, repo, credentials, config),
).rejects.toThrowError("Internal server error"); ).rejects.toThrow("Internal server error");
}); });
}); });
}); });

View File

@@ -7,14 +7,42 @@ import {
createFileSync, createFileSync,
pathExistsSync, pathExistsSync,
} from "fs-extra"; } from "fs-extra";
import { Uri } from "vscode"; import { CancellationTokenSource, Uri, window } from "vscode";
import type {
DatabaseImportQuickPickItems,
DatabaseQuickPickItem,
DatabaseSelectionQuickPickItem,
} from "../../../../src/databases/local-databases-ui";
import { DatabaseUI } from "../../../../src/databases/local-databases-ui"; import { DatabaseUI } from "../../../../src/databases/local-databases-ui";
import { testDisposeHandler } from "../../test-dispose-handler"; import { testDisposeHandler } from "../../test-dispose-handler";
import { createMockApp } from "../../../__mocks__/appMock"; import { createMockApp } from "../../../__mocks__/appMock";
import { QueryLanguage } from "../../../../src/common/query-language"; import { QueryLanguage } from "../../../../src/common/query-language";
import { mockedQuickPickItem, mockedObject } from "../../utils/mocking.helpers";
describe("local-databases-ui", () => { describe("local-databases-ui", () => {
const storageDir = dirSync({ unsafeCleanup: true }).name;
const db1 = createDatabase(storageDir, "db1-imported", QueryLanguage.Cpp);
const db2 = createDatabase(storageDir, "db2-notimported", QueryLanguage.Cpp);
const db3 = createDatabase(storageDir, "db3-invalidlanguage", "hucairz");
// these two should be deleted
const db4 = createDatabase(
storageDir,
"db2-notimported-with-db-info",
QueryLanguage.Cpp,
".dbinfo",
);
const db5 = createDatabase(
storageDir,
"db2-notimported-with-codeql-database.yml",
QueryLanguage.Cpp,
"codeql-database.yml",
);
const app = createMockApp({});
describe("fixDbUri", () => { describe("fixDbUri", () => {
const fixDbUri = (DatabaseUI.prototype as any).fixDbUri; const fixDbUri = (DatabaseUI.prototype as any).fixDbUri;
it("should choose current directory normally", async () => { it("should choose current directory normally", async () => {
@@ -64,30 +92,6 @@ describe("local-databases-ui", () => {
}); });
it("should delete orphaned databases", async () => { it("should delete orphaned databases", async () => {
const storageDir = dirSync({ unsafeCleanup: true }).name;
const db1 = createDatabase(storageDir, "db1-imported", QueryLanguage.Cpp);
const db2 = createDatabase(
storageDir,
"db2-notimported",
QueryLanguage.Cpp,
);
const db3 = createDatabase(storageDir, "db3-invalidlanguage", "hucairz");
// these two should be deleted
const db4 = createDatabase(
storageDir,
"db2-notimported-with-db-info",
QueryLanguage.Cpp,
".dbinfo",
);
const db5 = createDatabase(
storageDir,
"db2-notimported-with-codeql-database.yml",
QueryLanguage.Cpp,
"codeql-database.yml",
);
const app = createMockApp({});
const databaseUI = new DatabaseUI( const databaseUI = new DatabaseUI(
app, app,
{ {
@@ -98,6 +102,7 @@ describe("local-databases-ui", () => {
onDidChangeCurrentDatabaseItem: () => { onDidChangeCurrentDatabaseItem: () => {
/**/ /**/
}, },
setCurrentDatabaseItem: () => {},
} as any, } as any,
{ {
onLanguageContextChanged: () => { onLanguageContextChanged: () => {
@@ -108,7 +113,6 @@ describe("local-databases-ui", () => {
storageDir, storageDir,
storageDir, storageDir,
); );
await databaseUI.handleRemoveOrphanedDatabases(); await databaseUI.handleRemoveOrphanedDatabases();
expect(pathExistsSync(db1)).toBe(true); expect(pathExistsSync(db1)).toBe(true);
@@ -121,6 +125,130 @@ describe("local-databases-ui", () => {
databaseUI.dispose(testDisposeHandler); databaseUI.dispose(testDisposeHandler);
}); });
describe("getDatabaseItem", () => {
const progress = jest.fn();
const token = new CancellationTokenSource().token;
describe("when there is a current database", () => {
const databaseUI = new DatabaseUI(
app,
{
databaseItems: [{ databaseUri: Uri.file(db1) }],
onDidChangeDatabaseItem: () => {
/**/
},
onDidChangeCurrentDatabaseItem: () => {
/**/
},
setCurrentDatabaseItem: () => {},
currentDatabaseItem: { databaseUri: Uri.file(db1) },
} as any,
{
onLanguageContextChanged: () => {
/**/
},
} as any,
{} as any,
storageDir,
storageDir,
);
it("should return current database", async () => {
const databaseItem = await databaseUI.getDatabaseItem(progress, token);
expect(databaseItem).toEqual({ databaseUri: Uri.file(db1) });
});
});
describe("when there is no current database", () => {
const databaseManager = {
databaseItems: [
{ databaseUri: Uri.file(db1) },
{ databaseUri: Uri.file(db2) },
],
onDidChangeDatabaseItem: () => {
/**/
},
onDidChangeCurrentDatabaseItem: () => {
/**/
},
setCurrentDatabaseItem: () => {},
currentDatabaseItem: undefined,
} as any;
const databaseUI = new DatabaseUI(
app,
databaseManager,
{
onLanguageContextChanged: () => {
/**/
},
} as any,
{} as any,
storageDir,
storageDir,
);
it("should prompt for a database and select existing one", async () => {
const showQuickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockResolvedValueOnce(
mockedQuickPickItem(
mockedObject<DatabaseSelectionQuickPickItem>({
databaseKind: "existing",
}),
),
)
.mockResolvedValueOnce(
mockedQuickPickItem(
mockedObject<DatabaseQuickPickItem>({
databaseItem: { databaseUri: Uri.file(db2) },
}),
),
);
const setCurrentDatabaseItemSpy = jest.spyOn(
databaseManager,
"setCurrentDatabaseItem",
);
await databaseUI.getDatabaseItem(progress, token);
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
expect(setCurrentDatabaseItemSpy).toHaveBeenCalledWith({
databaseUri: Uri.file(db2),
});
});
it("should prompt for a database and import a new one", async () => {
const showQuickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockResolvedValueOnce(
mockedQuickPickItem(
mockedObject<DatabaseSelectionQuickPickItem>({
databaseKind: "new",
}),
),
)
.mockResolvedValueOnce(
mockedQuickPickItem(
mockedObject<DatabaseImportQuickPickItems>({
importType: "github",
}),
),
);
const handleChooseDatabaseGithubSpy = jest
.spyOn(databaseUI as any, "handleChooseDatabaseGithub")
.mockResolvedValue(undefined);
await databaseUI.getDatabaseItem(progress, token);
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
expect(handleChooseDatabaseGithubSpy).toHaveBeenCalledTimes(1);
});
});
});
function createDatabase( function createDatabase(
storageDir: string, storageDir: string,
dbName: string, dbName: string,

View File

@@ -112,7 +112,7 @@ describe("resolveQueries", () => {
"tags contain": ["ide-contextual-queries/print-ast"], "tags contain": ["ide-contextual-queries/print-ast"],
}, },
), ),
).rejects.toThrowError( ).rejects.toThrow(
'No my query queries (kind "graph", tagged "ide-contextual-queries/print-ast") could be found in the current library path (tried searching the following packs: my-qlpack). Try upgrading the CodeQL libraries. If that doesn\'t work, then my query queries are not yet available for this language.', 'No my query queries (kind "graph", tagged "ide-contextual-queries/print-ast") could be found in the current library path (tried searching the following packs: my-qlpack). Try upgrading the CodeQL libraries. If that doesn\'t work, then my query queries are not yet available for this language.',
); );
}); });

View File

@@ -24,5 +24,5 @@
"noEmit": true "noEmit": true
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"exclude": ["node_modules", "test", "**/view"] "exclude": ["node_modules", "*.config.ts", "test", "**/view"]
} }