diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 75bc523b3..15d1499c6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -132,7 +132,12 @@ jobs: - name: Run unit tests working-directory: extensions/ql-vscode run: | - npm run test + npm run test:unit + + - name: Run view tests + working-directory: extensions/ql-vscode + run: | + npm run test:view test: name: Test @@ -173,7 +178,7 @@ jobs: VSCODE_CODEQL_GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' run: | unset DBUS_SESSION_BUS_ADDRESS - /usr/bin/xvfb-run npm run integration + /usr/bin/xvfb-run npm run test:vscode-integration - name: Run integration tests (Windows) if: matrix.os == 'windows-latest' @@ -181,7 +186,7 @@ jobs: env: VSCODE_CODEQL_GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' run: | - npm run integration + npm run test:vscode-integration set-matrix: name: Set Matrix for cli-test @@ -254,10 +259,10 @@ jobs: if: matrix.os == 'ubuntu-latest' run: | unset DBUS_SESSION_BUS_ADDRESS - /usr/bin/xvfb-run npm run cli-integration + /usr/bin/xvfb-run npm run test:cli-integration - name: Run CLI tests (Windows) working-directory: extensions/ql-vscode if: matrix.os == 'windows-latest' run: | - npm run cli-integration + npm run test:cli-integration diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a5e2dddd0..3115d05e7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -98,10 +98,11 @@ We have several types of tests: * Unit tests: these live in the `tests/unit-tests/` directory * View tests: these live in `src/view/variant-analysis/__tests__/` * VSCode integration tests: - * `test/vscode-tests/no-workspace` tests: These are intended to cover functionality that is meant to work before you even have a workspace open. + * `test/vscode-tests/activated-extension` tests: These are intended to cover functionality that require the full extension to be activated but don't require the CLI. This suite is not run against multiple versions of the CLI in CI. + * `test/vscode-tests/no-workspace` tests: These are intended to cover functionality around not having a workspace. The extension is not activated in these tests. * `test/vscode-tests/minimal-workspace` tests: These are intended to cover functionality that need a workspace but don't require the full extension to be activated. * CLI integration tests: these live in `test/vscode-tests/cli-integration` - * These tests are intendended to be cover functionality that is related to the integration between the CodeQL CLI and the extension. + * These tests are intended to cover functionality that is related to the integration between the CodeQL CLI and the extension. These tests are run against each supported versions of the CLI in CI. The CLI integration tests require an instance of the CodeQL CLI to run so they will require some extra setup steps. When adding new tests to our test suite, please be mindful of whether they need to be in the cli-integration folder. If the tests don't depend on the CLI, they are better suited to being a VSCode integration test. @@ -119,7 +120,7 @@ Then, from the `extensions/ql-vscode` directory, use the appropriate command to * Unit tests: `npm run test:unit` * View Tests: `npm test:view` -* VSCode integration tests: `npm run integration` +* VSCode integration tests: `npm run test:vscode-integration` ###### CLI integration tests @@ -130,7 +131,7 @@ The CLI integration tests require the CodeQL standard libraries in order to run 2. Run your test command: ```shell -cd extensions/ql-vscode && npm run cli-integration +cd extensions/ql-vscode && npm run test:cli-integration ``` ##### 2. From VSCode @@ -161,13 +162,13 @@ The easiest way to run a single test is to change the `it` of the test to `it.on to only run tests for this specific file. For example, to run the test `test/vscode-tests/cli-integration/run-queries.test.ts`: ```shell -npm run cli-integration -- --runTestsByPath test/vscode-tests/cli-integration/run-queries.test.ts +npm run test:cli-integration -- --runTestsByPath test/vscode-tests/cli-integration/run-queries.test.ts ``` You can also use the `--testNamePattern` option to run a specific test within a file. For example, to run the test `test/vscode-tests/cli-integration/run-queries.test.ts`: ```shell -npm run cli-integration -- --runTestsByPath test/vscode-tests/cli-integration/run-queries.test.ts --testNamePattern "should create a QueryEvaluationInfo" +npm run test:cli-integration -- --runTestsByPath test/vscode-tests/cli-integration/run-queries.test.ts --testNamePattern "should create a QueryEvaluationInfo" ``` ##### 2. From VSCode diff --git a/extensions/ql-vscode/jest.config.js b/extensions/ql-vscode/jest.config.js index 68933e683..f06906060 100644 --- a/extensions/ql-vscode/jest.config.js +++ b/extensions/ql-vscode/jest.config.js @@ -8,6 +8,7 @@ module.exports = { projects: [ "/src/view", "/test/unit-tests", + "/test/vscode-tests/activated-extension", "/test/vscode-tests/cli-integration", "/test/vscode-tests/no-workspace", "/test/vscode-tests/minimal-workspace", diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index 7390ec242..56d65c818 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -1326,13 +1326,14 @@ "scripts": { "build": "gulp", "watch": "gulp watch", - "test": "npm-run-all -p test:*", + "test": "npm-run-all test:*", "test:unit": "cross-env TZ=UTC LANG=en-US jest --projects test/unit-tests", "test:view": "jest --projects src/view", - "integration": "npm-run-all integration:*", - "integration:no-workspace": "jest --projects test/vscode-tests/no-workspace", - "integration:minimal-workspace": "jest --projects test/vscode-tests/minimal-workspace", - "cli-integration": "jest --projects test/vscode-tests/cli-integration", + "test:vscode-integration": "npm-run-all test:vscode-integration:*", + "test:vscode-integration:activated-extension": "jest --projects test/vscode-tests/activated-extension", + "test:vscode-integration:no-workspace": "jest --projects test/vscode-tests/no-workspace", + "test:vscode-integration:minimal-workspace": "jest --projects test/vscode-tests/minimal-workspace", + "test:cli-integration": "jest --projects test/vscode-tests/cli-integration", "update-vscode": "node ./node_modules/vscode/bin/install", "format": "prettier --write **/*.{ts,tsx} && eslint . --ext .ts,.tsx --fix", "lint": "eslint . --ext .js,.ts,.tsx --max-warnings=0", diff --git a/extensions/ql-vscode/src/cli.ts b/extensions/ql-vscode/src/cli.ts index 751aa374f..34e8442d0 100644 --- a/extensions/ql-vscode/src/cli.ts +++ b/extensions/ql-vscode/src/cli.ts @@ -28,6 +28,7 @@ import { CompilationMessage } from "./pure/legacy-messages"; import { sarifParser } from "./sarif-parser"; import { dbSchemeToLanguage, walkDirectory } from "./helpers"; import { App } from "./common/app"; +import { QueryLanguage } from "./qlpack-generator"; /** * The version of the SARIF format that we are using. @@ -1216,6 +1217,23 @@ export class CodeQLCliServer implements Disposable { ); } + /** + * Adds a core language QL library pack for the given query language as a dependency + * of the current package, and then installs them. This command modifies the qlpack.yml + * file of the current package. Formatting and comments will be removed. + * @param dir The directory where QL pack exists. + * @param language The language of the QL pack. + */ + async packAdd(dir: string, queryLanguage: QueryLanguage) { + const args = ["--dir", dir]; + args.push(`codeql/${queryLanguage}-all`); + return this.runJsonCodeQlCliCommandWithAuthentication( + ["pack", "add"], + args, + `Adding and installing ${queryLanguage} pack dependency.`, + ); + } + /** * Downloads a specified pack. * @param packs The `` of the packs to download. diff --git a/extensions/ql-vscode/src/databases.ts b/extensions/ql-vscode/src/databases.ts index e6651b8e0..995157883 100644 --- a/extensions/ql-vscode/src/databases.ts +++ b/extensions/ql-vscode/src/databases.ts @@ -26,6 +26,7 @@ import { QueryRunner } from "./queryRunner"; import { pathsEqual } from "./pure/files"; import { redactableError } from "./pure/errors"; import { isCodespacesTemplate } from "./config"; +import { QlPackGenerator, QueryLanguage } from "./qlpack-generator"; /** * databases.ts @@ -655,9 +656,27 @@ export class DatabaseManager extends DisposableObject { return; } - await showBinaryChoiceDialog( - `We've noticed you don't have a QL pack downloaded to analyze this database. Can we set up a ${databaseItem.language} query pack for you`, + const answer = await showBinaryChoiceDialog( + `We've noticed you don't have a CodeQL pack available to analyze this database. Can we set up a query pack for you?`, ); + + if (!answer) { + return; + } + + try { + const qlPackGenerator = new QlPackGenerator( + folderName, + databaseItem.language as QueryLanguage, + this.cli, + this.ctx.storageUri?.fsPath, + ); + await qlPackGenerator.generate(); + } catch (e: unknown) { + void this.logger.log( + `Could not create skeleton QL pack: ${getErrorMessage(e)}`, + ); + } } private async reregisterDatabases( diff --git a/extensions/ql-vscode/src/qlpack-generator.ts b/extensions/ql-vscode/src/qlpack-generator.ts new file mode 100644 index 000000000..d4505cc3e --- /dev/null +++ b/extensions/ql-vscode/src/qlpack-generator.ts @@ -0,0 +1,101 @@ +import { writeFile } from "fs-extra"; +import { dump } from "js-yaml"; +import { join } from "path"; +import { Uri, workspace } from "vscode"; +import { CodeQLCliServer } from "./cli"; + +export type QueryLanguage = + | "csharp" + | "cpp" + | "go" + | "java" + | "javascript" + | "python" + | "ruby" + | "swift"; + +export class QlPackGenerator { + private readonly qlpackName: string; + private readonly qlpackVersion: string; + private readonly header: string; + private readonly qlpackFileName: string; + private readonly folderUri: Uri; + + constructor( + private readonly folderName: string, + private readonly queryLanguage: QueryLanguage, + private readonly cliServer: CodeQLCliServer, + private readonly storagePath: string | undefined, + ) { + if (this.storagePath === undefined) { + throw new Error("Workspace storage path is undefined"); + } + this.qlpackName = `getting-started/codeql-extra-queries-${this.queryLanguage}`; + this.qlpackVersion = "1.0.0"; + this.header = "# This is an automatically generated file.\n\n"; + + this.qlpackFileName = "qlpack.yml"; + this.folderUri = Uri.file(join(this.storagePath, this.folderName)); + } + + public async generate() { + // create QL pack folder and add to workspace + await this.createWorkspaceFolder(); + + // create qlpack.yml + await this.createQlPackYaml(); + + // create example.ql + await this.createExampleQlFile(); + + // create codeql-pack.lock.yml + await this.createCodeqlPackLockYaml(); + } + + private async createWorkspaceFolder() { + await workspace.fs.createDirectory(this.folderUri); + + const end = (workspace.workspaceFolders || []).length; + + await workspace.updateWorkspaceFolders(end, 0, { + name: this.folderName, + uri: this.folderUri, + }); + } + + private async createQlPackYaml() { + const qlPackFilePath = join(this.folderUri.fsPath, this.qlpackFileName); + + const qlPackYml = { + name: this.qlpackName, + version: this.qlpackVersion, + dependencies: {}, + }; + + await writeFile(qlPackFilePath, this.header + dump(qlPackYml), "utf8"); + } + + private async createExampleQlFile() { + const exampleQlFilePath = join(this.folderUri.fsPath, "example.ql"); + + const exampleQl = ` +/** + * This is an automatically generated file + * @name Empty block + * @kind problem + * @problem.severity warning + * @id ${this.queryLanguage}/example/empty-block + */ + +import ${this.queryLanguage} + +select "Hello, world!" +`.trim(); + + await writeFile(exampleQlFilePath, exampleQl, "utf8"); + } + + private async createCodeqlPackLockYaml() { + await this.cliServer.packAdd(this.folderUri.fsPath, this.queryLanguage); + } +} diff --git a/extensions/ql-vscode/src/stories/remote-queries/TextButton.stories.tsx b/extensions/ql-vscode/src/stories/common/TextButton.stories.tsx similarity index 88% rename from extensions/ql-vscode/src/stories/remote-queries/TextButton.stories.tsx rename to extensions/ql-vscode/src/stories/common/TextButton.stories.tsx index fd3a6e376..6a4332e69 100644 --- a/extensions/ql-vscode/src/stories/remote-queries/TextButton.stories.tsx +++ b/extensions/ql-vscode/src/stories/common/TextButton.stories.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; -import TextButtonComponent from "../../view/remote-queries/TextButton"; +import TextButtonComponent from "../../view/common/TextButton"; export default { title: "Text Button", diff --git a/extensions/ql-vscode/src/stories/remote-queries/data/analysesResultsMessage.json b/extensions/ql-vscode/src/stories/data/analysesResultsMessage.json similarity index 100% rename from extensions/ql-vscode/src/stories/remote-queries/data/analysesResultsMessage.json rename to extensions/ql-vscode/src/stories/data/analysesResultsMessage.json diff --git a/extensions/ql-vscode/src/stories/remote-queries/data/rawResults.json b/extensions/ql-vscode/src/stories/data/rawResults.json similarity index 100% rename from extensions/ql-vscode/src/stories/remote-queries/data/rawResults.json rename to extensions/ql-vscode/src/stories/data/rawResults.json diff --git a/extensions/ql-vscode/src/stories/remote-queries/data/remoteQueryResultMessage.json b/extensions/ql-vscode/src/stories/data/remoteQueryResultMessage.json similarity index 100% rename from extensions/ql-vscode/src/stories/remote-queries/data/remoteQueryResultMessage.json rename to extensions/ql-vscode/src/stories/data/remoteQueryResultMessage.json diff --git a/extensions/ql-vscode/src/stories/remote-queries/DownloadButton.stories.tsx b/extensions/ql-vscode/src/stories/remote-queries/DownloadButton.stories.tsx deleted file mode 100644 index 1ea54caa0..000000000 --- a/extensions/ql-vscode/src/stories/remote-queries/DownloadButton.stories.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from "react"; - -import { ComponentStory, ComponentMeta } from "@storybook/react"; - -import DownloadButtonComponent from "../../view/remote-queries/DownloadButton"; - -export default { - title: "Download Button", - component: DownloadButtonComponent, - argTypes: { - onClick: { - action: "clicked", - table: { - disable: true, - }, - }, - }, -} as ComponentMeta; - -const Template: ComponentStory = (args) => ( - -); - -export const DownloadButton = Template.bind({}); -DownloadButton.args = { - text: "Download", -}; diff --git a/extensions/ql-vscode/src/stories/remote-queries/DownloadSpinner.stories.tsx b/extensions/ql-vscode/src/stories/remote-queries/DownloadSpinner.stories.tsx deleted file mode 100644 index 358933b77..000000000 --- a/extensions/ql-vscode/src/stories/remote-queries/DownloadSpinner.stories.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from "react"; - -import { ComponentMeta } from "@storybook/react"; - -import DownloadSpinnerComponent from "../../view/remote-queries/DownloadSpinner"; - -export default { - title: "Download Spinner", - component: DownloadSpinnerComponent, -} as ComponentMeta; - -export const DownloadSpinner = ; diff --git a/extensions/ql-vscode/src/stories/remote-queries/LastUpdated.stories.tsx b/extensions/ql-vscode/src/stories/remote-queries/LastUpdated.stories.tsx deleted file mode 100644 index b60bd0928..000000000 --- a/extensions/ql-vscode/src/stories/remote-queries/LastUpdated.stories.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from "react"; - -import { ComponentStory, ComponentMeta } from "@storybook/react"; - -import LastUpdatedComponent from "../../view/remote-queries/LastUpdated"; - -export default { - title: "MRVA/Last Updated", - component: LastUpdatedComponent, -} as ComponentMeta; - -const Template: ComponentStory = (args) => ( - -); - -export const LastUpdated = Template.bind({}); - -LastUpdated.args = { - lastUpdated: -3_600_000, // 1 hour ago -}; diff --git a/extensions/ql-vscode/src/stories/remote-queries/RemoteQueries.stories.tsx b/extensions/ql-vscode/src/stories/remote-queries/RemoteQueries.stories.tsx deleted file mode 100644 index b3ecd2667..000000000 --- a/extensions/ql-vscode/src/stories/remote-queries/RemoteQueries.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from "react"; -import { useEffect } from "react"; - -import { ComponentStory, ComponentMeta } from "@storybook/react"; - -import { RemoteQueries } from "../../view/remote-queries/RemoteQueries"; - -import * as remoteQueryResult from "./data/remoteQueryResultMessage.json"; -import * as analysesResults from "./data/analysesResultsMessage.json"; - -export default { - title: "MRVA/Remote Queries", - component: RemoteQueries, -} as ComponentMeta; - -const Template: ComponentStory = () => { - useEffect(() => { - window.postMessage(remoteQueryResult); - window.postMessage(analysesResults); - }); - - return ; -}; - -export const Top10JavaScript = Template.bind({}); diff --git a/extensions/ql-vscode/src/stories/remote-queries/RepositoriesSearch.stories.tsx b/extensions/ql-vscode/src/stories/remote-queries/RepositoriesSearch.stories.tsx deleted file mode 100644 index 5207d5806..000000000 --- a/extensions/ql-vscode/src/stories/remote-queries/RepositoriesSearch.stories.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from "react"; -import { useState } from "react"; - -import { ComponentMeta } from "@storybook/react"; - -import RepositoriesSearchComponent from "../../view/remote-queries/RepositoriesSearch"; - -export default { - title: "MRVA/Repositories Search", - component: RepositoriesSearchComponent, - argTypes: { - filterValue: { - control: { - disable: true, - }, - }, - }, -} as ComponentMeta; - -export const RepositoriesSearch = () => { - const [filterValue, setFilterValue] = useState(""); - - return ( - - ); -}; diff --git a/extensions/ql-vscode/src/stories/remote-queries/AnalysisAlertResult.stories.tsx b/extensions/ql-vscode/src/stories/variant-analysis/AnalysisAlertResult.stories.tsx similarity index 90% rename from extensions/ql-vscode/src/stories/remote-queries/AnalysisAlertResult.stories.tsx rename to extensions/ql-vscode/src/stories/variant-analysis/AnalysisAlertResult.stories.tsx index 219d797a3..8a8e9a683 100644 --- a/extensions/ql-vscode/src/stories/remote-queries/AnalysisAlertResult.stories.tsx +++ b/extensions/ql-vscode/src/stories/variant-analysis/AnalysisAlertResult.stories.tsx @@ -2,11 +2,11 @@ import * as React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; -import AnalysisAlertResult from "../../view/remote-queries/AnalysisAlertResult"; +import AnalysisAlertResult from "../../view/variant-analysis/AnalysisAlertResult"; import type { AnalysisAlert } from "../../remote-queries/shared/analysis-result"; export default { - title: "Analysis Alert Result", + title: "Variant Analysis/Analysis Alert Result", component: AnalysisAlertResult, } as ComponentMeta; diff --git a/extensions/ql-vscode/src/stories/variant-analysis/RepoRow.stories.tsx b/extensions/ql-vscode/src/stories/variant-analysis/RepoRow.stories.tsx index 95bce78bd..a57cbcc24 100644 --- a/extensions/ql-vscode/src/stories/variant-analysis/RepoRow.stories.tsx +++ b/extensions/ql-vscode/src/stories/variant-analysis/RepoRow.stories.tsx @@ -13,8 +13,8 @@ import { } from "../../remote-queries/shared/analysis-result"; import { createMockRepositoryWithMetadata } from "../../../test/factories/remote-queries/shared/repository"; -import * as analysesResults from "../remote-queries/data/analysesResultsMessage.json"; -import * as rawResults from "../remote-queries/data/rawResults.json"; +import * as analysesResults from "../data/analysesResultsMessage.json"; +import * as rawResults from "../data/rawResults.json"; import { RepoRow, RepoRowProps } from "../../view/variant-analysis/RepoRow"; export default { diff --git a/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisAnalyzedRepos.stories.tsx b/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisAnalyzedRepos.stories.tsx index 1b18b9942..3405e2f58 100644 --- a/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisAnalyzedRepos.stories.tsx +++ b/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisAnalyzedRepos.stories.tsx @@ -16,7 +16,7 @@ import { createMockVariantAnalysis } from "../../../test/factories/remote-querie import { createMockRepositoryWithMetadata } from "../../../test/factories/remote-queries/shared/repository"; import { createMockScannedRepo } from "../../../test/factories/remote-queries/shared/scanned-repositories"; -import * as analysesResults from "../remote-queries/data/analysesResultsMessage.json"; +import * as analysesResults from "../data/analysesResultsMessage.json"; export default { title: "Variant Analysis/Analyzed Repos", diff --git a/extensions/ql-vscode/src/view/remote-queries/TextButton.tsx b/extensions/ql-vscode/src/view/common/TextButton.tsx similarity index 100% rename from extensions/ql-vscode/src/view/remote-queries/TextButton.tsx rename to extensions/ql-vscode/src/view/common/TextButton.tsx diff --git a/extensions/ql-vscode/src/view/remote-queries/CollapsibleItem.tsx b/extensions/ql-vscode/src/view/remote-queries/CollapsibleItem.tsx deleted file mode 100644 index ad8f55411..000000000 --- a/extensions/ql-vscode/src/view/remote-queries/CollapsibleItem.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import * as React from "react"; -import styled from "styled-components"; -import { ChevronDownIcon, ChevronRightIcon } from "@primer/octicons-react"; -import { useState } from "react"; - -const Container = styled.div` - display: block; - vertical-align: middle; - cursor: pointer; -`; - -const TitleContainer = styled.span` - display: inline-block; -`; - -const Button = styled.button` - display: inline-block; - background-color: transparent; - color: var(--vscode-editor-foreground); - border: none; - padding-left: 0; - padding-right: 0.1em; -`; - -const CollapsibleItem = ({ - title, - children, -}: { - title: React.ReactNode; - children: React.ReactNode; -}) => { - const [isExpanded, setExpanded] = useState(false); - return ( - <> - setExpanded(!isExpanded)}> - - {title} - - {isExpanded && children} - - ); -}; - -export default CollapsibleItem; diff --git a/extensions/ql-vscode/src/view/remote-queries/DownloadButton.tsx b/extensions/ql-vscode/src/view/remote-queries/DownloadButton.tsx deleted file mode 100644 index c7328ab02..000000000 --- a/extensions/ql-vscode/src/view/remote-queries/DownloadButton.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from "react"; -import styled from "styled-components"; -import { DownloadIcon } from "@primer/octicons-react"; - -const ButtonLink = styled.a` - display: inline-block; - font-size: x-small; - text-decoration: none; - cursor: pointer; - vertical-align: middle; - - svg { - fill: var(--vscode-textLink-foreground); - } -`; - -const DownloadButton = ({ - text, - onClick, -}: { - text: string; - onClick: () => void; -}) => ( - - - {text} - -); - -export default DownloadButton; diff --git a/extensions/ql-vscode/src/view/remote-queries/DownloadSpinner.tsx b/extensions/ql-vscode/src/view/remote-queries/DownloadSpinner.tsx deleted file mode 100644 index 5778de717..000000000 --- a/extensions/ql-vscode/src/view/remote-queries/DownloadSpinner.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"; -import * as React from "react"; -import styled from "styled-components"; - -const SpinnerContainer = styled.span` - vertical-align: middle; - display: inline-block; -`; - -const DownloadSpinner = () => ( - - - -); - -export default DownloadSpinner; diff --git a/extensions/ql-vscode/src/view/remote-queries/FullScreenModal.tsx b/extensions/ql-vscode/src/view/remote-queries/FullScreenModal.tsx deleted file mode 100644 index 4de087f7f..000000000 --- a/extensions/ql-vscode/src/view/remote-queries/FullScreenModal.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from "react"; -import { createPortal } from "react-dom"; -import styled from "styled-components"; -import { XCircleIcon } from "@primer/octicons-react"; - -const Container = styled.div` - position: fixed; - top: 0; - left: 0; - height: 100%; - width: 100%; - opacity: 1; - background-color: var(--vscode-editor-background); - z-index: 5000; - padding-top: 1em; -`; - -const CloseButton = styled.button` - position: absolute; - top: 1em; - right: 1em; - background-color: var(--vscode-editor-background); - border: none; -`; - -const FullScreenModal = ({ - setOpen, - containerElementId, - children, -}: { - setOpen: (open: boolean) => void; - containerElementId: string; - children: React.ReactNode; -}) => { - const containerElement = document.getElementById(containerElementId); - if (!containerElement) { - throw Error(`Could not find container element. Id: ${containerElementId}`); - } - - return createPortal( - <> - - setOpen(false)}> - - - {children} - - , - containerElement, - ); -}; - -export default FullScreenModal; diff --git a/extensions/ql-vscode/src/view/remote-queries/LastUpdated.tsx b/extensions/ql-vscode/src/view/remote-queries/LastUpdated.tsx deleted file mode 100644 index eb41ae4a7..000000000 --- a/extensions/ql-vscode/src/view/remote-queries/LastUpdated.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from "react"; -import { RepoPushIcon } from "@primer/octicons-react"; -import styled from "styled-components"; - -import { humanizeRelativeTime } from "../../pure/time"; - -const IconContainer = styled.span` - flex-grow: 0; - text-align: right; - margin-right: 0; -`; - -const Duration = styled.span` - text-align: left; - width: 8em; - margin-left: 0.5em; -`; - -type Props = { lastUpdated?: number }; - -const LastUpdated = ({ lastUpdated }: Props) => - // lastUpdated will be undefined for older results that were - // created before the lastUpdated field was added. - Number.isFinite(lastUpdated) ? ( - <> - - - - {humanizeRelativeTime(lastUpdated)} - - ) : ( - <> - ); - -export default LastUpdated; diff --git a/extensions/ql-vscode/src/view/remote-queries/RemoteQueries.tsx b/extensions/ql-vscode/src/view/remote-queries/RemoteQueries.tsx deleted file mode 100644 index 69c8292a6..000000000 --- a/extensions/ql-vscode/src/view/remote-queries/RemoteQueries.tsx +++ /dev/null @@ -1,551 +0,0 @@ -import * as React from "react"; -import { useEffect, useState } from "react"; -import { Flash, ThemeProvider } from "@primer/react"; -import { ToRemoteQueriesMessage } from "../../pure/interface-types"; -import { - AnalysisSummary, - RemoteQueryResult, -} from "../../remote-queries/shared/remote-query-result"; -import { MAX_RAW_RESULTS } from "../../remote-queries/shared/result-limits"; -import { vscode } from "../vscode-api"; -import { VSCodeBadge, VSCodeButton } from "@vscode/webview-ui-toolkit/react"; -import { - HorizontalSpace, - SectionTitle, - VerticalSpace, - ViewTitle, -} from "../common"; -import DownloadButton from "./DownloadButton"; -import { - AnalysisResults, - getAnalysisResultCount, -} from "../../remote-queries/shared/analysis-result"; -import DownloadSpinner from "./DownloadSpinner"; -import CollapsibleItem from "./CollapsibleItem"; -import { - AlertIcon, - CodeSquareIcon, - FileCodeIcon, - RepoIcon, - TerminalIcon, -} from "@primer/octicons-react"; -import AnalysisAlertResult from "./AnalysisAlertResult"; -import RawResultsTable from "./RawResultsTable"; -import RepositoriesSearch from "./RepositoriesSearch"; -import StarCount from "../common/StarCount"; -import SortRepoFilter, { Sort, sorter } from "./SortRepoFilter"; -import LastUpdated from "./LastUpdated"; -import RepoListCopyButton from "./RepoListCopyButton"; - -import "./baseStyles.css"; -import "./remoteQueries.css"; - -const numOfReposInContractedMode = 10; - -const emptyQueryResult: RemoteQueryResult = { - queryId: "", - queryTitle: "", - queryFileName: "", - queryFilePath: "", - queryText: "", - language: "", - workflowRunUrl: "", - totalRepositoryCount: 0, - affectedRepositoryCount: 0, - totalResultCount: 0, - executionTimestamp: "", - executionDuration: "", - analysisSummaries: [], - analysisFailures: [], -}; - -const downloadAnalysisResults = (analysisSummary: AnalysisSummary) => { - vscode.postMessage({ - t: "remoteQueryDownloadAnalysisResults", - analysisSummary, - }); -}; - -const downloadAllAnalysesResults = (query: RemoteQueryResult) => { - vscode.postMessage({ - t: "remoteQueryDownloadAllAnalysesResults", - analysisSummaries: query.analysisSummaries, - }); -}; - -const openQueryFile = (queryResult: RemoteQueryResult) => { - vscode.postMessage({ - t: "openFile", - filePath: queryResult.queryFilePath, - }); -}; - -const openQueryTextVirtualFile = (queryResult: RemoteQueryResult) => { - vscode.postMessage({ - t: "openVirtualFile", - queryText: queryResult.queryText, - }); -}; - -function createResultsDescription(queryResult: RemoteQueryResult) { - const reposCount = `${queryResult.totalRepositoryCount} ${ - queryResult.totalRepositoryCount === 1 ? "repository" : "repositories" - }`; - return `${queryResult.totalResultCount} results from running against ${reposCount} (${queryResult.executionDuration}), ${queryResult.executionTimestamp}`; -} - -const sumAnalysesResults = (analysesResults: AnalysisResults[]) => - analysesResults.reduce((acc, curr) => acc + getAnalysisResultCount(curr), 0); - -const QueryInfo = (queryResult: RemoteQueryResult) => ( - <> - - {createResultsDescription(queryResult)} - - - openQueryFile(queryResult)} - > - - {" "} - {" "} - - {queryResult.queryFileName} - - - - openQueryTextVirtualFile(queryResult)} - > - - {" "} - {" "} - - Query - - - - - - {" "} - {" "} - - Logs - - - -); - -const Failures = (queryResult: RemoteQueryResult) => { - if (queryResult.analysisFailures.length === 0) { - return <>; - } - return ( - <> - - - {queryResult.analysisFailures.map((f, i) => ( -
-

