Merge branch 'main' into robertbrignull/enable-await-thenable

This commit is contained in:
Robert
2023-02-15 10:28:43 +00:00
52 changed files with 413 additions and 1151 deletions

View File

@@ -132,7 +132,12 @@ jobs:
- name: Run unit tests - name: Run unit tests
working-directory: extensions/ql-vscode working-directory: extensions/ql-vscode
run: | run: |
npm run test npm run test:unit
- name: Run view tests
working-directory: extensions/ql-vscode
run: |
npm run test:view
test: test:
name: Test name: Test
@@ -173,7 +178,7 @@ jobs:
VSCODE_CODEQL_GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' VSCODE_CODEQL_GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: | run: |
unset DBUS_SESSION_BUS_ADDRESS 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) - name: Run integration tests (Windows)
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
@@ -181,7 +186,7 @@ jobs:
env: env:
VSCODE_CODEQL_GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' VSCODE_CODEQL_GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: | run: |
npm run integration npm run test:vscode-integration
set-matrix: set-matrix:
name: Set Matrix for cli-test name: Set Matrix for cli-test
@@ -254,10 +259,10 @@ jobs:
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
run: | run: |
unset DBUS_SESSION_BUS_ADDRESS 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) - name: Run CLI tests (Windows)
working-directory: extensions/ql-vscode working-directory: extensions/ql-vscode
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
run: | run: |
npm run cli-integration npm run test:cli-integration

View File

@@ -98,10 +98,11 @@ We have several types of tests:
* Unit tests: these live in the `tests/unit-tests/` directory * Unit tests: these live in the `tests/unit-tests/` directory
* View tests: these live in `src/view/variant-analysis/__tests__/` * View tests: these live in `src/view/variant-analysis/__tests__/`
* VSCode integration 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. * `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` * 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. 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` * Unit tests: `npm run test:unit`
* View Tests: `npm test:view` * View Tests: `npm test:view`
* VSCode integration tests: `npm run integration` * VSCode integration tests: `npm run test:vscode-integration`
###### CLI integration tests ###### 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: 2. Run your test command:
```shell ```shell
cd extensions/ql-vscode && npm run cli-integration cd extensions/ql-vscode && npm run test:cli-integration
``` ```
##### 2. From VSCode ##### 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`: to only run tests for this specific file. For example, to run the test `test/vscode-tests/cli-integration/run-queries.test.ts`:
```shell ```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`: 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 ```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 ##### 2. From VSCode

View File

