Merge branch 'main' into robertbrignull/enable-await-thenable
This commit is contained in:
15
.github/workflows/main.yml
vendored
15
.github/workflows/main.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
101
extensions/ql-vscode/src/qlpack-generator.ts
Normal file
101
extensions/ql-vscode/src/qlpack-generator.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
@@ -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",
|
|
||||||
};
|
|
||||||
@@ -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 />;
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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({});
|
|
||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>;
|
||||||
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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'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
|
|
||||||
{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>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
|
|
||||||
sans-serif, Apple Color Emoji, Segoe UI Emoji;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import {
|
||||||
|
beforeAllAction,
|
||||||
|
beforeEachAction,
|
||||||
|
} from "../jest.activated-extension.setup";
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await beforeAllAction();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await beforeEachAction();
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -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,
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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,16 +64,19 @@ describe("databases", () => {
|
|||||||
.spyOn(helpers, "showBinaryChoiceDialog")
|
.spyOn(helpers, "showBinaryChoiceDialog")
|
||||||
.mockResolvedValue(true);
|
.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(
|
databaseManager = new DatabaseManager(
|
||||||
{
|
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,
|
|
||||||
} as unknown as 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";
|
||||||
|
|
||||||
|
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", () => {
|
describe("when the language is set", () => {
|
||||||
it("should offer the user to set up a skeleton QL pack", async () => {
|
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);
|
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", () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user