- - {f.nwo}: - {f.error} -

- {i === queryResult.analysisFailures.length - 1 ? ( - <> - ) : ( - - )} -
- ))} -
- - ); -}; - -const SummaryTitleWithResults = ({ - queryResult, - analysesResults, - sort, - setSort, -}: { - queryResult: RemoteQueryResult; - analysesResults: AnalysisResults[]; - sort: Sort; - setSort: (sort: Sort) => void; -}) => { - const showDownloadButton = - queryResult.totalResultCount !== sumAnalysesResults(analysesResults); - - return ( -
- - Repositories with results ({queryResult.affectedRepositoryCount}): - - {showDownloadButton && ( - downloadAllAnalysesResults(queryResult)} - /> - )} -
- - - -
-
- ); -}; - -const SummaryTitleNoResults = () => ( -
- No results found -
-); - -const SummaryItemDownload = ({ - analysisSummary, - analysisResults, -}: { - analysisSummary: AnalysisSummary; - analysisResults: AnalysisResults | undefined; -}) => { - if (!analysisResults || analysisResults.status === "Failed") { - return ( - downloadAnalysisResults(analysisSummary)} - /> - ); - } - - if (analysisResults.status === "InProgress") { - return ( - <> - - - - ); - } - - return <>; -}; - -const SummaryItem = ({ - analysisSummary, - analysisResults, -}: { - analysisSummary: AnalysisSummary; - analysisResults: AnalysisResults | undefined; -}) => ( - <> - - - - {analysisSummary.nwo} - - - {analysisSummary.resultCount.toString()} - - - - - - - -); - -const Summary = ({ - queryResult, - analysesResults, - sort, - setSort, -}: { - queryResult: RemoteQueryResult; - analysesResults: AnalysisResults[]; - sort: Sort; - setSort: (sort: Sort) => void; -}) => { - const [repoListExpanded, setRepoListExpanded] = useState(false); - const numOfReposToShow = repoListExpanded - ? queryResult.analysisSummaries.length - : numOfReposInContractedMode; - - return ( - <> - {queryResult.affectedRepositoryCount === 0 ? ( - - ) : ( - - )} - -
    - {queryResult.analysisSummaries - .slice(0, numOfReposToShow) - .sort(sorter(sort)) - .map((summary, i) => ( -
  • - a.nwo === summary.nwo, - )} - /> -
  • - ))} -