@@ -8,6 +8,7 @@ module.exports = {
projects: [ projects: [
"<rootDir>/src/view", "<rootDir>/src/view",
"<rootDir>/test/unit-tests", "<rootDir>/test/unit-tests",
"<rootDir>/test/vscode-tests/activated-extension",
"<rootDir>/test/vscode-tests/cli-integration", "<rootDir>/test/vscode-tests/cli-integration",
"<rootDir>/test/vscode-tests/no-workspace", "<rootDir>/test/vscode-tests/no-workspace",
"<rootDir>/test/vscode-tests/minimal-workspace", "<rootDir>/test/vscode-tests/minimal-workspace",

View File

@@ -1326,13 +1326,14 @@
"scripts": { "scripts": {
"build": "gulp", "build": "gulp",
"watch": "gulp watch", "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:unit": "cross-env TZ=UTC LANG=en-US jest --projects test/unit-tests",
"test:view": "jest --projects src/view", "test:view": "jest --projects src/view",
"integration": "npm-run-all integration:*", "test:vscode-integration": "npm-run-all test:vscode-integration:*",
"integration:no-workspace": "jest --projects test/vscode-tests/no-workspace", "test:vscode-integration:activated-extension": "jest --projects test/vscode-tests/activated-extension",
"integration:minimal-workspace": "jest --projects test/vscode-tests/minimal-workspace", "test:vscode-integration:no-workspace": "jest --projects test/vscode-tests/no-workspace",
"cli-integration": "jest --projects test/vscode-tests/cli-integration", "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", "update-vscode": "node ./node_modules/vscode/bin/install",
"format": "prettier --write **/*.{ts,tsx} && eslint . --ext .ts,.tsx --fix", "format": "prettier --write **/*.{ts,tsx} && eslint . --ext .ts,.tsx --fix",
"lint": "eslint . --ext .js,.ts,.tsx --max-warnings=0", "lint": "eslint . --ext .js,.ts,.tsx --max-warnings=0",

View File

@@ -28,6 +28,7 @@ import { CompilationMessage } from "./pure/legacy-messages";
import { sarifParser } from "./sarif-parser"; import { sarifParser } from "./sarif-parser";
import { dbSchemeToLanguage, walkDirectory } from "./helpers"; import { dbSchemeToLanguage, walkDirectory } from "./helpers";
import { App } from "./common/app"; import { App } from "./common/app";
import { QueryLanguage } from "./qlpack-generator";
/** /**
* The version of the SARIF format that we are using. * 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. * Downloads a specified pack.
* @param packs The `<package-scope/name[@version]>` of the packs to download. * @param packs The `<package-scope/name[@version]>` of the packs to download.

View File

@@ -26,6 +26,7 @@ import { QueryRunner } from "./queryRunner";
import { pathsEqual } from "./pure/files"; import { pathsEqual } from "./pure/files";
import { redactableError } from "./pure/errors"; import { redactableError } from "./pure/errors";
import { isCodespacesTemplate } from "./config"; import { isCodespacesTemplate } from "./config";
import { QlPackGenerator, QueryLanguage } from "./qlpack-generator";
/** /**
* databases.ts * databases.ts
@@ -655,9 +656,27 @@ export class DatabaseManager extends DisposableObject {
return; return;
} }
await showBinaryChoiceDialog( const answer = 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`, `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( private async reregisterDatabases(

View File

@@ -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);
}
}

View File

@@ -2,7 +2,7 @@ import * as React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react"; import { ComponentStory, ComponentMeta } from "@storybook/react";
import TextButtonComponent from "../../view/remote-queries/TextButton"; import TextButtonComponent from "../../view/common/TextButton";
export default { export default {
title: "Text Button", title: "Text Button",

View File

@@ -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<typeof DownloadButtonComponent>;
const Template: ComponentStory<typeof DownloadButtonComponent> = (args) => (
<DownloadButtonComponent {...args} />
);
export const DownloadButton = Template.bind({});
DownloadButton.args = {
text: "Download",
};

View File

@@ -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<typeof DownloadSpinnerComponent>;
export const DownloadSpinner = <DownloadSpinnerComponent />;

View File

@@ -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<typeof LastUpdatedComponent>;
const Template: ComponentStory<typeof LastUpdatedComponent> = (args) => (
<LastUpdatedComponent {...args} />
);
export const LastUpdated = Template.bind({});
LastUpdated.args = {
lastUpdated: -3_600_000, // 1 hour ago
};

View File

@@ -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<typeof RemoteQueries>;
const Template: ComponentStory<typeof RemoteQueries> = () => {
useEffect(() => {
window.postMessage(remoteQueryResult);
window.postMessage(analysesResults);
});
return <RemoteQueries />;
};
export const Top10JavaScript = Template.bind({});

View File

@@ -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<typeof RepositoriesSearchComponent>;
export const RepositoriesSearch = () => {
const [filterValue, setFilterValue] = useState("");
return (
<RepositoriesSearchComponent
filterValue={filterValue}
setFilterValue={setFilterValue}
/>
);
};

View File

@@ -2,11 +2,11 @@ import * as React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/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"; import type { AnalysisAlert } from "../../remote-queries/shared/analysis-result";
export default { export default {
title: "Analysis Alert Result", title: "Variant Analysis/Analysis Alert Result",
component: AnalysisAlertResult, component: AnalysisAlertResult,
} as ComponentMeta<typeof AnalysisAlertResult>; } as ComponentMeta<typeof AnalysisAlertResult>;

View File

@@ -13,8 +13,8 @@ import {
} from "../../remote-queries/shared/analysis-result"; } from "../../remote-queries/shared/analysis-result";
import { createMockRepositoryWithMetadata } from "../../../test/factories/remote-queries/shared/repository"; import { createMockRepositoryWithMetadata } from "../../../test/factories/remote-queries/shared/repository";
import * as analysesResults from "../remote-queries/data/analysesResultsMessage.json"; import * as analysesResults from "../data/analysesResultsMessage.json";
import * as rawResults from "../remote-queries/data/rawResults.json"; import * as rawResults from "../data/rawResults.json";
import { RepoRow, RepoRowProps } from "../../view/variant-analysis/RepoRow"; import { RepoRow, RepoRowProps } from "../../view/variant-analysis/RepoRow";
export default { export default {

View File

@@ -16,7 +16,7 @@ import { createMockVariantAnalysis } from "../../../test/factories/remote-querie
import { createMockRepositoryWithMetadata } from "../../../test/factories/remote-queries/shared/repository"; import { createMockRepositoryWithMetadata } from "../../../test/factories/remote-queries/shared/repository";
import { createMockScannedRepo } from "../../../test/factories/remote-queries/shared/scanned-repositories"; 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 { export default {
title: "Variant Analysis/Analyzed Repos", title: "Variant Analysis/Analyzed Repos",

View File

@@ -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 (
<>
<Container onClick={() => setExpanded(!isExpanded)}>
<Button>
{isExpanded ? (
<ChevronDownIcon size={16} />
) : (
<ChevronRightIcon size={16} />
)}
</Button>
<TitleContainer>{title}</TitleContainer>
</Container>
{isExpanded && children}
</>
);
};
export default CollapsibleItem;

View File

@@ -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;
}) => (
<ButtonLink onClick={onClick}>
<DownloadIcon size={16} />
{text}
</ButtonLink>
);
export default DownloadButton;

View File

@@ -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 = () => (
<SpinnerContainer>
<VSCodeProgressRing style={{ height: "0.8em", width: "0.8em" }} />
</SpinnerContainer>
);
export default DownloadSpinner;

View File

@@ -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(
<>
<Container>
<CloseButton onClick={() => setOpen(false)}>
<XCircleIcon size={24} />
</CloseButton>
{children}
</Container>
</>,
containerElement,
);
};
export default FullScreenModal;

View File

@@ -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) ? (
<>
<IconContainer>
<RepoPushIcon size={16} />
</IconContainer>
<Duration>{humanizeRelativeTime(lastUpdated)}</Duration>
</>
) : (
<></>
);
export default LastUpdated;

View File

@@ -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) => (
<>
<VerticalSpace size={1} />
{createResultsDescription(queryResult)}
<VerticalSpace size={1} />
<span>
<a
className="vscode-codeql__query-info-link"
href="#"
onClick={() => openQueryFile(queryResult)}
>
<span>
{" "}
<FileCodeIcon size={16} />{" "}
</span>
{queryResult.queryFileName}
</a>
</span>
<span>
<a
className="vscode-codeql__query-info-link"
href="#"
onClick={() => openQueryTextVirtualFile(queryResult)}
>
<span>
{" "}
<CodeSquareIcon size={16} />{" "}
</span>
Query
</a>
</span>
<span>
<a
className="vscode-codeql__query-info-link"
href={queryResult.workflowRunUrl}
>
<span>
{" "}
<TerminalIcon size={16} />{" "}
</span>
Logs
</a>
</span>
</>
);
const Failures = (queryResult: RemoteQueryResult) => {
if (queryResult.analysisFailures.length === 0) {
return <></>;
}
return (
<>
<VerticalSpace size={3} />
<Flash variant="danger">
{queryResult.analysisFailures.map((f, i) => (
<div key={i}>
<p className="vscode-codeql__analysis-failure">
<AlertIcon size={16} />
<b>{f.nwo}: </b>
{f.error}
</p>
{i === queryResult.analysisFailures.length - 1 ? (
<></>
) : (
<VerticalSpace size={1} />
)}
</div>
))}
</Flash>
</>
);
};
const SummaryTitleWithResults = ({
queryResult,
analysesResults,
sort,
setSort,
}: {
queryResult: RemoteQueryResult;
analysesResults: AnalysisResults[];
sort: Sort;
setSort: (sort: Sort) => void;
}) => {
const showDownloadButton =
queryResult.totalResultCount !== sumAnalysesResults(analysesResults);
return (
<div className="vscode-codeql__query-summary-container">
<SectionTitle>
Repositories with results ({queryResult.affectedRepositoryCount}):
</SectionTitle>
{showDownloadButton && (
<DownloadButton
text="Download all"
onClick={() => downloadAllAnalysesResults(queryResult)}
/>
)}
<div style={{ flexGrow: 2, textAlign: "right" }}>
<RepoListCopyButton queryResult={queryResult} />
<HorizontalSpace size={1} />
<SortRepoFilter sort={sort} setSort={setSort} />
</div>
</div>
);
};
const SummaryTitleNoResults = () => (
<div className="vscode-codeql__query-summary-container">
<SectionTitle>No results found</SectionTitle>
</div>
);
const SummaryItemDownload = ({
analysisSummary,
analysisResults,
}: {
analysisSummary: AnalysisSummary;
analysisResults: AnalysisResults | undefined;
}) => {
if (!analysisResults || analysisResults.status === "Failed") {
return (
<DownloadButton
text={analysisSummary.fileSize}
onClick={() => downloadAnalysisResults(analysisSummary)}
/>
);
}
if (analysisResults.status === "InProgress") {
return (
<>
<HorizontalSpace size={2} />
<DownloadSpinner />
</>
);
}
return <></>;
};
const SummaryItem = ({
analysisSummary,
analysisResults,
}: {
analysisSummary: AnalysisSummary;
analysisResults: AnalysisResults | undefined;
}) => (
<>
<span className="vscode-codeql__analysis-item">
<RepoIcon size={16} />
</span>
<span className="vscode-codeql__analysis-item">{analysisSummary.nwo}</span>
<HorizontalSpace size={1} />
<span className="vscode-codeql__analysis-item">
<VSCodeBadge>{analysisSummary.resultCount.toString()}</VSCodeBadge>
</span>
<span className="vscode-codeql__analysis-item">
<SummaryItemDownload
analysisSummary={analysisSummary}
analysisResults={analysisResults}
/>
</span>
<StarCount starCount={analysisSummary.starCount} />
<LastUpdated lastUpdated={analysisSummary.lastUpdated} />
</>
);
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 ? (
<SummaryTitleNoResults />
) : (
<SummaryTitleWithResults
queryResult={queryResult}
analysesResults={analysesResults}
sort={sort}
setSort={setSort}
/>
)}
<ul className="vscode-codeql__flat-list">
{queryResult.analysisSummaries
.slice(0, numOfReposToShow)
.sort(sorter(sort))
.map((summary, i) => (
<li
key={summary.nwo}
className="vscode-codeql__analysis-summaries-list-item"
>
<SummaryItem
analysisSummary={summary}
analysisResults={analysesResults.find(
(a) => a.nwo === summary.nwo,
)}
/>
</li>
))}
</ul>
{queryResult.analysisSummaries.length > numOfReposInContractedMode && (
<button
className="vscode-codeql__expand-button"
onClick={() => setRepoListExpanded(!repoListExpanded)}
>
{repoListExpanded ? <span>View less</span> : <span>View all</span>}
</button>
)}
</>
);
};
const AnalysesResultsTitle = ({
totalAnalysesResults,
totalResults,
}: {
totalAnalysesResults: number;
totalResults: number;
}) => {
if (totalAnalysesResults === totalResults) {
return <SectionTitle>{totalAnalysesResults} results</SectionTitle>;
}
return (
<SectionTitle>
{totalAnalysesResults}/{totalResults} results
</SectionTitle>
);
};
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 = (
<>
<VerticalSpace size={1} />
Some results haven&apos;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 = (
<>
<VerticalSpace size={1} />
Some repositories have more than {MAX_RAW_RESULTS} results. We will only
show you up to&nbsp;
{MAX_RAW_RESULTS} results for each repository.
</>
);
return (
<>
{showDownloadsMessage && downloadsMessage}
{showMaxResultsMessage && maxRawResultsMessage}
</>
);
};
const RepoAnalysisResults = (analysisResults: AnalysisResults) => {
const numOfResults = getAnalysisResultCount(analysisResults);
const title = (
<>
{analysisResults.nwo}
<HorizontalSpace size={1} />
<VSCodeBadge>{numOfResults.toString()}</VSCodeBadge>
</>
);
return (
<CollapsibleItem title={title}>
<ul className="vscode-codeql__flat-list">
{analysisResults.interpretedResults.map((r, i) => (
<li key={i}>
<AnalysisAlertResult alert={r} />
<VerticalSpace size={2} />
</li>
))}
</ul>
{analysisResults.rawResults && (
<RawResultsTable
schema={analysisResults.rawResults.schema}
results={analysisResults.rawResults.resultSet}
fileLinkPrefix={analysisResults.rawResults.fileLinkPrefix}
sourceLocationPrefix={analysisResults.rawResults.sourceLocationPrefix}
/>
)}
</CollapsibleItem>
);
};
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 (
<>
<VerticalSpace size={2} />
<div style={{ display: "flex" }}>
<div style={{ flexGrow: 1 }}>
<AnalysesResultsTitle
totalAnalysesResults={totalAnalysesResults}
totalResults={totalResults}
/>
</div>
<div>
<VSCodeButton onClick={() => exportResults(queryResult)}>
Export all
</VSCodeButton>
</div>
</div>
<AnalysesResultsDescription
queryResult={queryResult}
analysesResults={analysesResults}
/>
<VerticalSpace size={2} />
<RepositoriesSearch
filterValue={filterValue}
setFilterValue={setFilterValue}
/>
<ul className="vscode-codeql__flat-list">
{analysesResults
.filter(
(a) =>
a.interpretedResults.length ||
a.rawResults?.resultSet?.rows?.length,
)
.filter((a) =>
a.nwo.toLowerCase().includes(filterValue.toLowerCase()),
)
.sort(sorter(sort))
.map((r) => (
<li
key={r.nwo}
className="vscode-codeql__analyses-results-list-item"
>
<RepoAnalysisResults {...r} />
</li>
))}
</ul>
</>
);
};
export function RemoteQueries(): JSX.Element {
const [queryResult, setQueryResult] =
useState<RemoteQueryResult>(emptyQueryResult);
const [analysesResults, setAnalysesResults] = useState<AnalysisResults[]>([]);
const [sort, setSort] = useState<Sort>("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 <div>Waiting for results to load.</div>;
}
try {
return (
<div className="vscode-codeql__remote-queries">
<ThemeProvider colorMode="auto">
<ViewTitle>{queryResult.queryTitle}</ViewTitle>
<QueryInfo {...queryResult} />
<Failures {...queryResult} />
<Summary
queryResult={queryResult}
analysesResults={analysesResults}
sort={sort}
setSort={setSort}
/>
<AnalysesResults
queryResult={queryResult}
analysesResults={analysesResults}
totalResults={queryResult.totalResultCount}
sort={sort}
/>
</ThemeProvider>
</div>
);
} catch (err) {
console.error(err);
return <div>There was an error displaying the view.</div>;
}
}

View File

@@ -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;
}) => (
<IconButton
aria-label="Copy repository list"
icon={CopyIcon}
variant="invisible"
size="small"
sx={{ "text-align": "right" }}
onClick={() => copyRepositoryList(queryResult)}
/>
);
export default RepoListCopyButton;

View File

@@ -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 (
<>
<VSCodeTextField
style={{ width: "100%" }}
placeholder="Filter by repository owner/name"
ariaLabel="Repository search"
name="repository-search"
value={filterValue}
onInput={(e: InputEvent) =>
setFilterValue((e.target as HTMLInputElement).value)
}
>
<span slot="start" className="codicon codicon-search"></span>
</VSCodeTextField>
</>
);
};
export default RepositoriesSearch;

View File

@@ -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 (
<SortWrapper>
<ActionMenu>
<ActionMenu.Anchor>
<IconButton
icon={FilterIcon}
variant="invisible"
aria-label="Sort results"
/>
</ActionMenu.Anchor>
<ActionMenu.Overlay width="small" anchorSide="outside-bottom">
<ActionList selectionVariant="single">
{sortBy.map((type, index) => (
<ActionList.Item
key={index}
selected={type.sort === sort}
onSelect={() => setSort(type.sort as Sort)}
>
{type.name}
</ActionList.Item>
))}
</ActionList>
</ActionMenu.Overlay>
</ActionMenu>
</SortWrapper>
);
};
export default SortRepoFilter;

View File

@@ -1,4 +0,0 @@
body {
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
sans-serif, Apple Color Emoji, Segoe UI Emoji;
}

View File

@@ -1,9 +0,0 @@
import * as React from "react";
import { WebviewDefinition } from "../webview-definition";
import { RemoteQueries } from "./RemoteQueries";
const definition: WebviewDefinition = {
component: <RemoteQueries />,
};
export default definition;

View File

@@ -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;
}

View File

@@ -4,8 +4,8 @@ import {
AnalysisAlert, AnalysisAlert,
AnalysisRawResults, AnalysisRawResults,
} from "../../remote-queries/shared/analysis-result"; } from "../../remote-queries/shared/analysis-result";
import AnalysisAlertResult from "../remote-queries/AnalysisAlertResult"; import AnalysisAlertResult from "./AnalysisAlertResult";
import RawResultsTable from "../remote-queries/RawResultsTable"; import RawResultsTable from "./RawResultsTable";
import { import {
VariantAnalysisRepoStatus, VariantAnalysisRepoStatus,
VariantAnalysisScannedRepositoryDownloadStatus, VariantAnalysisScannedRepositoryDownloadStatus,

View File

@@ -8,7 +8,7 @@ import {
ResultSetSchema, ResultSetSchema,
} from "../../pure/bqrs-cli-types"; } from "../../pure/bqrs-cli-types";
import { tryGetRemoteLocation } from "../../pure/bqrs-utils"; import { tryGetRemoteLocation } from "../../pure/bqrs-utils";
import TextButton from "./TextButton"; import TextButton from "../common/TextButton";
import { convertNonPrintableChars } from "../../text-utils"; import { convertNonPrintableChars } from "../../text-utils";
import { sendTelemetry, useTelemetryOnChange } from "../common/telemetry"; import { sendTelemetry, useTelemetryOnChange } from "../common/telemetry";

View File

@@ -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;

View File

@@ -0,0 +1,11 @@
import type { Config } from "jest";
import baseConfig from "../jest.config.base";
const config: Config = {
...baseConfig,
runner: "<rootDir>/../jest-runner-installed-extensions.ts",
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
};
export default config;

View File

@@ -0,0 +1,12 @@
import {
beforeAllAction,
beforeEachAction,
} from "../jest.activated-extension.setup";
beforeAll(async () => {
await beforeAllAction();
});
beforeEach(async () => {
await beforeEachAction();
});

View File

@@ -8,7 +8,7 @@ import {
importArchiveDatabase, importArchiveDatabase,
promptImportInternetDatabase, promptImportInternetDatabase,
} from "../../../src/databaseFetcher"; } 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); jest.setTimeout(60_000);

View File

@@ -4,7 +4,7 @@ import baseConfig from "../jest.config.base";
const config: Config = { const config: Config = {
...baseConfig, ...baseConfig,
runner: "<rootDir>/jest-runner-cli-integration.ts", runner: "<rootDir>/../jest-runner-installed-extensions.ts",
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"], setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
}; };

View File

@@ -1,28 +1,24 @@
import { workspace } from "vscode";
import {
beforeAllAction,
beforeEachAction,
} from "../jest.activated-extension.setup";
import * as tmp from "tmp";
import { import {
mkdirpSync,
existsSync,
createWriteStream, createWriteStream,
existsSync,
mkdirpSync,
realpathSync, realpathSync,
} from "fs-extra"; } from "fs-extra";
import { dirname } from "path"; import { dirname } from "path";
import { DB_URL, dbLoc, setStoragePath, storagePath } from "../global.helper";
import fetch from "node-fetch"; 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 // create an extension storage location
let removeStorage: tmp.DirResult["removeCallback"] | undefined; let removeStorage: tmp.DirResult["removeCallback"] | undefined;
beforeAll(async () => { 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 // ensure the test database is downloaded
mkdirpSync(dirname(dbLoc)); mkdirpSync(dirname(dbLoc));
if (!existsSync(dbLoc)) { if (!existsSync(dbLoc)) {
@@ -54,6 +50,14 @@ beforeAll(async () => {
removeStorage = dir.removeCallback; removeStorage = dir.removeCallback;
await beforeAllAction();
});
beforeEach(async () => {
await beforeEachAction();
});
beforeAll(() => {
// check that the codeql folder is found in the workspace // check that the codeql folder is found in the workspace
const folders = workspace.workspaceFolders; const folders = workspace.workspaceFolders;
if (!folders) { 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. // ensure extension is cleaned up.

View File

@@ -11,7 +11,7 @@ import { describeWithCodeQL } from "../cli";
import { QueryServerClient } from "../../../src/query-server/queryserver-client"; import { QueryServerClient } from "../../../src/query-server/queryserver-client";
import { extLogger, ProgressReporter } from "../../../src/common"; import { extLogger, ProgressReporter } from "../../../src/common";
import { QueryResultType } from "../../../src/pure/new-messages"; 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"; import { importArchiveDatabase } from "../../../src/databaseFetcher";
const baseDir = join(__dirname, "../../../test/data"); const baseDir = join(__dirname, "../../../test/data");

View File

@@ -17,7 +17,7 @@ import { load, dump } from "js-yaml";
import { DatabaseItem, DatabaseManager } from "../../../src/databases"; import { DatabaseItem, DatabaseManager } from "../../../src/databases";
import { CodeQLExtensionInterface } from "../../../src/extension"; 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 { importArchiveDatabase } from "../../../src/databaseFetcher";
import { CodeQLCliServer } from "../../../src/cli"; import { CodeQLCliServer } from "../../../src/cli";
import { describeWithCodeQL } from "../cli"; import { describeWithCodeQL } from "../cli";

View File

@@ -31,7 +31,7 @@ import {
fixWorkspaceReferences, fixWorkspaceReferences,
restoreWorkspaceReferences, restoreWorkspaceReferences,
storagePath, storagePath,
} from "../global.helper"; } from "../../global.helper";
import { VariantAnalysisResultsManager } from "../../../../src/remote-queries/variant-analysis-results-manager"; import { VariantAnalysisResultsManager } from "../../../../src/remote-queries/variant-analysis-results-manager";
import { createMockVariantAnalysis } from "../../../factories/remote-queries/shared/variant-analysis"; import { createMockVariantAnalysis } from "../../../factories/remote-queries/shared/variant-analysis";
import * as VariantAnalysisModule from "../../../../src/remote-queries/shared/variant-analysis"; import * as VariantAnalysisModule from "../../../../src/remote-queries/shared/variant-analysis";

View File

@@ -9,7 +9,7 @@ import * as fetchModule from "node-fetch";
import { VariantAnalysisResultsManager } from "../../../../src/remote-queries/variant-analysis-results-manager"; import { VariantAnalysisResultsManager } from "../../../../src/remote-queries/variant-analysis-results-manager";
import { CodeQLCliServer } from "../../../../src/cli"; import { CodeQLCliServer } from "../../../../src/cli";
import { storagePath } from "../global.helper"; import { storagePath } from "../../global.helper";
import { faker } from "@faker-js/faker"; import { faker } from "@faker-js/faker";
import { createMockVariantAnalysisRepositoryTask } from "../../../factories/remote-queries/shared/variant-analysis-repo-tasks"; import { createMockVariantAnalysisRepositoryTask } from "../../../factories/remote-queries/shared/variant-analysis-repo-tasks";
import { import {

View File

@@ -2,11 +2,11 @@ import { join } from "path";
import { load, dump } from "js-yaml"; import { load, dump } from "js-yaml";
import { realpathSync, readFileSync, writeFileSync } from "fs-extra"; import { realpathSync, readFileSync, writeFileSync } from "fs-extra";
import { commands } from "vscode"; import { commands } from "vscode";
import { DatabaseManager } from "../../../src/databases"; import { DatabaseManager } from "../../src/databases";
import { CodeQLCliServer } from "../../../src/cli"; import { CodeQLCliServer } from "../../src/cli";
import { removeWorkspaceRefs } from "../../../src/remote-queries/run-remote-query"; 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 = export const DB_URL =
"https://github.com/github/vscode-codeql/files/5586722/simple-db.zip"; "https://github.com/github/vscode-codeql/files/5586722/simple-db.zip";

View File

@@ -8,9 +8,9 @@ import {
downloadAndUnzipVSCode, downloadAndUnzipVSCode,
resolveCliArgsFromVSCodeExecutablePath, resolveCliArgsFromVSCodeExecutablePath,
} from "@vscode/test-electron"; } 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( async runTests(
tests: JestRunner.Test[], tests: JestRunner.Test[],
watcher: JestRunner.TestWatcher, watcher: JestRunner.TestWatcher,

View File

@@ -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,
);
}

View File

@@ -1,6 +1,10 @@
import { env } from "vscode"; import { env } from "vscode";
import { beforeEachAction } from "./test-config"; import { beforeEachAction } from "./test-config";
jest.retryTimes(3, {
logErrorsBeforeRetry: true,
});
beforeEach(async () => { beforeEach(async () => {
jest.spyOn(env, "openExternal").mockResolvedValue(false); jest.spyOn(env, "openExternal").mockResolvedValue(false);

View File

@@ -23,6 +23,7 @@ import { testDisposeHandler } from "../test-dispose-handler";
import { QueryRunner } from "../../../src/queryRunner"; import { QueryRunner } from "../../../src/queryRunner";
import * as helpers from "../../../src/helpers"; import * as helpers from "../../../src/helpers";
import { Setting } from "../../../src/config"; import { Setting } from "../../../src/config";
import { QlPackGenerator } from "../../../src/qlpack-generator";
describe("databases", () => { describe("databases", () => {
const MOCK_DB_OPTIONS: FullDatabaseOptions = { const MOCK_DB_OPTIONS: FullDatabaseOptions = {
@@ -32,11 +33,13 @@ describe("databases", () => {
}; };
let databaseManager: DatabaseManager; let databaseManager: DatabaseManager;
let extensionContext: ExtensionContext;
let updateSpy: jest.Mock<Promise<void>, []>; let updateSpy: jest.Mock<Promise<void>, []>;
let registerSpy: jest.Mock<Promise<void>, []>; let registerSpy: jest.Mock<Promise<void>, []>;
let deregisterSpy: jest.Mock<Promise<void>, []>; let deregisterSpy: jest.Mock<Promise<void>, []>;
let resolveDatabaseSpy: jest.Mock<Promise<DbInfo>, []>; let resolveDatabaseSpy: jest.Mock<Promise<DbInfo>, []>;
let packAddSpy: jest.Mock<any, []>;
let logSpy: jest.Mock<any, []>; let logSpy: jest.Mock<any, []>;
let showBinaryChoiceDialogSpy: jest.SpiedFunction< let showBinaryChoiceDialogSpy: jest.SpiedFunction<
@@ -52,6 +55,7 @@ describe("databases", () => {
registerSpy = jest.fn(() => Promise.resolve(undefined)); registerSpy = jest.fn(() => Promise.resolve(undefined));
deregisterSpy = jest.fn(() => Promise.resolve(undefined)); deregisterSpy = jest.fn(() => Promise.resolve(undefined));
resolveDatabaseSpy = jest.fn(() => Promise.resolve({} as DbInfo)); resolveDatabaseSpy = jest.fn(() => Promise.resolve({} as DbInfo));
packAddSpy = jest.fn();
logSpy = jest.fn(() => { logSpy = jest.fn(() => {
/* */ /* */
}); });
@@ -60,8 +64,7 @@ describe("databases", () => {
.spyOn(helpers, "showBinaryChoiceDialog") .spyOn(helpers, "showBinaryChoiceDialog")
.mockResolvedValue(true); .mockResolvedValue(true);
databaseManager = new DatabaseManager( extensionContext = {
{
workspaceState: { workspaceState: {
update: updateSpy, update: updateSpy,
get: () => [], get: () => [],
@@ -69,7 +72,11 @@ describe("databases", () => {
// pretend like databases added in the temp dir are controlled by the extension // pretend like databases added in the temp dir are controlled by the extension
// so that they are deleted upon removal // so that they are deleted upon removal
storagePath: dir.name, storagePath: dir.name,
} as unknown as ExtensionContext, storageUri: Uri.parse(dir.name),
} as unknown as ExtensionContext;
databaseManager = new DatabaseManager(
extensionContext,
{ {
registerDatabase: registerSpy, registerDatabase: registerSpy,
deregisterDatabase: deregisterSpy, deregisterDatabase: deregisterSpy,
@@ -79,6 +86,7 @@ describe("databases", () => {
} as unknown as QueryRunner, } as unknown as QueryRunner,
{ {
resolveDatabase: resolveDatabaseSpy, resolveDatabase: resolveDatabaseSpy,
packAdd: packAddSpy,
} as unknown as CodeQLCliServer, } as unknown as CodeQLCliServer,
{ {
log: logSpy, log: logSpy,
@@ -589,20 +597,46 @@ describe("databases", () => {
describe("createSkeletonPacks", () => { describe("createSkeletonPacks", () => {
let mockDbItem: DatabaseItemImpl; let mockDbItem: DatabaseItemImpl;
let language: string;
let generateSpy: jest.SpyInstance;
beforeEach(() => {
language = "ruby";
describe("when the language is set", () => {
it("should offer the user to set up a skeleton QL pack", async () => {
const options: FullDatabaseOptions = { const options: FullDatabaseOptions = {
dateAdded: 123, dateAdded: 123,
ignoreSourceArchive: false, ignoreSourceArchive: false,
language: "ruby", language,
}; };
mockDbItem = createMockDB(options); 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 () => {
await (databaseManager as any).createSkeletonPacks(mockDbItem); await (databaseManager as any).createSkeletonPacks(mockDbItem);
expect(showBinaryChoiceDialogSpy).toBeCalledTimes(1); 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", () => { describe("when the language is not set", () => {

View File

@@ -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);
});
});