- {queryResult.analysisSummaries.length > numOfReposInContractedMode && ( - - )} - - ); -}; - -const AnalysesResultsTitle = ({ - totalAnalysesResults, - totalResults, -}: { - totalAnalysesResults: number; - totalResults: number; -}) => { - if (totalAnalysesResults === totalResults) { - return {totalAnalysesResults} results; - } - - return ( - - {totalAnalysesResults}/{totalResults} results - - ); -}; - -const exportResults = (queryResult: RemoteQueryResult) => { - vscode.postMessage({ - t: "remoteQueryExportResults", - queryId: queryResult.queryId, - }); -}; - -const AnalysesResultsDescription = ({ - queryResult, - analysesResults, -}: { - queryResult: RemoteQueryResult; - analysesResults: AnalysisResults[]; -}) => { - const showDownloadsMessage = queryResult.analysisSummaries.some( - (s) => - !analysesResults.some((a) => a.nwo === s.nwo && a.status === "Completed"), - ); - const downloadsMessage = ( - <> - - Some results haven't been downloaded automatically because of their - size or because enough were downloaded already. Download them manually - from the list above if you want to see them here. - - ); - - const showMaxResultsMessage = analysesResults.some( - (a) => a.rawResults?.capped, - ); - const maxRawResultsMessage = ( - <> - - Some repositories have more than {MAX_RAW_RESULTS} results. We will only - show you up to  - {MAX_RAW_RESULTS} results for each repository. - - ); - - return ( - <> - {showDownloadsMessage && downloadsMessage} - {showMaxResultsMessage && maxRawResultsMessage} - - ); -}; - -const RepoAnalysisResults = (analysisResults: AnalysisResults) => { - const numOfResults = getAnalysisResultCount(analysisResults); - const title = ( - <> - {analysisResults.nwo} - - {numOfResults.toString()} - - ); - - return ( - -
    - {analysisResults.interpretedResults.map((r, i) => ( -
  • - - -
  • - ))} -
- {analysisResults.rawResults && ( - - )} -
- ); -}; - -const AnalysesResults = ({ - queryResult, - analysesResults, - totalResults, - sort, -}: { - queryResult: RemoteQueryResult; - analysesResults: AnalysisResults[]; - totalResults: number; - sort: Sort; -}) => { - const totalAnalysesResults = sumAnalysesResults(analysesResults); - const [filterValue, setFilterValue] = useState(""); - - if (totalResults === 0) { - return <>; - } - - return ( - <> - -
-
- -
-
- exportResults(queryResult)}> - Export all - -
-
- - - - - -
    - {analysesResults - .filter( - (a) => - a.interpretedResults.length || - a.rawResults?.resultSet?.rows?.length, - ) - .filter((a) => - a.nwo.toLowerCase().includes(filterValue.toLowerCase()), - ) - .sort(sorter(sort)) - .map((r) => ( -
  • - -
  • - ))} -
- - ); -}; - -export function RemoteQueries(): JSX.Element { - const [queryResult, setQueryResult] = - useState(emptyQueryResult); - const [analysesResults, setAnalysesResults] = useState([]); - const [sort, setSort] = useState("name"); - - useEffect(() => { - const listener = (evt: MessageEvent) => { - if (evt.origin === window.origin) { - const msg: ToRemoteQueriesMessage = evt.data; - if (msg.t === "setRemoteQueryResult") { - setQueryResult(msg.queryResult); - } else if (msg.t === "setAnalysesResults") { - setAnalysesResults(msg.analysesResults); - } - } else { - // sanitize origin - const origin = evt.origin.replace(/\n|\r/g, ""); - console.error(`Invalid event origin ${origin}`); - } - }; - window.addEventListener("message", listener); - - return () => { - window.removeEventListener("message", listener); - }; - }, []); - - if (!queryResult) { - return
Waiting for results to load.
; - } - - try { - return ( -
- - {queryResult.queryTitle} - - - - - -
- ); - } catch (err) { - console.error(err); - return
There was an error displaying the view.
; - } -} diff --git a/extensions/ql-vscode/src/view/remote-queries/RepoListCopyButton.tsx b/extensions/ql-vscode/src/view/remote-queries/RepoListCopyButton.tsx deleted file mode 100644 index a241c17a9..000000000 --- a/extensions/ql-vscode/src/view/remote-queries/RepoListCopyButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from "react"; -import { vscode } from "../vscode-api"; -import { RemoteQueryResult } from "../../remote-queries/shared/remote-query-result"; -import { CopyIcon } from "@primer/octicons-react"; -import { IconButton } from "@primer/react"; - -const copyRepositoryList = (queryResult: RemoteQueryResult) => { - vscode.postMessage({ - t: "copyRepoList", - queryId: queryResult.queryId, - }); -}; - -const RepoListCopyButton = ({ - queryResult, -}: { - queryResult: RemoteQueryResult; -}) => ( - copyRepositoryList(queryResult)} - /> -); - -export default RepoListCopyButton; diff --git a/extensions/ql-vscode/src/view/remote-queries/RepositoriesSearch.tsx b/extensions/ql-vscode/src/view/remote-queries/RepositoriesSearch.tsx deleted file mode 100644 index dfcdf14ad..000000000 --- a/extensions/ql-vscode/src/view/remote-queries/RepositoriesSearch.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from "react"; -import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"; - -interface RepositoriesSearchProps { - filterValue: string; - setFilterValue: (value: string) => void; -} - -const RepositoriesSearch = ({ - filterValue, - setFilterValue, -}: RepositoriesSearchProps) => { - return ( - <> - - setFilterValue((e.target as HTMLInputElement).value) - } - > - - - - ); -}; - -export default RepositoriesSearch; diff --git a/extensions/ql-vscode/src/view/remote-queries/SortRepoFilter.tsx b/extensions/ql-vscode/src/view/remote-queries/SortRepoFilter.tsx deleted file mode 100644 index 6c63b5c73..000000000 --- a/extensions/ql-vscode/src/view/remote-queries/SortRepoFilter.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import * as React from "react"; -import { FilterIcon } from "@primer/octicons-react"; -import { ActionList, ActionMenu, IconButton } from "@primer/react"; -import styled from "styled-components"; - -const SortWrapper = styled.span` - flex-grow: 2; - text-align: right; - margin-right: 0; -`; - -export type Sort = "name" | "stars" | "results" | "lastUpdated"; -type Props = { - sort: Sort; - setSort: (sort: Sort) => void; -}; - -type Sortable = { - nwo: string; - starCount?: number; - resultCount?: number; - lastUpdated?: number; -}; - -const sortBy = [ - { name: "Sort by Name", sort: "name" }, - { name: "Sort by Results", sort: "results" }, - { name: "Sort by Stars", sort: "stars" }, - { name: "Sort by Last Updated", sort: "lastUpdated" }, -]; - -export function sorter( - sort: Sort, -): (left: Sortable, right: Sortable) => number { - // stars and results are highest to lowest - // name is alphabetical - return (left: Sortable, right: Sortable) => { - if (sort === "stars") { - const stars = (right.starCount || 0) - (left.starCount || 0); - if (stars !== 0) { - return stars; - } - } - if (sort === "lastUpdated") { - const lastUpdated = (right.lastUpdated || 0) - (left.lastUpdated || 0); - if (lastUpdated !== 0) { - return lastUpdated; - } - } - if (sort === "results") { - const results = (right.resultCount || 0) - (left.resultCount || 0); - if (results !== 0) { - return results; - } - } - - // Fall back on name compare if results, stars, or lastUpdated are equal - return left.nwo.localeCompare(right.nwo, undefined, { - sensitivity: "base", - }); - }; -} - -const SortRepoFilter = ({ sort, setSort }: Props) => { - return ( - - - - - - - - - {sortBy.map((type, index) => ( - setSort(type.sort as Sort)} - > - {type.name} - - ))} - - - - - ); -}; - -export default SortRepoFilter; diff --git a/extensions/ql-vscode/src/view/remote-queries/baseStyles.css b/extensions/ql-vscode/src/view/remote-queries/baseStyles.css deleted file mode 100644 index 4d3d4d543..000000000 --- a/extensions/ql-vscode/src/view/remote-queries/baseStyles.css +++ /dev/null @@ -1,4 +0,0 @@ -body { - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, - sans-serif, Apple Color Emoji, Segoe UI Emoji; -} diff --git a/extensions/ql-vscode/src/view/remote-queries/index.tsx b/extensions/ql-vscode/src/view/remote-queries/index.tsx deleted file mode 100644 index d7292eeca..000000000 --- a/extensions/ql-vscode/src/view/remote-queries/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import * as React from "react"; -import { WebviewDefinition } from "../webview-definition"; -import { RemoteQueries } from "./RemoteQueries"; - -const definition: WebviewDefinition = { - component: , -}; - -export default definition; diff --git a/extensions/ql-vscode/src/view/remote-queries/remoteQueries.css b/extensions/ql-vscode/src/view/remote-queries/remoteQueries.css deleted file mode 100644 index 943de24f4..000000000 --- a/extensions/ql-vscode/src/view/remote-queries/remoteQueries.css +++ /dev/null @@ -1,53 +0,0 @@ -.vscode-codeql__remote-queries { - max-width: 55em; -} - -.vscode-codeql__query-info-link { - text-decoration: none; - padding-right: 1em; - color: var(--vscode-editor-foreground); -} - -.vscode-codeql__query-info-link:hover { - color: var(--vscode-editor-foreground); -} - -.vscode-codeql__query-summary-container { - padding-top: 1.5em; - display: flex; -} - -.vscode-codeql__analysis-summaries-list-item { - margin-top: 0.5em; - display: flex; -} - -.vscode-codeql__analyses-results-list-item { - padding-top: 0.5em; -} - -.vscode-codeql__analysis-item { - padding-right: 0.1em; -} - -.vscode-codeql__expand-button { - background: none; - color: var(--vscode-textLink-foreground); - border: none; - cursor: pointer; - padding-top: 1em; - font-size: x-small; -} - -.vscode-codeql__analysis-failure { - margin: 0; - font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, - Liberation Mono, monospace; - color: var(--vscode-editor-foreground); -} - -.vscode-codeql__flat-list { - list-style-type: none; - margin: 0; - padding: 0.5em 0 0 0; -} diff --git a/extensions/ql-vscode/src/view/remote-queries/AnalysisAlertResult.tsx b/extensions/ql-vscode/src/view/variant-analysis/AnalysisAlertResult.tsx similarity index 100% rename from extensions/ql-vscode/src/view/remote-queries/AnalysisAlertResult.tsx rename to extensions/ql-vscode/src/view/variant-analysis/AnalysisAlertResult.tsx diff --git a/extensions/ql-vscode/src/view/variant-analysis/AnalyzedRepoItemContent.tsx b/extensions/ql-vscode/src/view/variant-analysis/AnalyzedRepoItemContent.tsx index 085cbd84d..264c96c69 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/AnalyzedRepoItemContent.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/AnalyzedRepoItemContent.tsx @@ -4,8 +4,8 @@ import { AnalysisAlert, AnalysisRawResults, } from "../../remote-queries/shared/analysis-result"; -import AnalysisAlertResult from "../remote-queries/AnalysisAlertResult"; -import RawResultsTable from "../remote-queries/RawResultsTable"; +import AnalysisAlertResult from "./AnalysisAlertResult"; +import RawResultsTable from "./RawResultsTable"; import { VariantAnalysisRepoStatus, VariantAnalysisScannedRepositoryDownloadStatus, diff --git a/extensions/ql-vscode/src/view/remote-queries/RawResultsTable.tsx b/extensions/ql-vscode/src/view/variant-analysis/RawResultsTable.tsx similarity index 98% rename from extensions/ql-vscode/src/view/remote-queries/RawResultsTable.tsx rename to extensions/ql-vscode/src/view/variant-analysis/RawResultsTable.tsx index 6ad78c299..9078651fc 100644 --- a/extensions/ql-vscode/src/view/remote-queries/RawResultsTable.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/RawResultsTable.tsx @@ -8,7 +8,7 @@ import { ResultSetSchema, } from "../../pure/bqrs-cli-types"; import { tryGetRemoteLocation } from "../../pure/bqrs-utils"; -import TextButton from "./TextButton"; +import TextButton from "../common/TextButton"; import { convertNonPrintableChars } from "../../text-utils"; import { sendTelemetry, useTelemetryOnChange } from "../common/telemetry"; diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/databases/db-panel.test.ts b/extensions/ql-vscode/test/vscode-tests/activated-extension/databases/db-panel.test.ts similarity index 100% rename from extensions/ql-vscode/test/vscode-tests/cli-integration/databases/db-panel.test.ts rename to extensions/ql-vscode/test/vscode-tests/activated-extension/databases/db-panel.test.ts diff --git a/extensions/ql-vscode/test/vscode-tests/activated-extension/jest-runner-vscode.config.js b/extensions/ql-vscode/test/vscode-tests/activated-extension/jest-runner-vscode.config.js new file mode 100644 index 000000000..9f95a10f0 --- /dev/null +++ b/extensions/ql-vscode/test/vscode-tests/activated-extension/jest-runner-vscode.config.js @@ -0,0 +1,29 @@ +const path = require("path"); + +const { + config: baseConfig, + rootDir, +} = require("../jest-runner-vscode.config.base"); + +/** @type import("jest-runner-vscode").RunnerOptions */ +const config = { + ...baseConfig, + launchArgs: [ + ...(baseConfig.launchArgs ?? []), + // explicitly disable extensions that are known to interfere with the CLI integration tests + "--disable-extension", + "eamodio.gitlens", + "--disable-extension", + "github.codespaces", + "--disable-extension", + "github.copilot", + path.resolve(rootDir, "test/data"), + ], + extensionTestsEnv: { + ...baseConfig.extensionTestsEnv, + INTEGRATION_TEST_MODE: "true", + }, + retries: 3, +}; + +module.exports = config; diff --git a/extensions/ql-vscode/test/vscode-tests/activated-extension/jest.config.ts b/extensions/ql-vscode/test/vscode-tests/activated-extension/jest.config.ts new file mode 100644 index 000000000..3bd6d399c --- /dev/null +++ b/extensions/ql-vscode/test/vscode-tests/activated-extension/jest.config.ts @@ -0,0 +1,11 @@ +import type { Config } from "jest"; + +import baseConfig from "../jest.config.base"; + +const config: Config = { + ...baseConfig, + runner: "/../jest-runner-installed-extensions.ts", + setupFilesAfterEnv: ["/jest.setup.ts"], +}; + +export default config; diff --git a/extensions/ql-vscode/test/vscode-tests/activated-extension/jest.setup.ts b/extensions/ql-vscode/test/vscode-tests/activated-extension/jest.setup.ts new file mode 100644 index 000000000..69c4f39f1 --- /dev/null +++ b/extensions/ql-vscode/test/vscode-tests/activated-extension/jest.setup.ts @@ -0,0 +1,12 @@ +import { + beforeAllAction, + beforeEachAction, +} from "../jest.activated-extension.setup"; + +beforeAll(async () => { + await beforeAllAction(); +}); + +beforeEach(async () => { + await beforeEachAction(); +}); diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/databaseFetcher.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/databaseFetcher.test.ts index c5f0ad8cb..6296fced5 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/databaseFetcher.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/databaseFetcher.test.ts @@ -8,7 +8,7 @@ import { importArchiveDatabase, promptImportInternetDatabase, } from "../../../src/databaseFetcher"; -import { cleanDatabases, dbLoc, DB_URL, storagePath } from "./global.helper"; +import { cleanDatabases, dbLoc, DB_URL, storagePath } from "../global.helper"; jest.setTimeout(60_000); diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/jest.config.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/jest.config.ts index 3a419bc65..3bd6d399c 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/jest.config.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/jest.config.ts @@ -4,7 +4,7 @@ import baseConfig from "../jest.config.base"; const config: Config = { ...baseConfig, - runner: "/jest-runner-cli-integration.ts", + runner: "/../jest-runner-installed-extensions.ts", setupFilesAfterEnv: ["/jest.setup.ts"], }; diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/jest.setup.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/jest.setup.ts index ed9ce1e25..416ca1ea9 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/jest.setup.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/jest.setup.ts @@ -1,28 +1,24 @@ +import { workspace } from "vscode"; + +import { + beforeAllAction, + beforeEachAction, +} from "../jest.activated-extension.setup"; +import * as tmp from "tmp"; import { - mkdirpSync, - existsSync, createWriteStream, + existsSync, + mkdirpSync, realpathSync, } from "fs-extra"; import { dirname } from "path"; +import { DB_URL, dbLoc, setStoragePath, storagePath } from "../global.helper"; import fetch from "node-fetch"; -import { DB_URL, dbLoc, setStoragePath, storagePath } from "./global.helper"; -import * as tmp from "tmp"; -import { CUSTOM_CODEQL_PATH_SETTING } from "../../../src/config"; -import { ConfigurationTarget, env, extensions, workspace } from "vscode"; -import { beforeEachAction } from "../test-config"; // create an extension storage location let removeStorage: tmp.DirResult["removeCallback"] | undefined; beforeAll(async () => { - // Set the CLI version here before activation to ensure we don't accidentally try to download a cli - await beforeEachAction(); - await CUSTOM_CODEQL_PATH_SETTING.updateValue( - process.env.CLI_PATH, - ConfigurationTarget.Workspace, - ); - // ensure the test database is downloaded mkdirpSync(dirname(dbLoc)); if (!existsSync(dbLoc)) { @@ -54,6 +50,14 @@ beforeAll(async () => { removeStorage = dir.removeCallback; + await beforeAllAction(); +}); + +beforeEach(async () => { + await beforeEachAction(); +}); + +beforeAll(() => { // check that the codeql folder is found in the workspace const folders = workspace.workspaceFolders; if (!folders) { @@ -70,20 +74,6 @@ beforeAll(async () => { ); } } - - // Activate the extension - await extensions.getExtension("GitHub.vscode-codeql")?.activate(); -}); - -beforeEach(async () => { - jest.spyOn(env, "openExternal").mockResolvedValue(false); - - await beforeEachAction(); - - await CUSTOM_CODEQL_PATH_SETTING.updateValue( - process.env.CLI_PATH, - ConfigurationTarget.Workspace, - ); }); // ensure extension is cleaned up. diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/new-query.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/new-query.test.ts index bb58940cb..a7d8f9d4b 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/new-query.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/new-query.test.ts @@ -11,7 +11,7 @@ import { describeWithCodeQL } from "../cli"; import { QueryServerClient } from "../../../src/query-server/queryserver-client"; import { extLogger, ProgressReporter } from "../../../src/common"; import { QueryResultType } from "../../../src/pure/new-messages"; -import { cleanDatabases, dbLoc, storagePath } from "./global.helper"; +import { cleanDatabases, dbLoc, storagePath } from "../global.helper"; import { importArchiveDatabase } from "../../../src/databaseFetcher"; const baseDir = join(__dirname, "../../../test/data"); diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts index 889f09b02..d4f328c75 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts @@ -17,7 +17,7 @@ import { load, dump } from "js-yaml"; import { DatabaseItem, DatabaseManager } from "../../../src/databases"; import { CodeQLExtensionInterface } from "../../../src/extension"; -import { cleanDatabases, dbLoc, storagePath } from "./global.helper"; +import { cleanDatabases, dbLoc, storagePath } from "../global.helper"; import { importArchiveDatabase } from "../../../src/databaseFetcher"; import { CodeQLCliServer } from "../../../src/cli"; import { describeWithCodeQL } from "../cli"; diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts index e115e3883..c5b8bc54c 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts @@ -31,7 +31,7 @@ import { fixWorkspaceReferences, restoreWorkspaceReferences, storagePath, -} from "../global.helper"; +} from "../../global.helper"; import { VariantAnalysisResultsManager } from "../../../../src/remote-queries/variant-analysis-results-manager"; import { createMockVariantAnalysis } from "../../../factories/remote-queries/shared/variant-analysis"; import * as VariantAnalysisModule from "../../../../src/remote-queries/shared/variant-analysis"; diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/remote-queries/variant-analysis-results-manager.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/remote-queries/variant-analysis-results-manager.test.ts index ef52edb14..43deb4a40 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/remote-queries/variant-analysis-results-manager.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/remote-queries/variant-analysis-results-manager.test.ts @@ -9,7 +9,7 @@ import * as fetchModule from "node-fetch"; import { VariantAnalysisResultsManager } from "../../../../src/remote-queries/variant-analysis-results-manager"; import { CodeQLCliServer } from "../../../../src/cli"; -import { storagePath } from "../global.helper"; +import { storagePath } from "../../global.helper"; import { faker } from "@faker-js/faker"; import { createMockVariantAnalysisRepositoryTask } from "../../../factories/remote-queries/shared/variant-analysis-repo-tasks"; import { diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/global.helper.ts b/extensions/ql-vscode/test/vscode-tests/global.helper.ts similarity index 90% rename from extensions/ql-vscode/test/vscode-tests/cli-integration/global.helper.ts rename to extensions/ql-vscode/test/vscode-tests/global.helper.ts index 73a7a69a8..93b47e66f 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/global.helper.ts +++ b/extensions/ql-vscode/test/vscode-tests/global.helper.ts @@ -2,11 +2,11 @@ import { join } from "path"; import { load, dump } from "js-yaml"; import { realpathSync, readFileSync, writeFileSync } from "fs-extra"; import { commands } from "vscode"; -import { DatabaseManager } from "../../../src/databases"; -import { CodeQLCliServer } from "../../../src/cli"; -import { removeWorkspaceRefs } from "../../../src/remote-queries/run-remote-query"; +import { DatabaseManager } from "../../src/databases"; +import { CodeQLCliServer } from "../../src/cli"; +import { removeWorkspaceRefs } from "../../src/remote-queries/run-remote-query"; -// This file contains helpers shared between actual tests. +// This file contains helpers shared between tests that work with an activated extension. export const DB_URL = "https://github.com/github/vscode-codeql/files/5586722/simple-db.zip"; diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/jest-runner-cli-integration.ts b/extensions/ql-vscode/test/vscode-tests/jest-runner-installed-extensions.ts similarity index 94% rename from extensions/ql-vscode/test/vscode-tests/cli-integration/jest-runner-cli-integration.ts rename to extensions/ql-vscode/test/vscode-tests/jest-runner-installed-extensions.ts index 729624cb8..0c031b72d 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/jest-runner-cli-integration.ts +++ b/extensions/ql-vscode/test/vscode-tests/jest-runner-installed-extensions.ts @@ -8,9 +8,9 @@ import { downloadAndUnzipVSCode, resolveCliArgsFromVSCodeExecutablePath, } from "@vscode/test-electron"; -import { ensureCli } from "../ensureCli"; +import { ensureCli } from "./ensureCli"; -export default class JestRunnerCliIntegration extends VSCodeTestRunner { +export default class JestRunnerInstalledExtensions extends VSCodeTestRunner { async runTests( tests: JestRunner.Test[], watcher: JestRunner.TestWatcher, diff --git a/extensions/ql-vscode/test/vscode-tests/jest.activated-extension.setup.ts b/extensions/ql-vscode/test/vscode-tests/jest.activated-extension.setup.ts new file mode 100644 index 000000000..73886f2af --- /dev/null +++ b/extensions/ql-vscode/test/vscode-tests/jest.activated-extension.setup.ts @@ -0,0 +1,30 @@ +import { CUSTOM_CODEQL_PATH_SETTING } from "../../src/config"; +import { ConfigurationTarget, env, extensions } from "vscode"; +import { beforeEachAction as testConfigBeforeEachAction } from "./test-config"; + +jest.retryTimes(3, { + logErrorsBeforeRetry: true, +}); + +export async function beforeAllAction() { + // Set the CLI version here before activation to ensure we don't accidentally try to download a cli + await testConfigBeforeEachAction(); + await CUSTOM_CODEQL_PATH_SETTING.updateValue( + process.env.CLI_PATH, + ConfigurationTarget.Workspace, + ); + + // Activate the extension + await extensions.getExtension("GitHub.vscode-codeql")?.activate(); +} + +export async function beforeEachAction() { + jest.spyOn(env, "openExternal").mockResolvedValue(false); + + await testConfigBeforeEachAction(); + + await CUSTOM_CODEQL_PATH_SETTING.updateValue( + process.env.CLI_PATH, + ConfigurationTarget.Workspace, + ); +} diff --git a/extensions/ql-vscode/test/vscode-tests/jest.setup.ts b/extensions/ql-vscode/test/vscode-tests/jest.setup.ts index 55a546471..fe58b7f48 100644 --- a/extensions/ql-vscode/test/vscode-tests/jest.setup.ts +++ b/extensions/ql-vscode/test/vscode-tests/jest.setup.ts @@ -1,6 +1,10 @@ import { env } from "vscode"; import { beforeEachAction } from "./test-config"; +jest.retryTimes(3, { + logErrorsBeforeRetry: true, +}); + beforeEach(async () => { jest.spyOn(env, "openExternal").mockResolvedValue(false); diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts index 068be7385..cd81841cb 100644 --- a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts @@ -23,6 +23,7 @@ import { testDisposeHandler } from "../test-dispose-handler"; import { QueryRunner } from "../../../src/queryRunner"; import * as helpers from "../../../src/helpers"; import { Setting } from "../../../src/config"; +import { QlPackGenerator } from "../../../src/qlpack-generator"; describe("databases", () => { const MOCK_DB_OPTIONS: FullDatabaseOptions = { @@ -32,11 +33,13 @@ describe("databases", () => { }; let databaseManager: DatabaseManager; + let extensionContext: ExtensionContext; let updateSpy: jest.Mock, []>; let registerSpy: jest.Mock, []>; let deregisterSpy: jest.Mock, []>; let resolveDatabaseSpy: jest.Mock, []>; + let packAddSpy: jest.Mock; let logSpy: jest.Mock; let showBinaryChoiceDialogSpy: jest.SpiedFunction< @@ -52,6 +55,7 @@ describe("databases", () => { registerSpy = jest.fn(() => Promise.resolve(undefined)); deregisterSpy = jest.fn(() => Promise.resolve(undefined)); resolveDatabaseSpy = jest.fn(() => Promise.resolve({} as DbInfo)); + packAddSpy = jest.fn(); logSpy = jest.fn(() => { /* */ }); @@ -60,16 +64,19 @@ describe("databases", () => { .spyOn(helpers, "showBinaryChoiceDialog") .mockResolvedValue(true); + extensionContext = { + workspaceState: { + update: updateSpy, + get: () => [], + }, + // pretend like databases added in the temp dir are controlled by the extension + // so that they are deleted upon removal + storagePath: dir.name, + storageUri: Uri.parse(dir.name), + } as unknown as ExtensionContext; + databaseManager = new DatabaseManager( - { - workspaceState: { - update: updateSpy, - get: () => [], - }, - // pretend like databases added in the temp dir are controlled by the extension - // so that they are deleted upon removal - storagePath: dir.name, - } as unknown as ExtensionContext, + extensionContext, { registerDatabase: registerSpy, deregisterDatabase: deregisterSpy, @@ -79,6 +86,7 @@ describe("databases", () => { } as unknown as QueryRunner, { resolveDatabase: resolveDatabaseSpy, + packAdd: packAddSpy, } as unknown as CodeQLCliServer, { log: logSpy, @@ -589,20 +597,46 @@ describe("databases", () => { describe("createSkeletonPacks", () => { let mockDbItem: DatabaseItemImpl; + let language: string; + let generateSpy: jest.SpyInstance; + + beforeEach(() => { + language = "ruby"; + + const options: FullDatabaseOptions = { + dateAdded: 123, + ignoreSourceArchive: false, + language, + }; + mockDbItem = createMockDB(options); + + generateSpy = jest + .spyOn(QlPackGenerator.prototype, "generate") + .mockImplementation(() => Promise.resolve()); + }); describe("when the language is set", () => { it("should offer the user to set up a skeleton QL pack", async () => { - const options: FullDatabaseOptions = { - dateAdded: 123, - ignoreSourceArchive: false, - language: "ruby", - }; - mockDbItem = createMockDB(options); - await (databaseManager as any).createSkeletonPacks(mockDbItem); expect(showBinaryChoiceDialogSpy).toBeCalledTimes(1); }); + + it("should return early if the user refuses help", async () => { + showBinaryChoiceDialogSpy = jest + .spyOn(helpers, "showBinaryChoiceDialog") + .mockResolvedValue(false); + + await (databaseManager as any).createSkeletonPacks(mockDbItem); + + expect(generateSpy).not.toBeCalled(); + }); + + it("should create the skeleton QL pack for the user", async () => { + await (databaseManager as any).createSkeletonPacks(mockDbItem); + + expect(generateSpy).toBeCalled(); + }); }); describe("when the language is not set", () => { diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts new file mode 100644 index 000000000..504b7801b --- /dev/null +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts @@ -0,0 +1,74 @@ +import { join } from "path"; +import { existsSync } from "fs"; +import { QlPackGenerator, QueryLanguage } from "../../../src/qlpack-generator"; +import { CodeQLCliServer } from "../../../src/cli"; +import { Uri, workspace } from "vscode"; +import { getErrorMessage } from "../../../src/pure/helpers-pure"; +import * as tmp from "tmp"; + +describe("QlPackGenerator", () => { + let packFolderName: string; + let packFolderPath: string; + let qlPackYamlFilePath: string; + let exampleQlFilePath: string; + let language: string; + let generator: QlPackGenerator; + let packAddSpy: jest.SpyInstance; + let dir: tmp.DirResult; + + beforeEach(async () => { + dir = tmp.dirSync(); + + language = "ruby"; + packFolderName = `test-ql-pack-${language}`; + packFolderPath = Uri.file(join(dir.name, packFolderName)).fsPath; + + qlPackYamlFilePath = join(packFolderPath, "qlpack.yml"); + exampleQlFilePath = join(packFolderPath, "example.ql"); + + packAddSpy = jest.fn(); + const mockCli = { + packAdd: packAddSpy, + } as unknown as CodeQLCliServer; + + generator = new QlPackGenerator( + packFolderName, + language as QueryLanguage, + mockCli, + dir.name, + ); + }); + + afterEach(async () => { + try { + dir.removeCallback(); + + const workspaceFolders = workspace.workspaceFolders || []; + const folderIndex = workspaceFolders.findIndex( + (workspaceFolder) => workspaceFolder.name === dir.name, + ); + + if (folderIndex !== undefined) { + workspace.updateWorkspaceFolders(folderIndex, 1); + } + } catch (e) { + console.log( + `Could not remove folder from workspace: ${getErrorMessage(e)}`, + ); + } + }); + + it("should generate a QL pack", async () => { + expect(existsSync(packFolderPath)).toBe(false); + expect(existsSync(qlPackYamlFilePath)).toBe(false); + expect(existsSync(exampleQlFilePath)).toBe(false); + + await generator.generate(); + + expect(existsSync(packFolderPath)).toBe(true); + expect(existsSync(qlPackYamlFilePath)).toBe(true); + expect(existsSync(exampleQlFilePath)).toBe(true); + + expect(packAddSpy).toHaveBeenCalledWith(packFolderPath, language); + }); +});