Merge branch 'main' into asgerf/navigate-alerts
This commit is contained in:
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '16.14.0'
|
||||
node-version: '16.14.2'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: extensions/ql-vscode
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
version: ['v2.6.3', 'v2.7.6', 'v2.8.5', 'v2.9.4', 'v2.10.5', 'v2.11.0', 'nightly']
|
||||
version: ['v2.7.6', 'v2.8.5', 'v2.9.4', 'v2.10.5', 'v2.11.1', 'nightly']
|
||||
env:
|
||||
CLI_VERSION: ${{ matrix.version }}
|
||||
NIGHTLY_URL: ${{ needs.find-nightly.outputs.url }}
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '16.14.0'
|
||||
node-version: '16.14.2'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
**/* @github/codeql-vscode-reviewers
|
||||
/extensions/ql-vscode/src/remote-queries/ @github/code-scanning-secexp-reviewers
|
||||
/extensions/ql-vscode/src/view/remote-queries/ @github/code-scanning-secexp-reviewers
|
||||
/extensions/ql-vscode/src/view/variant-analysis/ @github/code-scanning-secexp-reviewers
|
||||
**/remote-queries/ @github/code-scanning-secexp-reviewers
|
||||
**/variant-analysis/ @github/code-scanning-secexp-reviewers
|
||||
|
||||
@@ -91,29 +91,58 @@ Alternatively, you can start Storybook inside of VSCode. There is a VSCode launc
|
||||
|
||||
More information about Storybook can be found inside the **Overview** page once you have launched Storybook.
|
||||
|
||||
### Running the unit tests and integration tests that do not require a CLI instance
|
||||
### Testing
|
||||
|
||||
Unit tests and many integration tests do not require a copy of the CodeQL CLI.
|
||||
We have several types of tests:
|
||||
|
||||
Outside of vscode, in the `extensions/ql-vscode` directory, run:
|
||||
* Unit tests: these live in the `tests/pure-tests/` directory
|
||||
* View tests: these live in `src/view/variant-analysis/__tests__/`
|
||||
* VSCode integration tests: these live in `src/vscode-tests/no-workspace` and `src/vscode-tests/minimal-workspace`
|
||||
* CLI integration tests: these live in `src/vscode-tests/cli-integration`
|
||||
|
||||
```shell
|
||||
npm run test && npm run integration
|
||||
```
|
||||
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.
|
||||
|
||||
Alternatively, you can run the tests inside of vscode. There are several vscode launch configurations defined that run the unit and integration tests. They can all be found in the debug view.
|
||||
#### Running the tests
|
||||
|
||||
Only the _With CLI_ tests require a CLI instance to run. See below on how to do that.
|
||||
##### 1. From the terminal
|
||||
|
||||
Running from a terminal, you _must_ set the `TEST_CODEQL_PATH` variable to point to a checkout of the `github/codeql` repository. The appropriate CLI version will be downloaded as part of the test.
|
||||
First move into the `extensions/ql-vscode` directory. Then, depending on which tests you want to run, use the appropriate command to run the tests:
|
||||
|
||||
### Running the integration tests
|
||||
* Unit tests: `npm run test:unit`
|
||||
* View Tests: `npm test:view`
|
||||
* VSCode integration tests: `npm run integration`
|
||||
|
||||
You will need to run CLI tests using a task from inside of VS Code called _Launch Integration Tests - With CLI_.
|
||||
###### CLI integration tests
|
||||
|
||||
The CLI integration tests require the CodeQL standard libraries in order to run so you will need to clone a local copy of the `github/codeql` repository.
|
||||
|
||||
From inside of VSCode, open the `launch.json` file and in the _Launch Integration Tests - With CLI_ task, uncomment the `"${workspaceRoot}/../codeql"` line. If necessary, replace value with a path to your checkout, and then run the task.
|
||||
1. Set the `TEST_CODEQL_PATH` environment variable: running from a terminal, you _must_ set the `TEST_CODEQL_PATH` variable to point to a checkout of the `github/codeql` repository. The appropriate CLI version will be downloaded as part of the test.
|
||||
|
||||
2. Run your test command:
|
||||
|
||||
```shell
|
||||
cd extensions/ql-vscode && npm run cli-integration
|
||||
```
|
||||
|
||||
##### 2. From VSCode
|
||||
|
||||
Alternatively, you can run the tests inside of VSCode. There are several VSCode launch configurations defined that run the unit and integration tests.
|
||||
|
||||
You will need to run tests using a task from inside of VS Code, under the "Run and Debug" view:
|
||||
|
||||
* Unit tests: run the _Launch Unit Tests - React_ task
|
||||
* View Tests: run the _Launch Unit Tests_ task
|
||||
* VSCode integration tests: run the _Launch Unit Tests - No Workspace_ and _Launch Unit Tests - Minimal Workspace_ tasks
|
||||
|
||||
###### CLI integration tests
|
||||
|
||||
The CLI integration tests require the CodeQL standard libraries in order to run so you will need to clone a local copy of the `github/codeql` repository.
|
||||
|
||||
1. Set the `TEST_CODEQL_PATH` environment variable: running from a terminal, you _must_ set the `TEST_CODEQL_PATH` variable to point to a checkout of the `github/codeql` repository. The appropriate CLI version will be downloaded as part of the test.
|
||||
|
||||
2. Set the codeql path in VSCode's launch configuration: open `launch.json` and under the _Launch Integration Tests - With CLI_ section, uncomment the `"${workspaceRoot}/../codeql"` line. If you've cloned the `github/codeql` repo to a different path, replace the value with the correct path.
|
||||
|
||||
3. Run the VSCode task from the "Run and Debug" view called _Launch Integration Tests - With CLI_.
|
||||
|
||||
## Releasing (write access required)
|
||||
|
||||
@@ -137,7 +166,7 @@ From inside of VSCode, open the `launch.json` file and in the _Launch Integratio
|
||||
git tag v1.3.6
|
||||
```
|
||||
|
||||
If you've accidentally created a badly named tag, you can delete it via
|
||||
If you've accidentally created a badly named tag, you can delete it via
|
||||
```bash
|
||||
git tag -d badly-named-tag
|
||||
```
|
||||
@@ -148,13 +177,13 @@ From inside of VSCode, open the `launch.json` file and in the _Launch Integratio
|
||||
```bash
|
||||
git push upstream refs/tags/v1.3.6
|
||||
```
|
||||
|
||||
|
||||
b. If you're working straight in this repo:
|
||||
|
||||
```bash
|
||||
git push origin refs/tags/v1.3.6
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
This will trigger [a release build](https://github.com/github/vscode-codeql/releases) on Actions.
|
||||
|
||||
* **IMPORTANT** Make sure you are on the `main` branch and your local checkout is fully updated when you add the tag.
|
||||
|
||||
@@ -1 +1 @@
|
||||
v16.14.0
|
||||
v16.14.2
|
||||
|
||||
@@ -8,7 +8,8 @@ const config: StorybookConfig = {
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions'
|
||||
'@storybook/addon-interactions',
|
||||
'./vscode-theme-addon/preset.ts',
|
||||
],
|
||||
framework: '@storybook/react',
|
||||
core: {
|
||||
|
||||
@@ -4,8 +4,6 @@ import { action } from '@storybook/addon-actions';
|
||||
// Allow all stories/components to use Codicons
|
||||
import '@vscode/codicons/dist/codicon.css';
|
||||
|
||||
import '../src/stories/vscode-theme.css';
|
||||
|
||||
// https://storybook.js.org/docs/react/configure/overview#configure-story-rendering
|
||||
export const parameters = {
|
||||
// All props starting with `on` will automatically receive an action as a prop
|
||||
@@ -22,16 +20,12 @@ export const parameters = {
|
||||
theme: themes.dark,
|
||||
},
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
values: [
|
||||
{
|
||||
name: 'dark',
|
||||
value: '#1e1e1e',
|
||||
},
|
||||
],
|
||||
// The background is injected by our theme CSS files
|
||||
disable: true,
|
||||
}
|
||||
};
|
||||
|
||||
(window as any).acquireVsCodeApi = () => ({
|
||||
postMessage: action('post-vscode-message')
|
||||
postMessage: action('post-vscode-message'),
|
||||
setState: action('set-vscode-state'),
|
||||
});
|
||||
|
||||
19
extensions/ql-vscode/.storybook/tsconfig.json
Normal file
19
extensions/ql-vscode/.storybook/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"target": "es6",
|
||||
"outDir": "out",
|
||||
"lib": ["ES2021", "dom"],
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"rootDir": "..",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import { FunctionComponent, useCallback } from 'react';
|
||||
|
||||
import { useGlobals } from '@storybook/api';
|
||||
import { IconButton, Icons, WithTooltip, TooltipLinkList, Link, WithHideFn } from '@storybook/components';
|
||||
|
||||
import { themeNames, VSCodeTheme } from './theme';
|
||||
|
||||
export const ThemeSelector: FunctionComponent = () => {
|
||||
const [{ vscodeTheme }, updateGlobals] = useGlobals();
|
||||
|
||||
const changeTheme = useCallback((theme: VSCodeTheme) => {
|
||||
updateGlobals({
|
||||
vscodeTheme: theme,
|
||||
});
|
||||
}, [updateGlobals]);
|
||||
|
||||
const createLinks = useCallback((onHide: () => void): Link[] => Object.values(VSCodeTheme).map((theme) => ({
|
||||
id: theme,
|
||||
onClick() {
|
||||
changeTheme(theme);
|
||||
onHide();
|
||||
},
|
||||
title: themeNames[theme],
|
||||
value: theme,
|
||||
active: vscodeTheme === theme,
|
||||
})), [vscodeTheme, changeTheme]);
|
||||
|
||||
return (
|
||||
<WithTooltip
|
||||
placement="top"
|
||||
trigger="click"
|
||||
closeOnClick
|
||||
tooltip={({ onHide }: WithHideFn) => (
|
||||
<TooltipLinkList
|
||||
links={createLinks(onHide)}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
key="theme"
|
||||
title="Change the theme of the preview"
|
||||
active={vscodeTheme !== VSCodeTheme.Dark}
|
||||
>
|
||||
<Icons icon="dashboard" />
|
||||
</IconButton>
|
||||
</WithTooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import { addons, types } from '@storybook/addons';
|
||||
import { ThemeSelector } from './ThemeSelector';
|
||||
|
||||
const ADDON_ID = 'vscode-theme-addon';
|
||||
|
||||
addons.register(ADDON_ID, () => {
|
||||
addons.add(ADDON_ID, {
|
||||
title: 'VSCode Themes',
|
||||
type: types.TOOL,
|
||||
match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)),
|
||||
render: () => <ThemeSelector />,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
export function config(entry = []) {
|
||||
return [...entry, require.resolve("./preview.ts")];
|
||||
}
|
||||
|
||||
export function managerEntries(entry = []) {
|
||||
return [...entry, require.resolve("./manager.tsx")];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { withTheme } from './withTheme';
|
||||
import { VSCodeTheme } from './theme';
|
||||
|
||||
export const decorators = [withTheme];
|
||||
|
||||
export const globals = {
|
||||
vscodeTheme: VSCodeTheme.Dark,
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export enum VSCodeTheme {
|
||||
Dark = 'dark',
|
||||
Light = 'light',
|
||||
}
|
||||
|
||||
export const themeNames: { [key in VSCodeTheme]: string } = {
|
||||
[VSCodeTheme.Dark]: 'Dark+',
|
||||
[VSCodeTheme.Light]: 'Light+',
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useEffect, useGlobals } from '@storybook/addons';
|
||||
import type { AnyFramework, PartialStoryFn as StoryFunction, StoryContext } from '@storybook/csf';
|
||||
|
||||
import { VSCodeTheme } from './theme';
|
||||
|
||||
const themeFiles: { [key in VSCodeTheme]: string } = {
|
||||
[VSCodeTheme.Dark]: require('!file-loader?modules!../../src/stories/vscode-theme-dark.css').default,
|
||||
[VSCodeTheme.Light]: require('!file-loader?modules!../../src/stories/vscode-theme-light.css').default,
|
||||
}
|
||||
|
||||
export const withTheme = (
|
||||
StoryFn: StoryFunction<AnyFramework>,
|
||||
context: StoryContext<AnyFramework>
|
||||
) => {
|
||||
const [{ vscodeTheme }] = useGlobals();
|
||||
|
||||
useEffect(() => {
|
||||
const styleSelectorId =
|
||||
context.viewMode === 'docs'
|
||||
? `addon-vscode-theme-docs-${context.id}`
|
||||
: `addon-vscode-theme-theme`;
|
||||
|
||||
const theme = Object.values(VSCodeTheme).includes(vscodeTheme) ? vscodeTheme as VSCodeTheme : VSCodeTheme.Dark;
|
||||
|
||||
document.getElementById(styleSelectorId)?.remove();
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.id = styleSelectorId;
|
||||
link.href = themeFiles[theme];
|
||||
link.rel = 'stylesheet';
|
||||
|
||||
document.head.appendChild(link);
|
||||
}, [vscodeTheme]);
|
||||
|
||||
return StoryFn();
|
||||
};
|
||||
@@ -2,13 +2,20 @@
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
## 1.7.2 - 14 October 2022
|
||||
|
||||
- Fix a bug where results created in older versions were thought to be unsuccessful. [#1605](https://github.com/github/vscode-codeql/pull/1605)
|
||||
|
||||
## 1.7.1 - 12 October 2022
|
||||
|
||||
- Fix a bug where it was not possible to add a database folder if the folder name starts with `db-`. [#1565](https://github.com/github/vscode-codeql/pull/1565)
|
||||
- Add commands for navigating up, down, left, or right in the result viewer. Previously there were only commands for moving up and down the currently-selected path. We suggest binding keyboard shortcuts to these commands, for navigating the result viewer using the keyboard. [#1568](https://github.com/github/vscode-codeql/pull/1568)
|
||||
- Ensure the results view opens in an editor column beside the currently active editor. [#1557](https://github.com/github/vscode-codeql/pull/1557)
|
||||
|
||||
## 1.7.0 - 20 September 2022
|
||||
|
||||
- Remove ability to download databases from LGTM. [#1467](https://github.com/github/vscode-codeql/pull/1467)
|
||||
- Removed the ability to manually upgrade databases from the context menu on databases. Databases are non-destructively upgraded automatically so for most users this was not needed. For advanced users this is still available in the Command Palette. [#1501](https://github.com/github/vscode-codeql/pull/1501)
|
||||
- Remove the ability to manually upgrade databases from the context menu on databases. Databases are non-destructively upgraded automatically so for most users this was not needed. For advanced users this is still available in the Command Palette. [#1501](https://github.com/github/vscode-codeql/pull/1501)
|
||||
- Always restart the query server after a manual database upgrade. This avoids a bug in the query server where an invalid dbscheme was being retained in memory after an upgrade. [#1519](https://github.com/github/vscode-codeql/pull/1519)
|
||||
|
||||
## 1.6.12 - 1 September 2022
|
||||
|
||||
2000
extensions/ql-vscode/package-lock.json
generated
2000
extensions/ql-vscode/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.7.1",
|
||||
"version": "1.7.3",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -63,6 +63,7 @@
|
||||
"onCommand:codeQL.quickQuery",
|
||||
"onCommand:codeQL.restartQueryServer",
|
||||
"onWebviewPanel:resultsView",
|
||||
"onWebviewPanel:codeQL.variantAnalysis",
|
||||
"onFileSystem:codeql-zip-archive"
|
||||
],
|
||||
"main": "./out/extension",
|
||||
@@ -289,6 +290,13 @@
|
||||
"pattern": "^$|^(?:[a-zA-Z0-9]+-)*[a-zA-Z0-9]+/[a-zA-Z0-9-_]+$",
|
||||
"patternErrorMessage": "Please enter a valid GitHub repository",
|
||||
"markdownDescription": "[For internal use only] The name of the GitHub repository in which the GitHub Actions workflow is run when using the \"Run Variant Analysis\" command. The repository should be of the form `<owner>/<repo>`)."
|
||||
},
|
||||
"codeQL.logInsights.joinOrderWarningThreshold": {
|
||||
"type": "number",
|
||||
"default": 50,
|
||||
"scope": "window",
|
||||
"minimum": 0,
|
||||
"description": "Report a warning for any join order whose metric exceeds this value."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -314,8 +322,8 @@
|
||||
"title": "CodeQL: Export Variant Analysis Results"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.mockVariantAnalysisView",
|
||||
"title": "CodeQL: Open Variant Analysis Mock View"
|
||||
"command": "codeQL.openVariantAnalysis",
|
||||
"title": "CodeQL: Open Variant Analysis"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runQueries",
|
||||
@@ -645,6 +653,18 @@
|
||||
"command": "codeQL.gotoQL",
|
||||
"title": "CodeQL: Go to QL Code",
|
||||
"enablement": "codeql.hasQLSource"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.mockGitHubApiServer.startRecording",
|
||||
"title": "CodeQL: Mock GitHub API Server: Start Scenario Recording"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.mockGitHubApiServer.saveScenario",
|
||||
"title": "CodeQL: Mock GitHub API Server: Save Scenario"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.mockGitHubApiServer.cancelRecording",
|
||||
"title": "CodeQL: Mock GitHub API Server: Cancel Scenario Recording"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
@@ -902,12 +922,12 @@
|
||||
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.exportVariantAnalysisResults",
|
||||
"when": "config.codeQL.canary"
|
||||
"command": "codeQL.openVariantAnalysis",
|
||||
"when": "config.codeQL.canary && config.codeQL.variantAnalysis.liveResults"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.mockVariantAnalysisView",
|
||||
"when": "config.codeQL.canary && config.codeQL.variantAnalysis.liveResults"
|
||||
"command": "codeQL.exportVariantAnalysisResults",
|
||||
"when": "config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runQueries",
|
||||
@@ -1104,6 +1124,18 @@
|
||||
{
|
||||
"command": "codeQLTests.showOutputDifferences",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.mockGitHubApiServer.startRecording",
|
||||
"when": "config.codeQL.variantAnalysis.mockGitHubApiServer && !codeQL.mockGitHubApiServer.recording"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.mockGitHubApiServer.saveScenario",
|
||||
"when": "config.codeQL.variantAnalysis.mockGitHubApiServer && codeQL.mockGitHubApiServer.recording"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.mockGitHubApiServer.cancelRecording",
|
||||
"when": "config.codeQL.variantAnalysis.mockGitHubApiServer && codeQL.mockGitHubApiServer.recording"
|
||||
}
|
||||
],
|
||||
"editor/context": [
|
||||
@@ -1215,7 +1247,7 @@
|
||||
"dependencies": {
|
||||
"@octokit/plugin-retry": "^3.0.9",
|
||||
"@octokit/rest": "^19.0.4",
|
||||
"@primer/octicons-react": "^16.3.0",
|
||||
"@primer/octicons-react": "^17.6.0",
|
||||
"@primer/react": "^35.0.0",
|
||||
"@vscode/codicons": "^0.0.31",
|
||||
"@vscode/webview-ui-toolkit": "^1.0.1",
|
||||
@@ -1230,6 +1262,7 @@
|
||||
"minimist": "~1.2.6",
|
||||
"nanoid": "^3.2.0",
|
||||
"node-fetch": "~2.6.7",
|
||||
"p-queue": "^6.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
@@ -1322,13 +1355,14 @@
|
||||
"gulp-replace": "^1.1.3",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-typescript": "^5.0.1",
|
||||
"husky": "~4.3.8",
|
||||
"husky": "~8.0.1",
|
||||
"jest": "^29.0.3",
|
||||
"jest-environment-jsdom": "^29.0.3",
|
||||
"lint-staged": "~10.2.2",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"mocha": "^10.0.0",
|
||||
"mocha-sinon": "~2.1.2",
|
||||
"msw": "^0.47.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "~2.0.5",
|
||||
"proxyquire": "~2.1.3",
|
||||
|
||||
@@ -33,6 +33,11 @@ export abstract class AbstractWebview<ToMessage extends WebviewMessage, FromMess
|
||||
super();
|
||||
}
|
||||
|
||||
public async restoreView(panel: WebviewPanel): Promise<void> {
|
||||
this.panel = panel;
|
||||
this.setupPanel(panel);
|
||||
}
|
||||
|
||||
protected get isShowingPanel() {
|
||||
return !!this.panel;
|
||||
}
|
||||
@@ -59,37 +64,43 @@ export abstract class AbstractWebview<ToMessage extends WebviewMessage, FromMess
|
||||
],
|
||||
}
|
||||
);
|
||||
this.push(
|
||||
this.panel.onDidDispose(
|
||||
() => {
|
||||
this.panel = undefined;
|
||||
this.panelLoaded = false;
|
||||
this.onPanelDispose();
|
||||
},
|
||||
null,
|
||||
ctx.subscriptions
|
||||
)
|
||||
);
|
||||
|
||||
this.panel.webview.html = getHtmlForWebview(
|
||||
ctx,
|
||||
this.panel.webview,
|
||||
config.view,
|
||||
{
|
||||
allowInlineStyles: true,
|
||||
}
|
||||
);
|
||||
this.push(
|
||||
this.panel.webview.onDidReceiveMessage(
|
||||
async (e) => this.onMessage(e),
|
||||
undefined,
|
||||
ctx.subscriptions
|
||||
)
|
||||
);
|
||||
this.setupPanel(this.panel);
|
||||
}
|
||||
return this.panel;
|
||||
}
|
||||
|
||||
protected setupPanel(panel: WebviewPanel): void {
|
||||
const config = this.getPanelConfig();
|
||||
|
||||
this.push(
|
||||
panel.onDidDispose(
|
||||
() => {
|
||||
this.panel = undefined;
|
||||
this.panelLoaded = false;
|
||||
this.onPanelDispose();
|
||||
},
|
||||
null,
|
||||
this.ctx.subscriptions
|
||||
)
|
||||
);
|
||||
|
||||
panel.webview.html = getHtmlForWebview(
|
||||
this.ctx,
|
||||
panel.webview,
|
||||
config.view,
|
||||
{
|
||||
allowInlineStyles: true,
|
||||
}
|
||||
);
|
||||
this.push(
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async (e) => this.onMessage(e),
|
||||
undefined,
|
||||
this.ctx.subscriptions
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected abstract getPanelConfig(): WebviewPanelConfig;
|
||||
|
||||
protected abstract onPanelDispose(): void;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { promisify } from 'util';
|
||||
import { CancellationToken, commands, Disposable, Uri } from 'vscode';
|
||||
|
||||
import { BQRSInfo, DecodedBqrsChunk } from './pure/bqrs-cli-types';
|
||||
import { CliConfig } from './config';
|
||||
import { allowCanaryQueryServer, CliConfig } from './config';
|
||||
import { DistributionProvider, FindDistributionResultKind } from './distribution';
|
||||
import { assertNever, getErrorMessage, getErrorStack } from './pure/helpers-pure';
|
||||
import { QueryMetadata, SortDirection } from './pure/interface-types';
|
||||
@@ -1330,6 +1330,11 @@ export class CliVersionConstraint {
|
||||
*/
|
||||
public static CLI_VERSION_WITH_SOURCEMAP = new SemVer('2.10.3');
|
||||
|
||||
/**
|
||||
* CLI version that supports the new query server.
|
||||
*/
|
||||
public static CLI_VERSION_WITH_NEW_QUERY_SERVER = new SemVer('2.11.0');
|
||||
|
||||
constructor(private readonly cli: CodeQLCliServer) {
|
||||
/**/
|
||||
}
|
||||
@@ -1405,4 +1410,12 @@ export class CliVersionConstraint {
|
||||
async supportsSourceMap() {
|
||||
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_SOURCEMAP);
|
||||
}
|
||||
|
||||
async supportsNewQueryServer() {
|
||||
// TODO while under development, users _must_ opt-in to the new query server
|
||||
// by setting the `codeql.canaryQueryServer` setting to `true`.
|
||||
// Ignore the version check for now.
|
||||
return allowCanaryQueryServer();
|
||||
// return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_NEW_QUERY_SERVER);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { DistributionManager } from './distribution';
|
||||
import { logger } from './logging';
|
||||
import { ONE_DAY_IN_MS } from './pure/time';
|
||||
|
||||
export const ALL_SETTINGS: Setting[] = [];
|
||||
|
||||
/** Helper class to look up a labelled (and possibly nested) setting. */
|
||||
export class Setting {
|
||||
name: string;
|
||||
@@ -12,6 +14,7 @@ export class Setting {
|
||||
constructor(name: string, parent?: Setting) {
|
||||
this.name = name;
|
||||
this.parent = parent;
|
||||
ALL_SETTINGS.push(this);
|
||||
}
|
||||
|
||||
get qualifiedName(): string {
|
||||
@@ -36,6 +39,18 @@ export class Setting {
|
||||
return workspace.getConfiguration(this.parent.qualifiedName).update(this.name, value, target);
|
||||
}
|
||||
|
||||
inspect<T>(): InspectionResult<T> | undefined {
|
||||
if (this.parent === undefined) {
|
||||
throw new Error('Cannot update the value of a root setting.');
|
||||
}
|
||||
return workspace.getConfiguration(this.parent.qualifiedName).inspect(this.name);
|
||||
}
|
||||
}
|
||||
|
||||
export interface InspectionResult<T> {
|
||||
globalValue?: T;
|
||||
workspaceValue?: T,
|
||||
workspaceFolderValue?: T,
|
||||
}
|
||||
|
||||
const ROOT_SETTING = new Setting('codeQL');
|
||||
@@ -44,6 +59,7 @@ const ROOT_SETTING = new Setting('codeQL');
|
||||
const TELEMETRY_SETTING = new Setting('telemetry', ROOT_SETTING);
|
||||
const AST_VIEWER_SETTING = new Setting('astViewer', ROOT_SETTING);
|
||||
const GLOBAL_TELEMETRY_SETTING = new Setting('telemetry');
|
||||
const LOG_INSIGHTS_SETTING = new Setting('logInsights', ROOT_SETTING);
|
||||
|
||||
export const LOG_TELEMETRY = new Setting('logTelemetry', TELEMETRY_SETTING);
|
||||
export const ENABLE_TELEMETRY = new Setting('enableTelemetry', TELEMETRY_SETTING);
|
||||
@@ -317,6 +333,22 @@ export function isCanary() {
|
||||
return !!CANARY_FEATURES.getValue<boolean>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the experimental query server
|
||||
*/
|
||||
export const CANARY_QUERY_SERVER = new Setting('canaryQueryServer', ROOT_SETTING);
|
||||
|
||||
|
||||
export function allowCanaryQueryServer() {
|
||||
return !!CANARY_QUERY_SERVER.getValue<boolean>();
|
||||
}
|
||||
|
||||
export const JOIN_ORDER_WARNING_THRESHOLD = new Setting('joinOrderWarningThreshold', LOG_INSIGHTS_SETTING);
|
||||
|
||||
export function joinOrderWarningThreshold(): number {
|
||||
return JOIN_ORDER_WARNING_THRESHOLD.getValue<number>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoids caching in the AST viewer if the user is also a canary user.
|
||||
*/
|
||||
@@ -343,12 +375,12 @@ export async function setRemoteRepositoryLists(lists: Record<string, string[]> |
|
||||
}
|
||||
|
||||
/**
|
||||
* Path to a file that contains lists of GitHub repositories that you want to query remotely via
|
||||
* Path to a file that contains lists of GitHub repositories that you want to query remotely via
|
||||
* the "Run Variant Analysis" command.
|
||||
* Note: This command is only available for internal users.
|
||||
*
|
||||
*
|
||||
* This setting should be a path to a JSON file that contains a JSON object where each key is a
|
||||
* user-specified name (string), and the value is an array of GitHub repositories
|
||||
* user-specified name (string), and the value is an array of GitHub repositories
|
||||
* (of the form `<owner>/<repo>`).
|
||||
*/
|
||||
const REPO_LISTS_PATH = new Setting('repositoryListsPath', REMOTE_QUERIES_SETTING);
|
||||
@@ -397,3 +429,29 @@ const LIVE_RESULTS = new Setting('liveResults', REMOTE_QUERIES_SETTING);
|
||||
export function isVariantAnalysisLiveResultsEnabled(): boolean {
|
||||
return !!LIVE_RESULTS.getValue<boolean>();
|
||||
}
|
||||
|
||||
/**
|
||||
* A flag indicating whether to enable a mock GitHub API server.
|
||||
*/
|
||||
const MOCK_GH_API_SERVER = new Setting('mockGitHubApiServer', REMOTE_QUERIES_SETTING);
|
||||
|
||||
export interface MockGitHubApiConfig {
|
||||
mockServerEnabled: boolean;
|
||||
onDidChangeConfiguration: Event<void>;
|
||||
}
|
||||
|
||||
export class MockGitHubApiConfigListener extends ConfigListener implements MockGitHubApiConfig {
|
||||
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
|
||||
this.handleDidChangeConfigurationForRelevantSettings([MOCK_GH_API_SERVER], e);
|
||||
}
|
||||
|
||||
public get mockServerEnabled(): boolean {
|
||||
return !!MOCK_GH_API_SERVER.getValue<boolean>();
|
||||
}
|
||||
}
|
||||
|
||||
const MOCK_GH_API_SERVER_SCENARIOS_PATH = new Setting('mockGitHubApiServerScenariosPath', REMOTE_QUERIES_SETTING);
|
||||
|
||||
export function getMockGitHubApiServerScenariosPath(): string | undefined {
|
||||
return MOCK_GH_API_SERVER_SCENARIOS_PATH.getValue<string>();
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export async function getLocationsForUriString(
|
||||
false
|
||||
);
|
||||
const results = await qs.compileAndRunQueryAgainstDatabase(db, initialInfo, queryStorageDir, progress, token, templates);
|
||||
if (results.sucessful) {
|
||||
if (results.successful) {
|
||||
links.push(...await getLinksFromResults(results, cli, db, filter));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,14 +358,12 @@ export class DatabaseItemImpl implements DatabaseItem {
|
||||
try {
|
||||
this._contents = await resolveDatabaseContents(this.databaseUri);
|
||||
this._error = undefined;
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
this._contents = undefined;
|
||||
this._error = e instanceof Error ? e : new Error(String(e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
this.onChanged({
|
||||
kind: DatabaseEventKind.Refresh,
|
||||
item: this
|
||||
@@ -561,9 +559,6 @@ export class DatabaseManager extends DisposableObject {
|
||||
super();
|
||||
|
||||
qs.onStart(this.reregisterDatabases.bind(this));
|
||||
|
||||
// Let this run async.
|
||||
void this.loadPersistedState();
|
||||
}
|
||||
|
||||
public async openDatabase(
|
||||
@@ -693,7 +688,7 @@ export class DatabaseManager extends DisposableObject {
|
||||
return item;
|
||||
}
|
||||
|
||||
private async loadPersistedState(): Promise<void> {
|
||||
public async loadPersistedState(): Promise<void> {
|
||||
return withProgress({
|
||||
location: vscode.ProgressLocation.Notification
|
||||
},
|
||||
@@ -707,6 +702,7 @@ export class DatabaseManager extends DisposableObject {
|
||||
step
|
||||
});
|
||||
try {
|
||||
void this.logger.log(`Found ${databases.length} persisted databases: ${databases.map(db => db.uri).join(', ')}`);
|
||||
for (const database of databases) {
|
||||
progress({
|
||||
maxStep: databases.length,
|
||||
@@ -721,16 +717,19 @@ export class DatabaseManager extends DisposableObject {
|
||||
if (currentDatabaseUri === database.uri) {
|
||||
await this.setCurrentDatabaseItem(databaseItem, true);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
void this.logger.log(`Loaded database ${databaseItem.name} at URI ${database.uri}.`);
|
||||
} catch (e) {
|
||||
// When loading from persisted state, leave invalid databases in the list. They will be
|
||||
// marked as invalid, and cannot be set as the current database.
|
||||
void this.logger.log(`Error loading database ${database.uri}: ${e}.`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// database list had an unexpected type - nothing to be done?
|
||||
void showAndLogErrorMessage(`Database list loading failed: ${getErrorMessage(e)}`);
|
||||
}
|
||||
|
||||
void this.logger.log('Finished loading persisted databases.');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
CliConfigListener,
|
||||
DistributionConfigListener,
|
||||
isCanary,
|
||||
joinOrderWarningThreshold,
|
||||
MAX_QUERIES,
|
||||
QueryHistoryConfigListener,
|
||||
QueryServerConfigListener
|
||||
@@ -73,7 +74,8 @@ import { WebviewReveal } from './interface-utils';
|
||||
import { ideServerLogger, logger, ProgressReporter, queryServerLogger } from './logging';
|
||||
import { QueryHistoryManager } from './query-history';
|
||||
import { CompletedLocalQueryInfo, LocalQueryInfo } from './query-results';
|
||||
import * as qsClient from './legacy-query-server/queryserver-client';
|
||||
import * as legacyQueryServer from './legacy-query-server/queryserver-client';
|
||||
import * as newQueryServer from './query-server/queryserver-client';
|
||||
import { displayQuickQuery } from './quick-query';
|
||||
import { QLTestAdapterFactory } from './test-adapter';
|
||||
import { TestUIService } from './test-ui';
|
||||
@@ -103,13 +105,18 @@ import { JoinOrderScannerProvider } from './log-insights/join-order';
|
||||
import { LogScannerService } from './log-insights/log-scanner-service';
|
||||
import { createInitialQueryInfo } from './run-queries-shared';
|
||||
import { LegacyQueryRunner } from './legacy-query-server/legacyRunner';
|
||||
import { NewQueryRunner } from './query-server/query-runner';
|
||||
import { QueryRunner } from './queryRunner';
|
||||
import { VariantAnalysisView } from './remote-queries/variant-analysis-view';
|
||||
import { VariantAnalysisViewSerializer } from './remote-queries/variant-analysis-view-serializer';
|
||||
import { VariantAnalysis } from './remote-queries/shared/variant-analysis';
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisApiResponse,
|
||||
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository
|
||||
} from './remote-queries/gh-api/variant-analysis';
|
||||
import { VariantAnalysisManager } from './remote-queries/variant-analysis-manager';
|
||||
import { createVariantAnalysisContentProvider } from './remote-queries/variant-analysis-content-provider';
|
||||
import { MockGitHubApiServer } from './mocks/mock-gh-api-server';
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -176,6 +183,7 @@ export interface CodeQLExtensionInterface {
|
||||
readonly distributionManager: DistributionManager;
|
||||
readonly databaseManager: DatabaseManager;
|
||||
readonly databaseUI: DatabaseUI;
|
||||
readonly variantAnalysisManager: VariantAnalysisManager;
|
||||
readonly dispose: () => void;
|
||||
}
|
||||
|
||||
@@ -386,7 +394,10 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
|
||||
allowAutoUpdating: true
|
||||
})));
|
||||
|
||||
return await installOrUpdateThenTryActivate({
|
||||
const variantAnalysisViewSerializer = new VariantAnalysisViewSerializer(ctx);
|
||||
Window.registerWebviewPanelSerializer(VariantAnalysisView.viewType, variantAnalysisViewSerializer);
|
||||
|
||||
const codeQlExtension = await installOrUpdateThenTryActivate({
|
||||
isUserInitiated: !!ctx.globalState.get(shouldUpdateOnNextActivationKey),
|
||||
shouldDisplayMessageWhenNoUpdates: false,
|
||||
|
||||
@@ -394,8 +405,14 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
|
||||
// otherwise, ask user to accept the update
|
||||
allowAutoUpdating: !!ctx.globalState.get(shouldUpdateOnNextActivationKey)
|
||||
});
|
||||
|
||||
variantAnalysisViewSerializer.onExtensionLoaded(codeQlExtension.variantAnalysisManager);
|
||||
|
||||
return codeQlExtension;
|
||||
}
|
||||
|
||||
const PACK_GLOBS = ['**/codeql-pack.yml', '**/qlpack.yml', '**/queries.xml', '**/codeql-pack.lock.yml', '**/qlpack.lock.yml', '.codeqlmanifest.json', 'codeql-workspace.yml'];
|
||||
|
||||
async function activateWithInstalledDistribution(
|
||||
ctx: ExtensionContext,
|
||||
distributionManager: DistributionManager,
|
||||
@@ -426,8 +443,21 @@ async function activateWithInstalledDistribution(
|
||||
void logger.log('Initializing query server client.');
|
||||
const qs = await createQueryServer(qlConfigurationListener, cliServer, ctx);
|
||||
|
||||
|
||||
for (const glob of PACK_GLOBS) {
|
||||
const fsWatcher = workspace.createFileSystemWatcher(glob);
|
||||
ctx.subscriptions.push(fsWatcher);
|
||||
fsWatcher.onDidChange(async (_uri) => {
|
||||
await qs.clearPackCache();
|
||||
});
|
||||
}
|
||||
|
||||
void logger.log('Initializing database manager.');
|
||||
const dbm = new DatabaseManager(ctx, qs, cliServer, logger);
|
||||
|
||||
// Let this run async.
|
||||
void dbm.loadPersistedState();
|
||||
|
||||
ctx.subscriptions.push(dbm);
|
||||
void logger.log('Initializing database panel.');
|
||||
const databaseUI = new DatabaseUI(
|
||||
@@ -458,7 +488,14 @@ async function activateWithInstalledDistribution(
|
||||
ctx.subscriptions.push(localQueryResultsView);
|
||||
|
||||
void logger.log('Initializing variant analysis manager.');
|
||||
const rqm = new RemoteQueriesManager(ctx, cliServer, queryStorageDir, logger);
|
||||
const variantAnalysisStorageDir = path.join(ctx.globalStorageUri.fsPath, 'variant-analyses');
|
||||
await fs.ensureDir(variantAnalysisStorageDir);
|
||||
const variantAnalysisManager = new VariantAnalysisManager(ctx, cliServer, variantAnalysisStorageDir, logger);
|
||||
ctx.subscriptions.push(variantAnalysisManager);
|
||||
ctx.subscriptions.push(workspace.registerTextDocumentContentProvider('codeql-variant-analysis', createVariantAnalysisContentProvider(variantAnalysisManager)));
|
||||
|
||||
void logger.log('Initializing remote queries manager.');
|
||||
const rqm = new RemoteQueriesManager(ctx, cliServer, queryStorageDir, logger, variantAnalysisManager);
|
||||
ctx.subscriptions.push(rqm);
|
||||
|
||||
void logger.log('Initializing query history.');
|
||||
@@ -467,6 +504,7 @@ async function activateWithInstalledDistribution(
|
||||
dbm,
|
||||
localQueryResultsView,
|
||||
rqm,
|
||||
variantAnalysisManager,
|
||||
evalLogViewer,
|
||||
queryStorageDir,
|
||||
ctx,
|
||||
@@ -482,7 +520,7 @@ async function activateWithInstalledDistribution(
|
||||
void logger.log('Initializing evaluation log scanners.');
|
||||
const logScannerService = new LogScannerService(qhm);
|
||||
ctx.subscriptions.push(logScannerService);
|
||||
ctx.subscriptions.push(logScannerService.scanners.registerLogScannerProvider(new JoinOrderScannerProvider()));
|
||||
ctx.subscriptions.push(logScannerService.scanners.registerLogScannerProvider(new JoinOrderScannerProvider(() => joinOrderWarningThreshold())));
|
||||
|
||||
void logger.log('Reading query history');
|
||||
await qhm.readQueryHistory();
|
||||
@@ -899,7 +937,6 @@ async function activateWithInstalledDistribution(
|
||||
})
|
||||
);
|
||||
|
||||
const variantAnalysisManager = new VariantAnalysisManager(ctx, logger);
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.monitorVariantAnalysis', async (
|
||||
variantAnalysis: VariantAnalysis,
|
||||
@@ -915,7 +952,13 @@ async function activateWithInstalledDistribution(
|
||||
variantAnalysisSummary: VariantAnalysisApiResponse,
|
||||
token: CancellationToken
|
||||
) => {
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(scannedRepo, variantAnalysisSummary, token);
|
||||
await variantAnalysisManager.enqueueDownload(scannedRepo, variantAnalysisSummary, token);
|
||||
})
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.openVariantAnalysis', async () => {
|
||||
await variantAnalysisManager.promptOpenVariantAnalysis();
|
||||
})
|
||||
);
|
||||
|
||||
@@ -933,8 +976,8 @@ async function activateWithInstalledDistribution(
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.mockVariantAnalysisView', async () => {
|
||||
await variantAnalysisManager.showView(1);
|
||||
commandRunner('codeQL.loadVariantAnalysisRepoResults', async (variantAnalysisId: number, repositoryFullName: string) => {
|
||||
await variantAnalysisManager.loadResults(variantAnalysisId, repositoryFullName);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1148,6 +1191,27 @@ async function activateWithInstalledDistribution(
|
||||
)
|
||||
);
|
||||
|
||||
const mockServer = new MockGitHubApiServer(ctx);
|
||||
ctx.subscriptions.push(mockServer);
|
||||
ctx.subscriptions.push(
|
||||
commandRunner(
|
||||
'codeQL.mockGitHubApiServer.startRecording',
|
||||
async () => await mockServer.startRecording(),
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commandRunner(
|
||||
'codeQL.mockGitHubApiServer.saveScenario',
|
||||
async () => await mockServer.saveScenario(),
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commandRunner(
|
||||
'codeQL.mockGitHubApiServer.cancelRecording',
|
||||
async () => await mockServer.cancelRecording(),
|
||||
)
|
||||
);
|
||||
|
||||
await commands.executeCommand('codeQLDatabases.removeOrphanedDatabases');
|
||||
|
||||
void logger.log('Successfully finished extension initialization.');
|
||||
@@ -1159,6 +1223,7 @@ async function activateWithInstalledDistribution(
|
||||
distributionManager,
|
||||
databaseManager: dbm,
|
||||
databaseUI,
|
||||
variantAnalysisManager,
|
||||
dispose: () => {
|
||||
ctx.subscriptions.forEach(d => d.dispose());
|
||||
}
|
||||
@@ -1174,15 +1239,28 @@ async function createQueryServer(qlConfigurationListener: QueryServerConfigListe
|
||||
{ title: 'CodeQL query server', location: ProgressLocation.Window },
|
||||
task
|
||||
);
|
||||
const qs = new qsClient.QueryServerClient(
|
||||
qlConfigurationListener,
|
||||
cliServer,
|
||||
qsOpts,
|
||||
progressCallback
|
||||
);
|
||||
ctx.subscriptions.push(qs);
|
||||
await qs.startQueryServer();
|
||||
return new LegacyQueryRunner(qs);
|
||||
if (await cliServer.cliConstraints.supportsNewQueryServer()) {
|
||||
const qs = new newQueryServer.QueryServerClient(
|
||||
qlConfigurationListener,
|
||||
cliServer,
|
||||
qsOpts,
|
||||
progressCallback
|
||||
);
|
||||
ctx.subscriptions.push(qs);
|
||||
await qs.startQueryServer();
|
||||
return new NewQueryRunner(qs);
|
||||
|
||||
} else {
|
||||
const qs = new legacyQueryServer.QueryServerClient(
|
||||
qlConfigurationListener,
|
||||
cliServer,
|
||||
qsOpts,
|
||||
progressCallback
|
||||
);
|
||||
ctx.subscriptions.push(qs);
|
||||
await qs.startQueryServer();
|
||||
return new LegacyQueryRunner(qs);
|
||||
}
|
||||
}
|
||||
|
||||
function getContextStoragePath(ctx: ExtensionContext) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { env } from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { QueryHistoryConfig } from './config';
|
||||
import { LocalQueryInfo, QueryHistoryInfo } from './query-results';
|
||||
import { LocalQueryInfo } from './query-results';
|
||||
import { getRawQueryName, QueryHistoryInfo } from './query-history-info';
|
||||
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
|
||||
import { pluralize } from './helpers';
|
||||
import { VariantAnalysisHistoryItem } from './remote-queries/variant-analysis-history-item';
|
||||
import { assertNever } from './pure/helpers-pure';
|
||||
|
||||
interface InterpolateReplacements {
|
||||
t: string; // Start time
|
||||
@@ -21,9 +24,20 @@ export class HistoryItemLabelProvider {
|
||||
}
|
||||
|
||||
getLabel(item: QueryHistoryInfo) {
|
||||
const replacements = item.t === 'local'
|
||||
? this.getLocalInterpolateReplacements(item)
|
||||
: this.getRemoteInterpolateReplacements(item);
|
||||
let replacements: InterpolateReplacements;
|
||||
switch (item.t) {
|
||||
case 'local':
|
||||
replacements = this.getLocalInterpolateReplacements(item);
|
||||
break;
|
||||
case 'remote':
|
||||
replacements = this.getRemoteInterpolateReplacements(item);
|
||||
break;
|
||||
case 'variant-analysis':
|
||||
replacements = this.getVariantAnalysisInterpolateReplacements(item);
|
||||
break;
|
||||
default:
|
||||
assertNever(item);
|
||||
}
|
||||
|
||||
const rawLabel = item.userSpecifiedLabel ?? (this.config.format || '%q');
|
||||
|
||||
@@ -39,9 +53,7 @@ export class HistoryItemLabelProvider {
|
||||
getShortLabel(item: QueryHistoryInfo): string {
|
||||
return item.userSpecifiedLabel
|
||||
? this.getLabel(item)
|
||||
: item.t === 'local'
|
||||
? item.getQueryName()
|
||||
: item.remoteQuery.queryName;
|
||||
: getRawQueryName(item);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,4 +102,17 @@ export class HistoryItemLabelProvider {
|
||||
'%': '%'
|
||||
};
|
||||
}
|
||||
|
||||
private getVariantAnalysisInterpolateReplacements(item: VariantAnalysisHistoryItem): InterpolateReplacements {
|
||||
const resultCount = item.resultCount ? `(${pluralize(item.resultCount, 'result', 'results')})` : '';
|
||||
return {
|
||||
t: new Date(item.variantAnalysis.executionStartTime).toLocaleString(env.language),
|
||||
q: `${item.variantAnalysis.query.name} (${item.variantAnalysis.query.language})`,
|
||||
d: 'TODO',
|
||||
r: resultCount,
|
||||
s: item.status,
|
||||
f: path.basename(item.variantAnalysis.query.filePath),
|
||||
'%': '%',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
|
||||
forceReveal: WebviewReveal,
|
||||
shouldKeepOldResultsWhileRendering = false
|
||||
): Promise<void> {
|
||||
if (!fullQuery.completedQuery.sucessful) {
|
||||
if (!fullQuery.completedQuery.successful) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,4 +56,10 @@ export class LegacyQueryRunner extends QueryRunner {
|
||||
async upgradeDatabaseExplicit(dbItem: DatabaseItem, progress: ProgressCallback, token: CancellationToken): Promise<void> {
|
||||
await upgradeDatabaseExplicit(this.qs, dbItem, progress, token);
|
||||
}
|
||||
|
||||
async clearPackCache(): Promise<void> {
|
||||
/**
|
||||
* Nothing needs to be done
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export class QueryInProgress {
|
||||
}
|
||||
|
||||
get compiledQueryPath() {
|
||||
return path.join(this.querySaveDir, 'compiledQuery.qlo');
|
||||
return this.queryEvalInfo.compileQueryPath;
|
||||
}
|
||||
|
||||
|
||||
@@ -420,7 +420,7 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
query: query.queryEvalInfo,
|
||||
message,
|
||||
result,
|
||||
sucessful: result.resultType == messages.QueryResultType.SUCCESS,
|
||||
successful: result.resultType == messages.QueryResultType.SUCCESS,
|
||||
logFileLocation: result.logFileLocation,
|
||||
dispose: () => {
|
||||
qs.logger.removeAdditionalLogLocation(result.logFileLocation);
|
||||
@@ -497,11 +497,19 @@ function createSyntheticResult(
|
||||
return {
|
||||
query: query.queryEvalInfo,
|
||||
message,
|
||||
sucessful: false,
|
||||
result: {
|
||||
evaluationTime: 0,
|
||||
queryId: 0,
|
||||
resultType: messages.QueryResultType.OTHER_ERROR,
|
||||
message,
|
||||
runId: 0,
|
||||
},
|
||||
successful: false,
|
||||
dispose: () => { /**/ },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function createSimpleTemplates(templates: Record<string, string> | undefined): messages.TemplateDefinitions | undefined {
|
||||
if (!templates) {
|
||||
return undefined;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getOnDiskWorkspaceFolders, showAndLogErrorMessage, tmpDir } from '../he
|
||||
import { ProgressCallback, UserCancellationException } from '../commandRunner';
|
||||
import { logger } from '../logging';
|
||||
import * as messages from '../pure/legacy-messages';
|
||||
import * as qsClient from '../legacy-query-server/queryserver-client';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import * as tmp from 'tmp-promise';
|
||||
import * as path from 'path';
|
||||
import { DatabaseItem } from '../databases';
|
||||
|
||||
@@ -2,8 +2,6 @@ import * as I from 'immutable';
|
||||
import { EvaluationLogProblemReporter, EvaluationLogScanner, EvaluationLogScannerProvider } from './log-scanner';
|
||||
import { InLayer, ComputeRecursive, SummaryEvent, PipelineRun, ComputeSimple } from './log-summary';
|
||||
|
||||
const DEFAULT_WARNING_THRESHOLD = 50;
|
||||
|
||||
/**
|
||||
* Like `max`, but returns 0 if no meaningful maximum can be computed.
|
||||
*/
|
||||
@@ -454,7 +452,11 @@ class JoinOrderScanner implements EvaluationLogScanner {
|
||||
}
|
||||
|
||||
export class JoinOrderScannerProvider implements EvaluationLogScannerProvider {
|
||||
constructor(private readonly getThreshdold: () => number) {
|
||||
}
|
||||
|
||||
public createScanner(problemReporter: EvaluationLogProblemReporter): EvaluationLogScanner {
|
||||
return new JoinOrderScanner(problemReporter, DEFAULT_WARNING_THRESHOLD);
|
||||
const threshold = this.getThreshdold();
|
||||
return new JoinOrderScanner(problemReporter, threshold);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Diagnostic, DiagnosticSeverity, languages, Range, Uri } from 'vscode';
|
||||
import { DisposableObject } from '../pure/disposable-object';
|
||||
import { QueryHistoryManager } from '../query-history';
|
||||
import { QueryHistoryInfo } from '../query-results';
|
||||
import { QueryHistoryInfo } from '../query-history-info';
|
||||
import { EvaluationLogProblemReporter, EvaluationLogScannerSet } from './log-scanner';
|
||||
import { PipelineInfo, SummarySymbols } from './summary-parser';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
72
extensions/ql-vscode/src/mocks/gh-api-request.ts
Normal file
72
extensions/ql-vscode/src/mocks/gh-api-request.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Repository } from '../remote-queries/gh-api/repository';
|
||||
import { VariantAnalysis, VariantAnalysisRepoTask } from '../remote-queries/gh-api/variant-analysis';
|
||||
|
||||
// Types that represent requests/responses from the GitHub API
|
||||
// that we need to mock.
|
||||
|
||||
export enum RequestKind {
|
||||
GetRepo = 'getRepo',
|
||||
SubmitVariantAnalysis = 'submitVariantAnalysis',
|
||||
GetVariantAnalysis = 'getVariantAnalysis',
|
||||
GetVariantAnalysisRepo = 'getVariantAnalysisRepo',
|
||||
GetVariantAnalysisRepoResult = 'getVariantAnalysisRepoResult',
|
||||
}
|
||||
|
||||
export interface GetRepoRequest {
|
||||
request: {
|
||||
kind: RequestKind.GetRepo
|
||||
},
|
||||
response: {
|
||||
status: number,
|
||||
body: Repository
|
||||
}
|
||||
}
|
||||
|
||||
export interface SubmitVariantAnalysisRequest {
|
||||
request: {
|
||||
kind: RequestKind.SubmitVariantAnalysis
|
||||
},
|
||||
response: {
|
||||
status: number,
|
||||
body: VariantAnalysis
|
||||
}
|
||||
}
|
||||
|
||||
export interface GetVariantAnalysisRequest {
|
||||
request: {
|
||||
kind: RequestKind.GetVariantAnalysis
|
||||
},
|
||||
response: {
|
||||
status: number,
|
||||
body: VariantAnalysis
|
||||
}
|
||||
}
|
||||
|
||||
export interface GetVariantAnalysisRepoRequest {
|
||||
request: {
|
||||
kind: RequestKind.GetVariantAnalysisRepo,
|
||||
repositoryId: number
|
||||
},
|
||||
response: {
|
||||
status: number,
|
||||
body: VariantAnalysisRepoTask
|
||||
}
|
||||
}
|
||||
|
||||
export interface GetVariantAnalysisRepoResultRequest {
|
||||
request: {
|
||||
kind: RequestKind.GetVariantAnalysisRepoResult,
|
||||
repositoryId: number
|
||||
},
|
||||
response: {
|
||||
status: number,
|
||||
body: ArrayBuffer
|
||||
}
|
||||
}
|
||||
|
||||
export type GitHubApiRequest =
|
||||
| GetRepoRequest
|
||||
| SubmitVariantAnalysisRequest
|
||||
| GetVariantAnalysisRequest
|
||||
| GetVariantAnalysisRepoRequest
|
||||
| GetVariantAnalysisRepoResultRequest;
|
||||
173
extensions/ql-vscode/src/mocks/mock-gh-api-server.ts
Normal file
173
extensions/ql-vscode/src/mocks/mock-gh-api-server.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import { commands, env, ExtensionContext, ExtensionMode, Uri, window } from 'vscode';
|
||||
import { setupServer, SetupServerApi } from 'msw/node';
|
||||
|
||||
import { getMockGitHubApiServerScenariosPath, MockGitHubApiConfigListener } from '../config';
|
||||
import { DisposableObject } from '../pure/disposable-object';
|
||||
|
||||
import { Recorder } from './recorder';
|
||||
|
||||
/**
|
||||
* Enables mocking of the GitHub API server via HTTP interception, using msw.
|
||||
*/
|
||||
export class MockGitHubApiServer extends DisposableObject {
|
||||
private isListening: boolean;
|
||||
private config: MockGitHubApiConfigListener;
|
||||
|
||||
private readonly server: SetupServerApi;
|
||||
private readonly recorder: Recorder;
|
||||
|
||||
constructor(
|
||||
private readonly ctx: ExtensionContext,
|
||||
) {
|
||||
super();
|
||||
this.isListening = false;
|
||||
this.config = new MockGitHubApiConfigListener();
|
||||
|
||||
this.server = setupServer();
|
||||
this.recorder = this.push(new Recorder(this.server));
|
||||
|
||||
this.setupConfigListener();
|
||||
}
|
||||
|
||||
public startServer(): void {
|
||||
if (this.isListening) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.server.listen();
|
||||
this.isListening = true;
|
||||
}
|
||||
|
||||
public stopServer(): void {
|
||||
this.server.close();
|
||||
this.isListening = false;
|
||||
}
|
||||
|
||||
public loadScenario(): void {
|
||||
// TODO: Implement logic to load a scenario from a directory.
|
||||
}
|
||||
|
||||
public listScenarios(): void {
|
||||
// TODO: Implement logic to list all available scenarios.
|
||||
}
|
||||
|
||||
public async startRecording(): Promise<void> {
|
||||
if (this.recorder.isRecording) {
|
||||
void window.showErrorMessage('A scenario is already being recorded. Use the "Save Scenario" or "Cancel Scenario" commands to finish recording.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.recorder.start();
|
||||
// Set a value in the context to track whether we are recording. This allows us to use this to show/hide commands (see package.json)
|
||||
await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', true);
|
||||
|
||||
await window.showInformationMessage('Recording scenario. To save the scenario, use the "CodeQL Mock GitHub API Server: Save Scenario" command.');
|
||||
}
|
||||
|
||||
public async saveScenario(): Promise<void> {
|
||||
const scenariosPath = await this.getScenariosPath();
|
||||
if (!scenariosPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set a value in the context to track whether we are recording. This allows us to use this to show/hide commands (see package.json)
|
||||
await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', false);
|
||||
|
||||
if (!this.recorder.isRecording) {
|
||||
void window.showErrorMessage('No scenario is currently being recorded.');
|
||||
return;
|
||||
}
|
||||
if (!this.recorder.anyRequestsRecorded) {
|
||||
void window.showWarningMessage('No requests were recorded. Cancelling scenario.');
|
||||
|
||||
await this.stopRecording();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const name = await window.showInputBox({
|
||||
title: 'Save scenario',
|
||||
prompt: 'Enter a name for the scenario.',
|
||||
placeHolder: 'successful-run',
|
||||
});
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = await this.recorder.save(scenariosPath, name);
|
||||
|
||||
await this.stopRecording();
|
||||
|
||||
const action = await window.showInformationMessage(`Scenario saved to ${filePath}`, 'Open directory');
|
||||
if (action === 'Open directory') {
|
||||
await env.openExternal(Uri.file(filePath));
|
||||
}
|
||||
}
|
||||
|
||||
public async cancelRecording(): Promise<void> {
|
||||
if (!this.recorder.isRecording) {
|
||||
void window.showErrorMessage('No scenario is currently being recorded.');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.stopRecording();
|
||||
|
||||
void window.showInformationMessage('Recording cancelled.');
|
||||
}
|
||||
|
||||
private async stopRecording(): Promise<void> {
|
||||
// Set a value in the context to track whether we are recording. This allows us to use this to show/hide commands (see package.json)
|
||||
await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', false);
|
||||
|
||||
await this.recorder.stop();
|
||||
await this.recorder.clear();
|
||||
}
|
||||
|
||||
private async getScenariosPath(): Promise<string | undefined> {
|
||||
const scenariosPath = getMockGitHubApiServerScenariosPath();
|
||||
if (scenariosPath) {
|
||||
return scenariosPath;
|
||||
}
|
||||
|
||||
if (this.ctx.extensionMode === ExtensionMode.Development) {
|
||||
const developmentScenariosPath = Uri.joinPath(this.ctx.extensionUri, 'src/mocks/scenarios').fsPath.toString();
|
||||
if (await fs.pathExists(developmentScenariosPath)) {
|
||||
return developmentScenariosPath;
|
||||
}
|
||||
}
|
||||
|
||||
const directories = await window.showOpenDialog({
|
||||
canSelectFolders: true,
|
||||
canSelectFiles: false,
|
||||
canSelectMany: false,
|
||||
openLabel: 'Select scenarios directory',
|
||||
title: 'Select scenarios directory',
|
||||
});
|
||||
if (directories === undefined || directories.length === 0) {
|
||||
void window.showErrorMessage('No scenarios directory selected.');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Unfortunately, we cannot save the directory in the configuration because that requires
|
||||
// the configuration to be registered. If we do that, it would be visible to all users; there
|
||||
// is no "when" clause that would allow us to only show it to users who have enabled the feature flag.
|
||||
|
||||
return directories[0].fsPath;
|
||||
}
|
||||
|
||||
private setupConfigListener(): void {
|
||||
// The config "changes" from the default at startup, so we need to call onConfigChange() to ensure the server is
|
||||
// started if required.
|
||||
this.onConfigChange();
|
||||
this.config.onDidChangeConfiguration(() => this.onConfigChange());
|
||||
}
|
||||
|
||||
private onConfigChange(): void {
|
||||
if (this.config.mockServerEnabled && !this.isListening) {
|
||||
this.startServer();
|
||||
} else if (!this.config.mockServerEnabled && this.isListening) {
|
||||
this.stopServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
176
extensions/ql-vscode/src/mocks/recorder.ts
Normal file
176
extensions/ql-vscode/src/mocks/recorder.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { MockedRequest } from 'msw';
|
||||
import { SetupServerApi } from 'msw/node';
|
||||
import { IsomorphicResponse } from '@mswjs/interceptors';
|
||||
|
||||
import { DisposableObject } from '../pure/disposable-object';
|
||||
|
||||
import { GitHubApiRequest, RequestKind } from './gh-api-request';
|
||||
|
||||
export class Recorder extends DisposableObject {
|
||||
private readonly allRequests = new Map<string, MockedRequest>();
|
||||
private currentRecordedScenario: GitHubApiRequest[] = [];
|
||||
|
||||
private _isRecording = false;
|
||||
|
||||
constructor(
|
||||
private readonly server: SetupServerApi,
|
||||
) {
|
||||
super();
|
||||
this.onRequestStart = this.onRequestStart.bind(this);
|
||||
this.onResponseBypass = this.onResponseBypass.bind(this);
|
||||
}
|
||||
|
||||
public get isRecording(): boolean {
|
||||
return this._isRecording;
|
||||
}
|
||||
|
||||
public get anyRequestsRecorded(): boolean {
|
||||
return this.currentRecordedScenario.length > 0;
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
if (this._isRecording) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isRecording = true;
|
||||
|
||||
this.clear();
|
||||
|
||||
this.server.events.on('request:start', this.onRequestStart);
|
||||
this.server.events.on('response:bypass', this.onResponseBypass);
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (!this._isRecording) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isRecording = false;
|
||||
|
||||
this.server.events.removeListener('request:start', this.onRequestStart);
|
||||
this.server.events.removeListener('response:bypass', this.onResponseBypass);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.currentRecordedScenario = [];
|
||||
this.allRequests.clear();
|
||||
}
|
||||
|
||||
public async save(scenariosPath: string, name: string): Promise<string> {
|
||||
const scenarioDirectory = path.join(scenariosPath, name);
|
||||
|
||||
await fs.ensureDir(scenarioDirectory);
|
||||
|
||||
for (let i = 0; i < this.currentRecordedScenario.length; i++) {
|
||||
const request = this.currentRecordedScenario[i];
|
||||
|
||||
const fileName = `${i}-${request.request.kind}.json`;
|
||||
const filePath = path.join(scenarioDirectory, fileName);
|
||||
await fs.writeFile(filePath, JSON.stringify(request, null, 2));
|
||||
}
|
||||
|
||||
this.stop();
|
||||
|
||||
return scenarioDirectory;
|
||||
}
|
||||
|
||||
private onRequestStart(request: MockedRequest): void {
|
||||
this.allRequests.set(request.id, request);
|
||||
}
|
||||
|
||||
private onResponseBypass(response: IsomorphicResponse, requestId: string): void {
|
||||
const request = this.allRequests.get(requestId);
|
||||
this.allRequests.delete(requestId);
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.body === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gitHubApiRequest = createGitHubApiRequest(request.url.toString(), response.status, response.body);
|
||||
if (!gitHubApiRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentRecordedScenario.push(gitHubApiRequest);
|
||||
}
|
||||
}
|
||||
|
||||
function createGitHubApiRequest(url: string, status: number, body: string): GitHubApiRequest | undefined {
|
||||
if (!url) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (url.match(/\/repos\/[a-zA-Z0-9-_.]+\/[a-zA-Z0-9-_.]+$/)) {
|
||||
return {
|
||||
request: {
|
||||
kind: RequestKind.GetRepo,
|
||||
},
|
||||
response: {
|
||||
status,
|
||||
body: JSON.parse(body),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (url.match(/\/repositories\/\d+\/code-scanning\/codeql\/variant-analyses$/)) {
|
||||
return {
|
||||
request: {
|
||||
kind: RequestKind.SubmitVariantAnalysis,
|
||||
},
|
||||
response: {
|
||||
status,
|
||||
body: JSON.parse(body),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (url.match(/\/repositories\/\d+\/code-scanning\/codeql\/variant-analyses\/\d+$/)) {
|
||||
return {
|
||||
request: {
|
||||
kind: RequestKind.GetVariantAnalysis,
|
||||
},
|
||||
response: {
|
||||
status,
|
||||
body: JSON.parse(body),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const repoTaskMatch = url.match(/\/repositories\/\d+\/code-scanning\/codeql\/variant-analyses\/\d+\/repositories\/(?<repositoryId>\d+)$/);
|
||||
if (repoTaskMatch?.groups?.repositoryId) {
|
||||
return {
|
||||
request: {
|
||||
kind: RequestKind.GetVariantAnalysisRepo,
|
||||
repositoryId: parseInt(repoTaskMatch.groups.repositoryId, 10),
|
||||
},
|
||||
response: {
|
||||
status,
|
||||
body: JSON.parse(body),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// if url is a download URL for a variant analysis result, then it's a get-variant-analysis-repoResult.
|
||||
const repoDownloadMatch = url.match(/objects-origin\.githubusercontent\.com\/codeql-query-console\/codeql-variant-analysis-repo-tasks\/\d+\/(?<repositoryId>\d+)/);
|
||||
if (repoDownloadMatch?.groups?.repositoryId) {
|
||||
return {
|
||||
request: {
|
||||
kind: RequestKind.GetVariantAnalysisRepoResult,
|
||||
repositoryId: parseInt(repoDownloadMatch.groups.repositoryId, 10),
|
||||
},
|
||||
response: {
|
||||
status,
|
||||
body: body as unknown as ArrayBuffer,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -24,3 +24,16 @@ export function formatDate(value: Date): string {
|
||||
|
||||
return dateFormatter.format(value);
|
||||
}
|
||||
|
||||
// These are overloads for the function that allow us to not add an extra
|
||||
// type check when the value is definitely not undefined.
|
||||
export function parseDate(value: string): Date;
|
||||
export function parseDate(value: string | undefined | null): Date | undefined;
|
||||
|
||||
export function parseDate(value: string | undefined | null): Date | undefined {
|
||||
if (value === undefined || value === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new Date(value);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ import * as sarif from 'sarif';
|
||||
import { AnalysisResults } from '../remote-queries/shared/analysis-result';
|
||||
import { AnalysisSummary, RemoteQueryResult } from '../remote-queries/shared/remote-query-result';
|
||||
import { RawResultSet, ResultRow, ResultSetSchema, Column, ResolvableLocationValue } from './bqrs-cli-types';
|
||||
import { VariantAnalysis } from '../remote-queries/shared/variant-analysis';
|
||||
import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
VariantAnalysisScannedRepositoryState,
|
||||
} from '../remote-queries/shared/variant-analysis';
|
||||
|
||||
/**
|
||||
* This module contains types and code that are shared between
|
||||
@@ -441,8 +445,46 @@ export interface SetVariantAnalysisMessage {
|
||||
variantAnalysis: VariantAnalysis;
|
||||
}
|
||||
|
||||
export type StopVariantAnalysisMessage = {
|
||||
t: 'stopVariantAnalysis';
|
||||
variantAnalysisId: number;
|
||||
}
|
||||
|
||||
export type VariantAnalysisState = {
|
||||
variantAnalysisId: number;
|
||||
}
|
||||
|
||||
export interface SetRepoResultsMessage {
|
||||
t: 'setRepoResults';
|
||||
repoResults: VariantAnalysisScannedRepositoryResult[];
|
||||
}
|
||||
|
||||
export interface SetRepoStatesMessage {
|
||||
t: 'setRepoStates';
|
||||
repoStates: VariantAnalysisScannedRepositoryState[];
|
||||
}
|
||||
|
||||
export interface RequestRepositoryResultsMessage {
|
||||
t: 'requestRepositoryResults';
|
||||
repositoryFullName: string;
|
||||
}
|
||||
|
||||
export interface OpenQueryFileMessage {
|
||||
t: 'openQueryFile';
|
||||
}
|
||||
|
||||
export interface OpenQueryTextMessage {
|
||||
t: 'openQueryText';
|
||||
}
|
||||
|
||||
export type ToVariantAnalysisMessage =
|
||||
| SetVariantAnalysisMessage;
|
||||
| SetVariantAnalysisMessage
|
||||
| SetRepoResultsMessage
|
||||
| SetRepoStatesMessage;
|
||||
|
||||
export type FromVariantAnalysisMessage =
|
||||
| ViewLoadedMsg;
|
||||
| ViewLoadedMsg
|
||||
| StopVariantAnalysisMessage
|
||||
| RequestRepositoryResultsMessage
|
||||
| OpenQueryFileMessage
|
||||
| OpenQueryTextMessage;
|
||||
|
||||
215
extensions/ql-vscode/src/pure/new-messages.ts
Normal file
215
extensions/ql-vscode/src/pure/new-messages.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Types for messages exchanged during jsonrpc communication with the
|
||||
* the CodeQL query server.
|
||||
*
|
||||
* This file exists in the queryserver and in the vscode extension, and
|
||||
* should be kept in sync between them.
|
||||
*
|
||||
* A note about the namespaces below, which look like they are
|
||||
* essentially enums, namely Severity, ResultColumnKind, and
|
||||
* QueryResultType. By design, for the sake of extensibility, clients
|
||||
* receiving messages of this protocol are supposed to accept any
|
||||
* number for any of these types. We commit to the given meaning of
|
||||
* the numbers listed in constants in the namespaces, and we commit to
|
||||
* the fact that any unknown QueryResultType value counts as an error.
|
||||
*/
|
||||
|
||||
import * as rpc from 'vscode-jsonrpc';
|
||||
import * as shared from './messages-shared';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Parameters to clear the cache
|
||||
*/
|
||||
export interface ClearCacheParams {
|
||||
/**
|
||||
* The dataset for which we want to clear the cache
|
||||
*/
|
||||
db: string;
|
||||
/**
|
||||
* Whether the cache should actually be cleared.
|
||||
*/
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for trimming the cache of a dataset
|
||||
*/
|
||||
export interface TrimCacheParams {
|
||||
/**
|
||||
* The dataset that we want to trim the cache of.
|
||||
*/
|
||||
db: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of trimming or clearing the cache.
|
||||
*/
|
||||
export interface ClearCacheResult {
|
||||
/**
|
||||
* A user friendly message saying what was or would be
|
||||
* deleted.
|
||||
*/
|
||||
deletionMessage: string;
|
||||
}
|
||||
|
||||
|
||||
export type QueryResultType = number;
|
||||
/**
|
||||
* The result of running a query. This namespace is intentionally not
|
||||
* an enum, see "for the sake of extensibility" comment above.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace QueryResultType {
|
||||
/**
|
||||
* The query ran successfully
|
||||
*/
|
||||
export const SUCCESS = 0;
|
||||
/**
|
||||
* The query failed due to an reason
|
||||
* that isn't listed
|
||||
*/
|
||||
export const OTHER_ERROR = 1;
|
||||
/**
|
||||
* The query failed do to compilation erorrs
|
||||
*/
|
||||
export const COMPILATION_ERROR = 2;
|
||||
/**
|
||||
* The query failed due to running out of
|
||||
* memory
|
||||
*/
|
||||
export const OOM = 3;
|
||||
/**
|
||||
* The query failed because it was cancelled.
|
||||
*/
|
||||
export const CANCELLATION = 4;
|
||||
/**
|
||||
* The dbscheme basename was not the same
|
||||
*/
|
||||
export const DBSCHEME_MISMATCH_NAME = 5;
|
||||
/**
|
||||
* No upgrade was found
|
||||
*/
|
||||
export const DBSCHEME_NO_UPGRADE = 6;
|
||||
}
|
||||
|
||||
|
||||
export interface RegisterDatabasesParams {
|
||||
databases: string[];
|
||||
}
|
||||
|
||||
export interface DeregisterDatabasesParams {
|
||||
databases: string[];
|
||||
}
|
||||
|
||||
export type RegisterDatabasesResult = {
|
||||
registeredDatabases: string[];
|
||||
};
|
||||
|
||||
export type DeregisterDatabasesResult = {
|
||||
registeredDatabases: string[];
|
||||
};
|
||||
|
||||
|
||||
export interface RunQueryParams {
|
||||
/**
|
||||
* The path of the query
|
||||
*/
|
||||
queryPath: string,
|
||||
/**
|
||||
* The output path
|
||||
*/
|
||||
outputPath: string,
|
||||
/**
|
||||
* The database path
|
||||
*/
|
||||
db: string,
|
||||
additionalPacks: string[],
|
||||
target: CompilationTarget,
|
||||
externalInputs: Record<string, string>,
|
||||
singletonExternalInputs: Record<string, string>,
|
||||
dilPath?: string,
|
||||
logPath?: string
|
||||
}
|
||||
|
||||
export interface RunQueryResult {
|
||||
resultType: QueryResultType,
|
||||
message?: string,
|
||||
expectedDbschemeName?: string,
|
||||
evaluationTime: number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface UpgradeParams {
|
||||
db: string,
|
||||
additionalPacks: string[],
|
||||
}
|
||||
|
||||
export type UpgradeResult = Record<string, unknown>;
|
||||
|
||||
export type ClearPackCacheParams = Record<string, unknown>;
|
||||
export type ClearPackCacheResult = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* A position within a QL file.
|
||||
*/
|
||||
export type Position = shared.Position;
|
||||
|
||||
/**
|
||||
* The way of compiling the query, as a normal query
|
||||
* or a subset of it. Note that precisely one of the two options should be set.
|
||||
*/
|
||||
export type CompilationTarget = shared.CompilationTarget;
|
||||
|
||||
export type QuickEvalOptions = shared.QuickEvalOptions;
|
||||
|
||||
export type WithProgressId<T> = shared.WithProgressId<T>;
|
||||
export type ProgressMessage = shared.ProgressMessage;
|
||||
|
||||
/**
|
||||
* Clear the cache of a dataset
|
||||
*/
|
||||
export const clearCache = new rpc.RequestType<WithProgressId<ClearCacheParams>, ClearCacheResult, void, void>('evaluation/clearCache');
|
||||
/**
|
||||
* Trim the cache of a dataset
|
||||
*/
|
||||
export const trimCache = new rpc.RequestType<WithProgressId<TrimCacheParams>, ClearCacheResult, void, void>('evaluation/trimCache');
|
||||
|
||||
/**
|
||||
* Clear the pack cache
|
||||
*/
|
||||
export const clearPackCache = new rpc.RequestType<WithProgressId<ClearPackCacheParams>, ClearPackCacheResult, void, void>('evaluation/clearPackCache');
|
||||
|
||||
/**
|
||||
* Run a query on a database
|
||||
*/
|
||||
export const runQuery = new rpc.RequestType<WithProgressId<RunQueryParams>, RunQueryResult, void, void>('evaluation/runQuery');
|
||||
|
||||
export const registerDatabases = new rpc.RequestType<
|
||||
WithProgressId<RegisterDatabasesParams>,
|
||||
RegisterDatabasesResult,
|
||||
void,
|
||||
void
|
||||
>('evaluation/registerDatabases');
|
||||
|
||||
export const deregisterDatabases = new rpc.RequestType<
|
||||
WithProgressId<DeregisterDatabasesParams>,
|
||||
DeregisterDatabasesResult,
|
||||
void,
|
||||
void
|
||||
>('evaluation/deregisterDatabases');
|
||||
|
||||
|
||||
export const upgradeDatabase = new rpc.RequestType<
|
||||
WithProgressId<UpgradeParams>,
|
||||
UpgradeResult,
|
||||
void,
|
||||
void
|
||||
>('evaluation/runUpgrade');
|
||||
|
||||
/**
|
||||
* A notification that the progress has been changed.
|
||||
*/
|
||||
export const progress = shared.progress;
|
||||
45
extensions/ql-vscode/src/query-history-info.ts
Normal file
45
extensions/ql-vscode/src/query-history-info.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
|
||||
import { VariantAnalysisHistoryItem } from './remote-queries/variant-analysis-history-item';
|
||||
import { LocalQueryInfo } from './query-results';
|
||||
import { assertNever } from './pure/helpers-pure';
|
||||
|
||||
export type QueryHistoryInfo = LocalQueryInfo | RemoteQueryHistoryItem | VariantAnalysisHistoryItem;
|
||||
|
||||
export function getRawQueryName(item: QueryHistoryInfo): string {
|
||||
switch (item.t) {
|
||||
case 'local':
|
||||
return item.getQueryName();
|
||||
case 'remote':
|
||||
return item.remoteQuery.queryName;
|
||||
case 'variant-analysis':
|
||||
return item.variantAnalysis.query.name;
|
||||
default:
|
||||
assertNever(item);
|
||||
}
|
||||
}
|
||||
|
||||
export function getQueryHistoryItemId(item: QueryHistoryInfo): string {
|
||||
switch (item.t) {
|
||||
case 'local':
|
||||
return item.initialInfo.id;
|
||||
case 'remote':
|
||||
return item.queryId;
|
||||
case 'variant-analysis':
|
||||
return item.historyItemId;
|
||||
default:
|
||||
assertNever(item);
|
||||
}
|
||||
}
|
||||
|
||||
export function getQueryText(item: QueryHistoryInfo): string {
|
||||
switch (item.t) {
|
||||
case 'local':
|
||||
return item.initialInfo.queryText;
|
||||
case 'remote':
|
||||
return item.remoteQuery.queryText;
|
||||
case 'variant-analysis':
|
||||
return item.variantAnalysis.query.text;
|
||||
default:
|
||||
assertNever(item);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ type Counter = {
|
||||
* @param queryDirectory The directory containing all queries.
|
||||
* @param ctx The extension context.
|
||||
*/
|
||||
export function registerQueryHistoryScubber(
|
||||
export function registerQueryHistoryScrubber(
|
||||
wakeInterval: number,
|
||||
throttleTime: number,
|
||||
maxQueryTime: number,
|
||||
|
||||
@@ -30,9 +30,10 @@ import { DisposableObject } from './pure/disposable-object';
|
||||
import { commandRunner } from './commandRunner';
|
||||
import { ONE_HOUR_IN_MS, TWO_HOURS_IN_MS } from './pure/time';
|
||||
import { assertNever, getErrorMessage, getErrorStack } from './pure/helpers-pure';
|
||||
import { CompletedLocalQueryInfo, LocalQueryInfo as LocalQueryInfo, QueryHistoryInfo } from './query-results';
|
||||
import { CompletedLocalQueryInfo, LocalQueryInfo } from './query-results';
|
||||
import { getQueryHistoryItemId, getQueryText, QueryHistoryInfo } from './query-history-info';
|
||||
import { DatabaseManager } from './databases';
|
||||
import { registerQueryHistoryScubber } from './query-history-scrubber';
|
||||
import { registerQueryHistoryScrubber } from './query-history-scrubber';
|
||||
import { QueryStatus } from './query-status';
|
||||
import { slurpQueryHistory, splatQueryHistory } from './query-serialization';
|
||||
import * as fs from 'fs-extra';
|
||||
@@ -49,6 +50,8 @@ import EvalLogTreeBuilder from './eval-log-tree-builder';
|
||||
import { EvalLogData, parseViewerData } from './pure/log-summary-parser';
|
||||
import { QueryWithResults } from './run-queries-shared';
|
||||
import { QueryRunner } from './queryRunner';
|
||||
import { VariantAnalysisManager } from './remote-queries/variant-analysis-manager';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
/**
|
||||
* query-history.ts
|
||||
@@ -206,13 +209,9 @@ export class HistoryTreeDataProvider extends DisposableObject implements TreeDat
|
||||
const h1Label = this.labelProvider.getLabel(h1).toLowerCase();
|
||||
const h2Label = this.labelProvider.getLabel(h2).toLowerCase();
|
||||
|
||||
const h1Date = h1.t === 'local'
|
||||
? h1.initialInfo.start.getTime()
|
||||
: h1.remoteQuery?.executionStartTime;
|
||||
const h1Date = this.getItemDate(h1);
|
||||
|
||||
const h2Date = h2.t === 'local'
|
||||
? h2.initialInfo.start.getTime()
|
||||
: h2.remoteQuery?.executionStartTime;
|
||||
const h2Date = this.getItemDate(h2);
|
||||
|
||||
const resultCount1 = h1.t === 'local'
|
||||
? h1.completedQuery?.resultCount ?? -1
|
||||
@@ -311,6 +310,19 @@ export class HistoryTreeDataProvider extends DisposableObject implements TreeDat
|
||||
this._sortOrder = newSortOrder;
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
}
|
||||
|
||||
private getItemDate(item: QueryHistoryInfo) {
|
||||
switch (item.t) {
|
||||
case 'local':
|
||||
return item.initialInfo.start.getTime();
|
||||
case 'remote':
|
||||
return item.remoteQuery.executionStartTime;
|
||||
case 'variant-analysis':
|
||||
return item.variantAnalysis.executionStartTime;
|
||||
default:
|
||||
assertNever(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class QueryHistoryManager extends DisposableObject {
|
||||
@@ -333,6 +345,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
private readonly dbm: DatabaseManager,
|
||||
private readonly localQueriesResultsView: ResultsView,
|
||||
private readonly remoteQueriesManager: RemoteQueriesManager,
|
||||
private readonly variantAnalysisManager: VariantAnalysisManager,
|
||||
private readonly evalLogViewer: EvalLogViewer,
|
||||
private readonly queryStorageDir: string,
|
||||
private readonly ctx: ExtensionContext,
|
||||
@@ -555,6 +568,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
|
||||
this.registerQueryHistoryScrubber(queryHistoryConfigListener, this, ctx);
|
||||
this.registerToRemoteQueriesEvents();
|
||||
this.registerToVariantAnalysisEvents();
|
||||
}
|
||||
|
||||
public completeQuery(info: LocalQueryInfo, results: QueryWithResults): void {
|
||||
@@ -573,7 +587,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
this.queryHistoryScrubber?.dispose();
|
||||
// Every hour check if we need to re-run the query history scrubber.
|
||||
this.queryHistoryScrubber = this.push(
|
||||
registerQueryHistoryScubber(
|
||||
registerQueryHistoryScrubber(
|
||||
ONE_HOUR_IN_MS,
|
||||
TWO_HOURS_IN_MS,
|
||||
queryHistoryConfigListener.ttlInMillis,
|
||||
@@ -584,6 +598,22 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
);
|
||||
}
|
||||
|
||||
private registerToVariantAnalysisEvents() {
|
||||
const variantAnalysisAddedSubscription = this.variantAnalysisManager.onVariantAnalysisAdded(async (variantAnalysis) => {
|
||||
this.addQuery({
|
||||
t: 'variant-analysis',
|
||||
status: QueryStatus.InProgress,
|
||||
completed: false,
|
||||
historyItemId: nanoid(),
|
||||
variantAnalysis,
|
||||
});
|
||||
|
||||
await this.refreshTreeView();
|
||||
});
|
||||
|
||||
this.push(variantAnalysisAddedSubscription);
|
||||
}
|
||||
|
||||
private registerToRemoteQueriesEvents() {
|
||||
const queryAddedSubscription = this.remoteQueriesManager.onRemoteQueryAdded(async (event) => {
|
||||
this.addQuery({
|
||||
@@ -649,10 +679,20 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryPath = finalSingleItem.t === 'local'
|
||||
? finalSingleItem.initialInfo.queryPath
|
||||
: finalSingleItem.remoteQuery.queryFilePath;
|
||||
|
||||
let queryPath: string;
|
||||
switch (finalSingleItem.t) {
|
||||
case 'local':
|
||||
queryPath = finalSingleItem.initialInfo.queryPath;
|
||||
break;
|
||||
case 'remote':
|
||||
queryPath = finalSingleItem.remoteQuery.queryFilePath;
|
||||
break;
|
||||
case 'variant-analysis':
|
||||
queryPath = finalSingleItem.variantAnalysis.query.filePath;
|
||||
break;
|
||||
default:
|
||||
assertNever(finalSingleItem);
|
||||
}
|
||||
const textDocument = await workspace.openTextDocument(
|
||||
Uri.file(queryPath)
|
||||
);
|
||||
@@ -710,8 +750,12 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
// We need to delete it from disk as well.
|
||||
await item.completedQuery?.query.deleteQuery();
|
||||
}
|
||||
} else {
|
||||
} else if (item.t === 'remote') {
|
||||
await this.removeRemoteQuery(item);
|
||||
} else if (item.t === 'variant-analysis') {
|
||||
// TODO
|
||||
} else {
|
||||
assertNever(item);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -795,7 +839,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
throw new Error('Please select a local query.');
|
||||
}
|
||||
|
||||
if (!finalSingleItem.completedQuery?.sucessful) {
|
||||
if (!finalSingleItem.completedQuery?.successful) {
|
||||
throw new Error('Please select a query that has completed successfully.');
|
||||
}
|
||||
|
||||
@@ -867,6 +911,8 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}
|
||||
} else if (queryHistoryItem.t === 'remote') {
|
||||
return path.join(this.queryStorageDir, queryHistoryItem.queryId);
|
||||
} else if (queryHistoryItem.t === 'variant-analysis') {
|
||||
return this.variantAnalysisManager.getVariantAnalysisStorageLocation(queryHistoryItem.variantAnalysis.id);
|
||||
}
|
||||
|
||||
throw new Error('Unable to get query directory');
|
||||
@@ -889,6 +935,8 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}
|
||||
} else if (finalSingleItem.t === 'remote') {
|
||||
externalFilePath = path.join(this.queryStorageDir, finalSingleItem.queryId, 'timestamp');
|
||||
} else if (finalSingleItem.t === 'variant-analysis') {
|
||||
externalFilePath = path.join(this.variantAnalysisManager.getVariantAnalysisStorageLocation(finalSingleItem.variantAnalysis.id), 'timestamp');
|
||||
}
|
||||
|
||||
if (externalFilePath) {
|
||||
@@ -1023,14 +1071,13 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
|
||||
const params = new URLSearchParams({
|
||||
isQuickEval: String(!!(finalSingleItem.t === 'local' && finalSingleItem.initialInfo.quickEvalPosition)),
|
||||
queryText: encodeURIComponent(await this.getQueryText(finalSingleItem)),
|
||||
queryText: encodeURIComponent(getQueryText(finalSingleItem)),
|
||||
});
|
||||
const queryId = finalSingleItem.t === 'local'
|
||||
? finalSingleItem.initialInfo.id
|
||||
: finalSingleItem.queryId;
|
||||
|
||||
const queryId = getQueryHistoryItemId(finalSingleItem);
|
||||
|
||||
const uri = Uri.parse(
|
||||
`codeql:${queryId}?${params.toString()}`, true
|
||||
`codeql:${queryId}.ql?${params.toString()}`, true
|
||||
);
|
||||
const doc = await workspace.openTextDocument(uri);
|
||||
await window.showTextDocument(doc, { preview: false });
|
||||
@@ -1148,12 +1195,6 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
await commands.executeCommand('codeQL.copyRepoList', finalSingleItem.queryId);
|
||||
}
|
||||
|
||||
async getQueryText(item: QueryHistoryInfo): Promise<string> {
|
||||
return item.t === 'local'
|
||||
? item.initialInfo.queryText
|
||||
: item.remoteQuery.queryText;
|
||||
}
|
||||
|
||||
async handleExportResults(): Promise<void> {
|
||||
await commands.executeCommand('codeQL.exportVariantAnalysisResults');
|
||||
}
|
||||
@@ -1236,7 +1277,7 @@ the file in the file explorer and dragging it into the workspace.`
|
||||
if (!otherQuery.completedQuery) {
|
||||
throw new Error('Please select a completed query.');
|
||||
}
|
||||
if (!otherQuery.completedQuery.sucessful) {
|
||||
if (!otherQuery.completedQuery.successful) {
|
||||
throw new Error('Please select a successful query.');
|
||||
}
|
||||
if (otherQuery.initialInfo.databaseInfo.name !== dbName) {
|
||||
@@ -1256,7 +1297,7 @@ the file in the file explorer and dragging it into the workspace.`
|
||||
otherQuery !== singleItem &&
|
||||
otherQuery.t === 'local' &&
|
||||
otherQuery.completedQuery &&
|
||||
otherQuery.completedQuery.sucessful &&
|
||||
otherQuery.completedQuery.successful &&
|
||||
otherQuery.initialInfo.databaseInfo.name === dbName
|
||||
)
|
||||
.map((item) => ({
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
} from './pure/interface-types';
|
||||
import { DatabaseInfo } from './pure/interface-types';
|
||||
import { QueryStatus } from './query-status';
|
||||
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
|
||||
import { QueryEvaluationInfo, QueryWithResults } from './run-queries-shared';
|
||||
import { formatLegacyMessage } from './legacy-query-server/run-queries';
|
||||
|
||||
@@ -47,11 +46,11 @@ export interface InitialQueryInfo {
|
||||
export class CompletedQueryInfo implements QueryWithResults {
|
||||
readonly query: QueryEvaluationInfo;
|
||||
readonly message?: string;
|
||||
readonly sucessful?: boolean;
|
||||
readonly successful?: boolean;
|
||||
/**
|
||||
* The legacy result. This is only set when loading from the query history.
|
||||
*/
|
||||
readonly result?: legacyMessages.EvaluationResult;
|
||||
readonly result: legacyMessages.EvaluationResult;
|
||||
readonly logFileLocation?: string;
|
||||
resultCount: number;
|
||||
|
||||
@@ -83,11 +82,10 @@ export class CompletedQueryInfo implements QueryWithResults {
|
||||
) {
|
||||
this.query = evaluation.query;
|
||||
this.logFileLocation = evaluation.logFileLocation;
|
||||
if (evaluation.result) {
|
||||
this.result = evaluation.result;
|
||||
}
|
||||
this.result = evaluation.result;
|
||||
|
||||
this.message = evaluation.message;
|
||||
this.sucessful = evaluation.sucessful;
|
||||
this.successful = evaluation.successful;
|
||||
// Use the dispose method from the evaluation.
|
||||
// The dispose will clean up any additional log locations that this
|
||||
// query may have created.
|
||||
@@ -202,14 +200,12 @@ export function ensureMetadataIsComplete(metadata: QueryMetadata | undefined) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in Interface and Compare-Interface for queries that we know have been complated.
|
||||
* Used in Interface and Compare-Interface for queries that we know have been completed.
|
||||
*/
|
||||
export type CompletedLocalQueryInfo = LocalQueryInfo & {
|
||||
completedQuery: CompletedQueryInfo
|
||||
};
|
||||
|
||||
export type QueryHistoryInfo = LocalQueryInfo | RemoteQueryHistoryItem;
|
||||
|
||||
export class LocalQueryInfo {
|
||||
readonly t = 'local';
|
||||
|
||||
@@ -302,7 +298,7 @@ export class LocalQueryInfo {
|
||||
return QueryStatus.Failed;
|
||||
} else if (!this.completedQuery) {
|
||||
return QueryStatus.InProgress;
|
||||
} else if (this.completedQuery.sucessful) {
|
||||
} else if (this.completedQuery.successful) {
|
||||
return QueryStatus.Completed;
|
||||
} else {
|
||||
return QueryStatus.Failed;
|
||||
|
||||
@@ -3,9 +3,11 @@ import * as path from 'path';
|
||||
|
||||
import { showAndLogErrorMessage } from './helpers';
|
||||
import { asyncFilter, getErrorMessage, getErrorStack } from './pure/helpers-pure';
|
||||
import { CompletedQueryInfo, LocalQueryInfo, QueryHistoryInfo } from './query-results';
|
||||
import { CompletedQueryInfo, LocalQueryInfo } from './query-results';
|
||||
import { QueryHistoryInfo } from './query-history-info';
|
||||
import { QueryStatus } from './query-status';
|
||||
import { QueryEvaluationInfo } from './run-queries-shared';
|
||||
import { QueryResultType } from './pure/legacy-messages';
|
||||
|
||||
export async function slurpQueryHistory(fsPath: string): Promise<QueryHistoryInfo[]> {
|
||||
try {
|
||||
@@ -15,8 +17,8 @@ export async function slurpQueryHistory(fsPath: string): Promise<QueryHistoryInf
|
||||
|
||||
const data = await fs.readFile(fsPath, 'utf8');
|
||||
const obj = JSON.parse(data);
|
||||
if (obj.version !== 1) {
|
||||
void showAndLogErrorMessage(`Unsupported query history format: v${obj.version}. `);
|
||||
if (![1, 2].includes(obj.version)) {
|
||||
void showAndLogErrorMessage(`Can't parse query history. Unsupported query history format: v${obj.version}. `);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -38,6 +40,17 @@ export async function slurpQueryHistory(fsPath: string): Promise<QueryHistoryInf
|
||||
Object.setPrototypeOf(q.completedQuery.query, QueryEvaluationInfo.prototype);
|
||||
// slurped queries do not need to be disposed
|
||||
q.completedQuery.dispose = () => { /**/ };
|
||||
|
||||
// Previously, there was a typo in the completedQuery type. There was a field
|
||||
// `sucessful` and it was renamed to `successful`. We need to handle this case.
|
||||
if ('sucessful' in q.completedQuery) {
|
||||
(q.completedQuery as any).successful = (q.completedQuery as any).sucessful;
|
||||
delete (q.completedQuery as any).sucessful;
|
||||
}
|
||||
|
||||
if (!('successful' in q.completedQuery)) {
|
||||
(q.completedQuery as any).successful = q.completedQuery.result?.resultType === QueryResultType.SUCCESS;
|
||||
}
|
||||
}
|
||||
} else if (q.t === 'remote') {
|
||||
// A bug was introduced that didn't set the completed flag in query history
|
||||
@@ -54,7 +67,7 @@ export async function slurpQueryHistory(fsPath: string): Promise<QueryHistoryInf
|
||||
// most likely another workspace has deleted them because the
|
||||
// queries aged out.
|
||||
return asyncFilter(parsedQueries, async (q) => {
|
||||
if (q.t === 'remote') {
|
||||
if (q.t === 'remote' || q.t === 'variant-analysis') {
|
||||
// the slurper doesn't know where the remote queries are stored
|
||||
// so we need to assume here that they exist. Later, we check to
|
||||
// see if they exist on disk.
|
||||
@@ -90,7 +103,10 @@ export async function splatQueryHistory(queries: QueryHistoryInfo[], fsPath: str
|
||||
// remove incomplete local queries since they cannot be recreated on restart
|
||||
const filteredQueries = queries.filter(q => q.t === 'local' ? q.completedQuery !== undefined : true);
|
||||
const data = JSON.stringify({
|
||||
version: 1,
|
||||
// version 2:
|
||||
// - adds the `variant-analysis` type
|
||||
// - ensures a `successful` property exists on completedQuery
|
||||
version: 2,
|
||||
queries: filteredQueries
|
||||
}, null, 2);
|
||||
await fs.writeFile(fsPath, data);
|
||||
|
||||
81
extensions/ql-vscode/src/query-server/query-runner.ts
Normal file
81
extensions/ql-vscode/src/query-server/query-runner.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { CancellationToken } from 'vscode';
|
||||
import { ProgressCallback, UserCancellationException } from '../commandRunner';
|
||||
import { DatabaseItem } from '../databases';
|
||||
import { clearCache, ClearCacheParams, clearPackCache, deregisterDatabases, registerDatabases, upgradeDatabase } from '../pure/new-messages';
|
||||
import { InitialQueryInfo, LocalQueryInfo } from '../query-results';
|
||||
import { QueryRunner } from '../queryRunner';
|
||||
import { QueryWithResults } from '../run-queries-shared';
|
||||
import { QueryServerClient } from './queryserver-client';
|
||||
import { compileAndRunQueryAgainstDatabase } from './run-queries';
|
||||
import * as vscode from 'vscode';
|
||||
import { getOnDiskWorkspaceFolders } from '../helpers';
|
||||
export class NewQueryRunner extends QueryRunner {
|
||||
|
||||
|
||||
constructor(public readonly qs: QueryServerClient) {
|
||||
super();
|
||||
}
|
||||
|
||||
get cliServer() {
|
||||
return this.qs.cliServer;
|
||||
}
|
||||
|
||||
async restartQueryServer(progress: ProgressCallback, token: CancellationToken): Promise<void> {
|
||||
await this.qs.restartQueryServer(progress, token);
|
||||
}
|
||||
|
||||
onStart(callBack: (progress: ProgressCallback, token: CancellationToken) => Promise<void>) {
|
||||
this.qs.onDidStartQueryServer(callBack);
|
||||
}
|
||||
|
||||
async clearCacheInDatabase(dbItem: DatabaseItem, progress: ProgressCallback, token: CancellationToken): Promise<void> {
|
||||
if (dbItem.contents === undefined) {
|
||||
throw new Error('Can\'t clear the cache in an invalid database.');
|
||||
}
|
||||
|
||||
const db = dbItem.databaseUri.fsPath;
|
||||
const params: ClearCacheParams = {
|
||||
dryRun: false,
|
||||
db,
|
||||
};
|
||||
await this.qs.sendRequest(clearCache, params, token, progress);
|
||||
}
|
||||
async compileAndRunQueryAgainstDatabase(dbItem: DatabaseItem, initialInfo: InitialQueryInfo, queryStorageDir: string, progress: ProgressCallback, token: CancellationToken, templates?: Record<string, string>, queryInfo?: LocalQueryInfo): Promise<QueryWithResults> {
|
||||
return await compileAndRunQueryAgainstDatabase(this.qs.cliServer, this.qs, dbItem, initialInfo, queryStorageDir, progress, token, templates, queryInfo);
|
||||
}
|
||||
|
||||
async deregisterDatabase(progress: ProgressCallback, token: CancellationToken, dbItem: DatabaseItem): Promise<void> {
|
||||
if (dbItem.contents && (await this.qs.cliServer.cliConstraints.supportsDatabaseRegistration())) {
|
||||
const databases: string[] = [dbItem.databaseUri.fsPath];
|
||||
await this.qs.sendRequest(deregisterDatabases, { databases }, token, progress);
|
||||
}
|
||||
}
|
||||
async registerDatabase(progress: ProgressCallback, token: CancellationToken, dbItem: DatabaseItem): Promise<void> {
|
||||
if (dbItem.contents && (await this.qs.cliServer.cliConstraints.supportsDatabaseRegistration())) {
|
||||
const databases: string[] = [dbItem.databaseUri.fsPath];
|
||||
await this.qs.sendRequest(registerDatabases, { databases }, token, progress);
|
||||
}
|
||||
}
|
||||
|
||||
async clearPackCache(): Promise<void> {
|
||||
await this.qs.sendRequest(clearPackCache, {});
|
||||
}
|
||||
|
||||
async upgradeDatabaseExplicit(dbItem: DatabaseItem, progress: ProgressCallback, token: CancellationToken): Promise<void> {
|
||||
|
||||
const yesItem = { title: 'Yes', isCloseAffordance: false };
|
||||
const noItem = { title: 'No', isCloseAffordance: true };
|
||||
const dialogOptions: vscode.MessageItem[] = [yesItem, noItem];
|
||||
|
||||
|
||||
|
||||
const message = `Should the database ${dbItem.databaseUri.fsPath} be destructively upgraded?\n\nThis should not be necessary to run queries
|
||||
as we will non-destructively update it anyway.`;
|
||||
const chosenItem = await vscode.window.showInformationMessage(message, { modal: true }, ...dialogOptions);
|
||||
|
||||
if (chosenItem !== yesItem) {
|
||||
throw new UserCancellationException('User cancelled the database upgrade.');
|
||||
}
|
||||
await this.qs.sendRequest(upgradeDatabase, { db: dbItem.databaseUri.fsPath, additionalPacks: getOnDiskWorkspaceFolders() }, token, progress);
|
||||
}
|
||||
}
|
||||
205
extensions/ql-vscode/src/query-server/queryserver-client.ts
Normal file
205
extensions/ql-vscode/src/query-server/queryserver-client.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
import { DisposableObject } from '../pure/disposable-object';
|
||||
import { CancellationToken, commands } from 'vscode';
|
||||
import { createMessageConnection, RequestType } from 'vscode-jsonrpc';
|
||||
import * as cli from '../cli';
|
||||
import { QueryServerConfig } from '../config';
|
||||
import { Logger, ProgressReporter } from '../logging';
|
||||
import { progress, ProgressMessage, WithProgressId } from '../pure/new-messages';
|
||||
import * as messages from '../pure/new-messages';
|
||||
import { ProgressCallback, ProgressTask } from '../commandRunner';
|
||||
import { findQueryLogFile } from '../run-queries-shared';
|
||||
import { ServerProcess } from '../json-rpc-server';
|
||||
|
||||
type ServerOpts = {
|
||||
logger: Logger;
|
||||
contextStoragePath: string;
|
||||
}
|
||||
|
||||
|
||||
type WithProgressReporting = (task: (progress: ProgressReporter, token: CancellationToken) => Thenable<void>) => Thenable<void>;
|
||||
|
||||
/**
|
||||
* Client that manages a query server process.
|
||||
* The server process is started upon initialization and tracked during its lifetime.
|
||||
* The server process is disposed when the client is disposed, or if the client asks
|
||||
* to restart it (which disposes the existing process and starts a new one).
|
||||
*/
|
||||
export class QueryServerClient extends DisposableObject {
|
||||
|
||||
serverProcess?: ServerProcess;
|
||||
progressCallbacks: { [key: number]: ((res: ProgressMessage) => void) | undefined };
|
||||
nextCallback: number;
|
||||
nextProgress: number;
|
||||
withProgressReporting: WithProgressReporting;
|
||||
|
||||
private readonly queryServerStartListeners = [] as ProgressTask<void>[];
|
||||
|
||||
// Can't use standard vscode EventEmitter here since they do not cause the calling
|
||||
// function to fail if one of the event handlers fail. This is something that
|
||||
// we need here.
|
||||
readonly onDidStartQueryServer = (e: ProgressTask<void>) => {
|
||||
this.queryServerStartListeners.push(e);
|
||||
}
|
||||
|
||||
public activeQueryLogFile: string | undefined;
|
||||
|
||||
constructor(
|
||||
readonly config: QueryServerConfig,
|
||||
readonly cliServer: cli.CodeQLCliServer,
|
||||
readonly opts: ServerOpts,
|
||||
withProgressReporting: WithProgressReporting
|
||||
) {
|
||||
super();
|
||||
// When the query server configuration changes, restart the query server.
|
||||
if (config.onDidChangeConfiguration !== undefined) {
|
||||
this.push(config.onDidChangeConfiguration(() =>
|
||||
commands.executeCommand('codeQL.restartQueryServer')));
|
||||
}
|
||||
this.withProgressReporting = withProgressReporting;
|
||||
this.nextCallback = 0;
|
||||
this.nextProgress = 0;
|
||||
this.progressCallbacks = {};
|
||||
}
|
||||
|
||||
get logger(): Logger {
|
||||
return this.opts.logger;
|
||||
}
|
||||
|
||||
/** Stops the query server by disposing of the current server process. */
|
||||
private stopQueryServer(): void {
|
||||
if (this.serverProcess !== undefined) {
|
||||
this.disposeAndStopTracking(this.serverProcess);
|
||||
} else {
|
||||
void this.logger.log('No server process to be stopped.');
|
||||
}
|
||||
}
|
||||
|
||||
/** Restarts the query server by disposing of the current server process and then starting a new one. */
|
||||
async restartQueryServer(
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
): Promise<void> {
|
||||
this.stopQueryServer();
|
||||
await this.startQueryServer();
|
||||
|
||||
// Ensure we await all responses from event handlers so that
|
||||
// errors can be properly reported to the user.
|
||||
await Promise.all(this.queryServerStartListeners.map(handler => handler(
|
||||
progress,
|
||||
token
|
||||
)));
|
||||
}
|
||||
|
||||
showLog(): void {
|
||||
this.logger.show();
|
||||
}
|
||||
|
||||
/** Starts a new query server process, sending progress messages to the status bar. */
|
||||
async startQueryServer(): Promise<void> {
|
||||
// Use an arrow function to preserve the value of `this`.
|
||||
return this.withProgressReporting((progress, _) => this.startQueryServerImpl(progress));
|
||||
}
|
||||
|
||||
/** Starts a new query server process, sending progress messages to the given reporter. */
|
||||
private async startQueryServerImpl(progressReporter: ProgressReporter): Promise<void> {
|
||||
void this.logger.log('Starting NEW query server.');
|
||||
|
||||
const ramArgs = await this.cliServer.resolveRam(this.config.queryMemoryMb, progressReporter);
|
||||
const args = ['--threads', this.config.numThreads.toString()].concat(ramArgs);
|
||||
|
||||
if (this.config.saveCache) {
|
||||
args.push('--save-cache');
|
||||
}
|
||||
|
||||
if (this.config.cacheSize > 0) {
|
||||
args.push('--max-disk-cache');
|
||||
args.push(this.config.cacheSize.toString());
|
||||
}
|
||||
|
||||
const structuredLogFile = `${this.opts.contextStoragePath}/structured-evaluator-log.json`;
|
||||
await fs.ensureFile(structuredLogFile);
|
||||
|
||||
args.push('--evaluator-log');
|
||||
args.push(structuredLogFile);
|
||||
|
||||
// We hard-code the verbosity level to 5 and minify to false.
|
||||
// This will be the behavior of the per-query structured logging in the CLI after 2.8.3.
|
||||
args.push('--evaluator-log-level');
|
||||
args.push('5');
|
||||
|
||||
|
||||
if (this.config.debug) {
|
||||
args.push('--debug', '--tuple-counting');
|
||||
}
|
||||
|
||||
if (cli.shouldDebugQueryServer()) {
|
||||
args.push('-J=-agentlib:jdwp=transport=dt_socket,address=localhost:9010,server=y,suspend=y,quiet=y');
|
||||
}
|
||||
|
||||
const child = cli.spawnServer(
|
||||
this.config.codeQlPath,
|
||||
'CodeQL query server',
|
||||
['execute', 'query-server2'],
|
||||
args,
|
||||
this.logger,
|
||||
data => this.logger.log(data.toString(), {
|
||||
trailingNewline: false,
|
||||
additionalLogLocation: this.activeQueryLogFile
|
||||
}),
|
||||
undefined, // no listener for stdout
|
||||
progressReporter
|
||||
);
|
||||
progressReporter.report({ message: 'Connecting to CodeQL query server' });
|
||||
const connection = createMessageConnection(child.stdout, child.stdin);
|
||||
connection.onNotification(progress, res => {
|
||||
const callback = this.progressCallbacks[res.id];
|
||||
if (callback) {
|
||||
callback(res);
|
||||
}
|
||||
});
|
||||
this.serverProcess = new ServerProcess(child, connection, 'Query Server 2', this.logger);
|
||||
// Ensure the server process is disposed together with this client.
|
||||
this.track(this.serverProcess);
|
||||
connection.listen();
|
||||
progressReporter.report({ message: 'Connected to CodeQL query server v2' });
|
||||
this.nextCallback = 0;
|
||||
this.nextProgress = 0;
|
||||
this.progressCallbacks = {};
|
||||
}
|
||||
|
||||
get serverProcessPid(): number {
|
||||
return this.serverProcess!.child.pid || 0;
|
||||
}
|
||||
|
||||
async sendRequest<P, R, E, RO>(type: RequestType<WithProgressId<P>, R, E, RO>, parameter: P, token?: CancellationToken, progress?: (res: ProgressMessage) => void): Promise<R> {
|
||||
const id = this.nextProgress++;
|
||||
this.progressCallbacks[id] = progress;
|
||||
|
||||
this.updateActiveQuery(type.method, parameter);
|
||||
try {
|
||||
if (this.serverProcess === undefined) {
|
||||
throw new Error('No query server process found.');
|
||||
}
|
||||
return await this.serverProcess.connection.sendRequest(type, { body: parameter, progressId: id }, token);
|
||||
} finally {
|
||||
delete this.progressCallbacks[id];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the active query every time there is a new request to compile.
|
||||
* The active query is used to specify the side log.
|
||||
*
|
||||
* This isn't ideal because in situations where there are queries running
|
||||
* in parallel, each query's log messages are interleaved. Fixing this
|
||||
* properly will require a change in the query server.
|
||||
*/
|
||||
private updateActiveQuery(method: string, parameter: any): void {
|
||||
if (method === messages.runQuery.method) {
|
||||
this.activeQueryLogFile = findQueryLogFile(path.dirname(path.dirname((parameter as messages.RunQueryParams).outputPath)));
|
||||
}
|
||||
}
|
||||
}
|
||||
144
extensions/ql-vscode/src/query-server/run-queries.ts
Normal file
144
extensions/ql-vscode/src/query-server/run-queries.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import * as path from 'path';
|
||||
import {
|
||||
CancellationToken
|
||||
} from 'vscode';
|
||||
import * as cli from '../cli';
|
||||
import { ProgressCallback } from '../commandRunner';
|
||||
import { DatabaseItem } from '../databases';
|
||||
import {
|
||||
getOnDiskWorkspaceFolders,
|
||||
showAndLogErrorMessage,
|
||||
showAndLogWarningMessage,
|
||||
tryGetQueryMetadata
|
||||
} from '../helpers';
|
||||
import { logger } from '../logging';
|
||||
import * as messages from '../pure/new-messages';
|
||||
import * as legacyMessages from '../pure/legacy-messages';
|
||||
import { InitialQueryInfo, LocalQueryInfo } from '../query-results';
|
||||
import { QueryEvaluationInfo, QueryWithResults } from '../run-queries-shared';
|
||||
import * as qsClient from './queryserver-client';
|
||||
|
||||
|
||||
/**
|
||||
* run-queries.ts
|
||||
* --------------
|
||||
*
|
||||
* Compiling and running QL queries.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* A collection of evaluation-time information about a query,
|
||||
* including the query itself, and where we have decided to put
|
||||
* temporary files associated with it, such as the compiled query
|
||||
* output and results.
|
||||
*/
|
||||
|
||||
export async function compileAndRunQueryAgainstDatabase(
|
||||
cliServer: cli.CodeQLCliServer,
|
||||
qs: qsClient.QueryServerClient,
|
||||
dbItem: DatabaseItem,
|
||||
initialInfo: InitialQueryInfo,
|
||||
queryStorageDir: string,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
templates?: Record<string, string>,
|
||||
queryInfo?: LocalQueryInfo, // May be omitted for queries not initiated by the user. If omitted we won't create a structured log for the query.
|
||||
): Promise<QueryWithResults> {
|
||||
if (!dbItem.contents || !dbItem.contents.dbSchemeUri) {
|
||||
throw new Error(`Database ${dbItem.databaseUri} does not have a CodeQL database scheme.`);
|
||||
}
|
||||
|
||||
// Read the query metadata if possible, to use in the UI.
|
||||
const metadata = await tryGetQueryMetadata(cliServer, initialInfo.queryPath);
|
||||
|
||||
const hasMetadataFile = (await dbItem.hasMetadataFile());
|
||||
const query = new QueryEvaluationInfo(
|
||||
path.join(queryStorageDir, initialInfo.id),
|
||||
dbItem.databaseUri.fsPath,
|
||||
hasMetadataFile,
|
||||
initialInfo.quickEvalPosition,
|
||||
metadata,
|
||||
);
|
||||
|
||||
if (!dbItem.contents || dbItem.error) {
|
||||
throw new Error('Can\'t run query on invalid database.');
|
||||
}
|
||||
const target = query.quickEvalPosition ? {
|
||||
quickEval: { quickEvalPos: query.quickEvalPosition }
|
||||
} : { query: {} };
|
||||
|
||||
const diskWorkspaceFolders = getOnDiskWorkspaceFolders();
|
||||
const db = dbItem.databaseUri.fsPath;
|
||||
const logPath = queryInfo ? query.evalLogPath : undefined;
|
||||
const queryToRun: messages.RunQueryParams = {
|
||||
db,
|
||||
additionalPacks: diskWorkspaceFolders,
|
||||
externalInputs: {},
|
||||
singletonExternalInputs: templates || {},
|
||||
outputPath: query.resultsPaths.resultsPath,
|
||||
queryPath: initialInfo.queryPath,
|
||||
dilPath: query.dilPath,
|
||||
logPath,
|
||||
target,
|
||||
};
|
||||
await query.createTimestampFile();
|
||||
let result: messages.RunQueryResult | undefined;
|
||||
try {
|
||||
result = await qs.sendRequest(messages.runQuery, queryToRun, token, progress);
|
||||
if (qs.config.customLogDirectory) {
|
||||
void showAndLogWarningMessage(
|
||||
`Custom log directories are no longer supported. The "codeQL.runningQueries.customLogDirectory" setting is deprecated. Unset the setting to stop seeing this message. Query logs saved to ${query.logPath}.`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (queryInfo) {
|
||||
if (await query.hasEvalLog()) {
|
||||
await query.addQueryLogs(queryInfo, qs.cliServer, qs.logger);
|
||||
} else {
|
||||
void showAndLogWarningMessage(`Failed to write structured evaluator log to ${query.evalLogPath}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.resultType !== messages.QueryResultType.SUCCESS) {
|
||||
const message = result.message || 'Failed to run query';
|
||||
void logger.log(message);
|
||||
void showAndLogErrorMessage(message);
|
||||
}
|
||||
let message;
|
||||
switch (result.resultType) {
|
||||
case messages.QueryResultType.CANCELLATION:
|
||||
message = `cancelled after ${Math.round(result.evaluationTime / 1000)} seconds`;
|
||||
break;
|
||||
case messages.QueryResultType.OOM:
|
||||
message = 'out of memory';
|
||||
break;
|
||||
case messages.QueryResultType.SUCCESS:
|
||||
message = `finished in ${Math.round(result.evaluationTime / 1000)} seconds`;
|
||||
break;
|
||||
case messages.QueryResultType.COMPILATION_ERROR:
|
||||
message = `compilation failed: ${result.message}`;
|
||||
break;
|
||||
case messages.QueryResultType.OTHER_ERROR:
|
||||
default:
|
||||
message = result.message ? `failed: ${result.message}` : 'failed';
|
||||
break;
|
||||
}
|
||||
const successful = result.resultType === messages.QueryResultType.SUCCESS;
|
||||
return {
|
||||
query,
|
||||
result: {
|
||||
evaluationTime: result.evaluationTime,
|
||||
queryId: 0,
|
||||
resultType: successful ? legacyMessages.QueryResultType.SUCCESS : legacyMessages.QueryResultType.OTHER_ERROR,
|
||||
runId: 0,
|
||||
message
|
||||
},
|
||||
message,
|
||||
successful,
|
||||
dispose: () => {
|
||||
qs.logger.removeAdditionalLogLocation(undefined);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -45,4 +45,6 @@ export abstract class QueryRunner {
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void>
|
||||
|
||||
abstract clearPackCache(): Promise<void>
|
||||
}
|
||||
|
||||
@@ -77,12 +77,9 @@ export async function getVariantAnalysisRepo(
|
||||
export async function getVariantAnalysisRepoResult(
|
||||
credentials: Credentials,
|
||||
downloadUrl: string,
|
||||
): Promise<unknown> {
|
||||
): Promise<ArrayBuffer> {
|
||||
const octokit = await credentials.getOctokit();
|
||||
|
||||
const response: OctokitResponse<VariantAnalysisRepoTask> = await octokit.request(
|
||||
`GET ${downloadUrl}`
|
||||
);
|
||||
const response = await octokit.request(`GET ${downloadUrl}`);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,10 @@ export interface VariantAnalysis {
|
||||
actor_id: number,
|
||||
query_language: VariantAnalysisQueryLanguage,
|
||||
query_pack_url: string,
|
||||
created_at: string,
|
||||
updated_at: string,
|
||||
status: VariantAnalysisStatus,
|
||||
completed_at?: string,
|
||||
actions_workflow_run_id?: number,
|
||||
failure_reason?: VariantAnalysisFailureReason,
|
||||
scanned_repositories?: VariantAnalysisScannedRepository[],
|
||||
|
||||
@@ -22,6 +22,7 @@ import { assertNever } from '../pure/helpers-pure';
|
||||
import { QueryStatus } from '../query-status';
|
||||
import { DisposableObject } from '../pure/disposable-object';
|
||||
import { AnalysisResults } from './shared/analysis-result';
|
||||
import { VariantAnalysisManager } from './variant-analysis-manager';
|
||||
|
||||
const autoDownloadMaxSize = 300 * 1024;
|
||||
const autoDownloadMaxCount = 100;
|
||||
@@ -56,6 +57,7 @@ export class RemoteQueriesManager extends DisposableObject {
|
||||
|
||||
private readonly remoteQueriesMonitor: RemoteQueriesMonitor;
|
||||
private readonly analysesResultsManager: AnalysesResultsManager;
|
||||
private readonly variantAnalysisManager: VariantAnalysisManager;
|
||||
private readonly view: RemoteQueriesView;
|
||||
|
||||
constructor(
|
||||
@@ -63,11 +65,13 @@ export class RemoteQueriesManager extends DisposableObject {
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
private readonly storagePath: string,
|
||||
logger: Logger,
|
||||
variantAnalysisManager: VariantAnalysisManager,
|
||||
) {
|
||||
super();
|
||||
this.analysesResultsManager = new AnalysesResultsManager(ctx, cliServer, storagePath, logger);
|
||||
this.view = new RemoteQueriesView(ctx, logger, this.analysesResultsManager);
|
||||
this.remoteQueriesMonitor = new RemoteQueriesMonitor(ctx, logger);
|
||||
this.variantAnalysisManager = variantAnalysisManager;
|
||||
|
||||
this.remoteQueryAddedEventEmitter = this.push(new EventEmitter<NewQueryEvent>());
|
||||
this.remoteQueryRemovedEventEmitter = this.push(new EventEmitter<RemovedQueryEvent>());
|
||||
@@ -123,7 +127,8 @@ export class RemoteQueriesManager extends DisposableObject {
|
||||
credentials, uri || window.activeTextEditor?.document.uri,
|
||||
false,
|
||||
progress,
|
||||
token);
|
||||
token,
|
||||
this.variantAnalysisManager);
|
||||
|
||||
if (querySubmission?.query) {
|
||||
const query = querySubmission.query;
|
||||
|
||||
@@ -29,6 +29,7 @@ import { getRepositorySelection, isValidSelection, RepositorySelection } from '.
|
||||
import { parseVariantAnalysisQueryLanguage, VariantAnalysisSubmission } from './shared/variant-analysis';
|
||||
import { Repository } from './shared/repository';
|
||||
import { processVariantAnalysis } from './variant-analysis-processor';
|
||||
import { VariantAnalysisManager } from './variant-analysis-manager';
|
||||
|
||||
export interface QlPack {
|
||||
name: string;
|
||||
@@ -182,7 +183,8 @@ export async function runRemoteQuery(
|
||||
uri: Uri | undefined,
|
||||
dryRun: boolean,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
token: CancellationToken,
|
||||
variantAnalysisManager: VariantAnalysisManager
|
||||
): Promise<void | RemoteQuerySubmissionResult> {
|
||||
if (!(await cliServer.cliConstraints.supportsRemoteQueries())) {
|
||||
throw new Error(`Variant analysis is not supported by this version of CodeQL. Please upgrade to v${cli.CliVersionConstraint.CLI_VERSION_REMOTE_QUERIES
|
||||
@@ -249,6 +251,8 @@ export async function runRemoteQuery(
|
||||
throw new UserCancellationException(`Found unsupported language: ${language}`);
|
||||
}
|
||||
|
||||
const queryText = await fs.readFile(queryFile, 'utf8');
|
||||
|
||||
const variantAnalysisSubmission: VariantAnalysisSubmission = {
|
||||
startTime: queryStartTime,
|
||||
actionRepoRef: actionBranch,
|
||||
@@ -258,6 +262,7 @@ export async function runRemoteQuery(
|
||||
filePath: queryFile,
|
||||
pack: base64Pack,
|
||||
language: variantAnalysisLanguage,
|
||||
text: queryText,
|
||||
},
|
||||
databases: {
|
||||
repositories: repoSelection.repositories,
|
||||
@@ -273,6 +278,8 @@ export async function runRemoteQuery(
|
||||
|
||||
const processedVariantAnalysis = processVariantAnalysis(variantAnalysisSubmission, variantAnalysisResponse);
|
||||
|
||||
await variantAnalysisManager.onVariantAnalysisSubmitted(processedVariantAnalysis);
|
||||
|
||||
void logger.log(`Variant analysis:\n${JSON.stringify(processedVariantAnalysis, null, 2)}`);
|
||||
|
||||
void showAndLogInformationMessage(`Variant analysis ${processedVariantAnalysis.query.name} submitted for processing`);
|
||||
@@ -473,7 +480,7 @@ function getQueryName(queryMetadata: QueryMetadata | undefined, queryFilePath: s
|
||||
return queryMetadata?.name ?? path.basename(queryFilePath);
|
||||
}
|
||||
|
||||
async function getControllerRepo(credentials: Credentials): Promise<Repository> {
|
||||
export async function getControllerRepo(credentials: Credentials): Promise<Repository> {
|
||||
// Get the controller repo from the config, if it exists.
|
||||
// If it doesn't exist, prompt the user to enter it, and save that value to the config.
|
||||
let controllerRepoNwo: string | undefined;
|
||||
|
||||
@@ -7,14 +7,19 @@ export interface VariantAnalysis {
|
||||
query: {
|
||||
name: string,
|
||||
filePath: string,
|
||||
language: VariantAnalysisQueryLanguage
|
||||
language: VariantAnalysisQueryLanguage,
|
||||
text: string,
|
||||
},
|
||||
databases: {
|
||||
repositories?: string[],
|
||||
repositoryLists?: string[],
|
||||
repositoryOwners?: string[],
|
||||
},
|
||||
createdAt: string,
|
||||
updatedAt: string,
|
||||
executionStartTime: number;
|
||||
status: VariantAnalysisStatus,
|
||||
completedAt?: string,
|
||||
actionsWorkflowRunId?: number,
|
||||
failureReason?: VariantAnalysisFailureReason,
|
||||
scannedRepos?: VariantAnalysisScannedRepository[],
|
||||
@@ -82,7 +87,20 @@ export interface VariantAnalysisSkippedRepository {
|
||||
private?: boolean,
|
||||
}
|
||||
|
||||
export enum VariantAnalysisScannedRepositoryDownloadStatus {
|
||||
Pending = 'pending',
|
||||
InProgress = 'inProgress',
|
||||
Succeeded = 'succeeded',
|
||||
Failed = 'failed',
|
||||
}
|
||||
|
||||
export interface VariantAnalysisScannedRepositoryState {
|
||||
repositoryId: number;
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus;
|
||||
}
|
||||
|
||||
export interface VariantAnalysisScannedRepositoryResult {
|
||||
variantAnalysisId: number;
|
||||
repositoryId: number;
|
||||
interpretedResults?: AnalysisAlert[];
|
||||
rawResults?: AnalysisRawResults;
|
||||
@@ -100,6 +118,7 @@ export interface VariantAnalysisSubmission {
|
||||
name: string,
|
||||
filePath: string,
|
||||
language: VariantAnalysisQueryLanguage,
|
||||
text: string,
|
||||
|
||||
// Base64 encoded query pack.
|
||||
pack: string,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { TextDocumentContentProvider, Uri } from 'vscode';
|
||||
import { URLSearchParams } from 'url';
|
||||
import { showAndLogWarningMessage } from '../helpers';
|
||||
import { SHOW_QUERY_TEXT_MSG } from '../query-history';
|
||||
import { VariantAnalysisManager } from './variant-analysis-manager';
|
||||
|
||||
export const createVariantAnalysisContentProvider = (variantAnalysisManager: VariantAnalysisManager): TextDocumentContentProvider => ({
|
||||
async provideTextDocumentContent(uri: Uri): Promise<string | undefined> {
|
||||
const params = new URLSearchParams(uri.query);
|
||||
|
||||
const variantAnalysisIdString = params.get('variantAnalysisId');
|
||||
if (!variantAnalysisIdString) {
|
||||
void showAndLogWarningMessage('Unable to show query text. No variant analysis ID provided.');
|
||||
return undefined;
|
||||
}
|
||||
const variantAnalysisId = parseInt(variantAnalysisIdString);
|
||||
|
||||
const variantAnalysis = await variantAnalysisManager.getVariantAnalysis(variantAnalysisId);
|
||||
if (!variantAnalysis) {
|
||||
void showAndLogWarningMessage('Unable to show query text. No variant analysis found.');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return SHOW_QUERY_TEXT_MSG + variantAnalysis.query.text;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { QueryStatus } from '../query-status';
|
||||
import { VariantAnalysis } from './shared/variant-analysis';
|
||||
|
||||
/**
|
||||
* Information about a variant analysis.
|
||||
*/
|
||||
export interface VariantAnalysisHistoryItem {
|
||||
readonly t: 'variant-analysis';
|
||||
failureReason?: string;
|
||||
resultCount?: number;
|
||||
status: QueryStatus;
|
||||
completed: boolean;
|
||||
readonly historyItemId: string,
|
||||
variantAnalysis: VariantAnalysis;
|
||||
userSpecifiedLabel?: string;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as ghApiClient from './gh-api/gh-api-client';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import { CancellationToken, ExtensionContext } from 'vscode';
|
||||
|
||||
import * as ghApiClient from './gh-api/gh-api-client';
|
||||
import { CancellationToken, commands, EventEmitter, ExtensionContext, window } from 'vscode';
|
||||
import { DisposableObject } from '../pure/disposable-object';
|
||||
import { Logger } from '../logging';
|
||||
import { Credentials } from '../authentication';
|
||||
@@ -11,22 +11,45 @@ import {
|
||||
VariantAnalysisRepoTask,
|
||||
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository
|
||||
} from './gh-api/variant-analysis';
|
||||
import { VariantAnalysis } from './shared/variant-analysis';
|
||||
import {
|
||||
VariantAnalysis, VariantAnalysisQueryLanguage,
|
||||
VariantAnalysisScannedRepositoryDownloadStatus,
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
VariantAnalysisScannedRepositoryState
|
||||
} from './shared/variant-analysis';
|
||||
import { getErrorMessage } from '../pure/helpers-pure';
|
||||
import { VariantAnalysisView } from './variant-analysis-view';
|
||||
import { VariantAnalysisViewManager } from './variant-analysis-view-manager';
|
||||
import { VariantAnalysisResultsManager } from './variant-analysis-results-manager';
|
||||
import { CodeQLCliServer } from '../cli';
|
||||
import { getControllerRepo } from './run-remote-query';
|
||||
import { processUpdatedVariantAnalysis } from './variant-analysis-processor';
|
||||
import PQueue from 'p-queue';
|
||||
import { createTimestampFile } from '../helpers';
|
||||
|
||||
export class VariantAnalysisManager extends DisposableObject implements VariantAnalysisViewManager<VariantAnalysisView> {
|
||||
private readonly _onVariantAnalysisAdded = this.push(new EventEmitter<VariantAnalysis>());
|
||||
public readonly onVariantAnalysisAdded = this._onVariantAnalysisAdded.event;
|
||||
|
||||
private readonly variantAnalysisMonitor: VariantAnalysisMonitor;
|
||||
private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager;
|
||||
private readonly variantAnalyses = new Map<number, VariantAnalysis>();
|
||||
private readonly views = new Map<number, VariantAnalysisView>();
|
||||
private static readonly maxConcurrentDownloads = 3;
|
||||
private readonly queue = new PQueue({ concurrency: VariantAnalysisManager.maxConcurrentDownloads });
|
||||
|
||||
constructor(
|
||||
private readonly ctx: ExtensionContext,
|
||||
cliServer: CodeQLCliServer,
|
||||
private readonly storagePath: string,
|
||||
logger: Logger,
|
||||
) {
|
||||
super();
|
||||
this.variantAnalysisMonitor = this.push(new VariantAnalysisMonitor(ctx, logger));
|
||||
this.variantAnalysisMonitor = this.push(new VariantAnalysisMonitor(ctx));
|
||||
this.variantAnalysisMonitor.onVariantAnalysisChange(this.onVariantAnalysisUpdated.bind(this));
|
||||
|
||||
this.variantAnalysisResultsManager = this.push(new VariantAnalysisResultsManager(cliServer, logger));
|
||||
this.variantAnalysisResultsManager.onResultLoaded(this.onRepoResultLoaded.bind(this));
|
||||
}
|
||||
|
||||
public async showView(variantAnalysisId: number): Promise<void> {
|
||||
@@ -52,17 +75,45 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
|
||||
this.views.delete(view.variantAnalysisId);
|
||||
}
|
||||
|
||||
public getView(variantAnalysisId: number): VariantAnalysisView | undefined {
|
||||
return this.views.get(variantAnalysisId);
|
||||
}
|
||||
|
||||
public async getVariantAnalysis(variantAnalysisId: number): Promise<VariantAnalysis | undefined> {
|
||||
return this.variantAnalyses.get(variantAnalysisId);
|
||||
}
|
||||
|
||||
public async loadResults(variantAnalysisId: number, repositoryFullName: string): Promise<void> {
|
||||
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
|
||||
if (!variantAnalysis) {
|
||||
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
|
||||
}
|
||||
|
||||
await this.variantAnalysisResultsManager.loadResults(variantAnalysisId, this.getVariantAnalysisStorageLocation(variantAnalysisId), repositoryFullName);
|
||||
}
|
||||
|
||||
private async onVariantAnalysisUpdated(variantAnalysis: VariantAnalysis | undefined): Promise<void> {
|
||||
if (!variantAnalysis) {
|
||||
return;
|
||||
}
|
||||
|
||||
const view = this.views.get(variantAnalysis.id);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
this.variantAnalyses.set(variantAnalysis.id, variantAnalysis);
|
||||
|
||||
await view.updateView(variantAnalysis);
|
||||
await this.getView(variantAnalysis.id)?.updateView(variantAnalysis);
|
||||
}
|
||||
|
||||
public async onVariantAnalysisSubmitted(variantAnalysis: VariantAnalysis): Promise<void> {
|
||||
await this.prepareStorageDirectory(variantAnalysis.id);
|
||||
|
||||
this._onVariantAnalysisAdded.fire(variantAnalysis);
|
||||
}
|
||||
|
||||
private async onRepoResultLoaded(repositoryResult: VariantAnalysisScannedRepositoryResult): Promise<void> {
|
||||
await this.getView(repositoryResult.variantAnalysisId)?.sendRepositoryResults([repositoryResult]);
|
||||
}
|
||||
|
||||
private async onRepoStateUpdated(variantAnalysisId: number, repoState: VariantAnalysisScannedRepositoryState): Promise<void> {
|
||||
await this.getView(variantAnalysisId)?.updateRepoState(repoState);
|
||||
}
|
||||
|
||||
public async monitorVariantAnalysis(
|
||||
@@ -77,11 +128,19 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
|
||||
variantAnalysisSummary: VariantAnalysisApiResponse,
|
||||
cancellationToken: CancellationToken
|
||||
): Promise<void> {
|
||||
const repoState = {
|
||||
repositoryId: scannedRepo.repository.id,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Pending,
|
||||
};
|
||||
|
||||
await this.onRepoStateUpdated(variantAnalysisSummary.id, repoState);
|
||||
|
||||
const credentials = await Credentials.initialize(this.ctx);
|
||||
if (!credentials) { throw Error('Error authenticating with GitHub'); }
|
||||
|
||||
if (cancellationToken && cancellationToken.isCancellationRequested) {
|
||||
repoState.downloadStatus = VariantAnalysisScannedRepositoryDownloadStatus.Failed;
|
||||
await this.onRepoStateUpdated(variantAnalysisSummary.id, repoState);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,29 +152,83 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
|
||||
variantAnalysisSummary.id,
|
||||
scannedRepo.repository.id
|
||||
);
|
||||
} catch (e) {
|
||||
repoState.downloadStatus = VariantAnalysisScannedRepositoryDownloadStatus.Failed;
|
||||
await this.onRepoStateUpdated(variantAnalysisSummary.id, repoState);
|
||||
throw new Error(`Could not download the results for variant analysis with id: ${variantAnalysisSummary.id}. Error: ${getErrorMessage(e)}`);
|
||||
}
|
||||
catch (e) { throw new Error(`Could not download the results for variant analysis with id: ${variantAnalysisSummary.id}. Error: ${getErrorMessage(e)}`); }
|
||||
|
||||
if (repoTask.artifact_url) {
|
||||
const resultDirectory = path.join(
|
||||
this.ctx.globalStorageUri.fsPath,
|
||||
'variant-analyses',
|
||||
`${variantAnalysisSummary.id}`,
|
||||
scannedRepo.repository.full_name
|
||||
);
|
||||
repoState.downloadStatus = VariantAnalysisScannedRepositoryDownloadStatus.InProgress;
|
||||
await this.onRepoStateUpdated(variantAnalysisSummary.id, repoState);
|
||||
|
||||
const storagePath = path.join(
|
||||
resultDirectory,
|
||||
scannedRepo.repository.full_name
|
||||
);
|
||||
|
||||
const result = await ghApiClient.getVariantAnalysisRepoResult(
|
||||
credentials,
|
||||
repoTask.artifact_url
|
||||
);
|
||||
|
||||
fs.mkdirSync(resultDirectory, { recursive: true });
|
||||
await fs.writeFile(storagePath, JSON.stringify(result, null, 2), 'utf8');
|
||||
await this.variantAnalysisResultsManager.download(credentials, variantAnalysisSummary.id, repoTask, this.getVariantAnalysisStorageLocation(variantAnalysisSummary.id));
|
||||
}
|
||||
|
||||
repoState.downloadStatus = VariantAnalysisScannedRepositoryDownloadStatus.Succeeded;
|
||||
await this.onRepoStateUpdated(variantAnalysisSummary.id, repoState);
|
||||
}
|
||||
|
||||
public async enqueueDownload(
|
||||
scannedRepo: ApiVariantAnalysisScannedRepository,
|
||||
variantAnalysisSummary: VariantAnalysisApiResponse,
|
||||
token: CancellationToken
|
||||
): Promise<void> {
|
||||
await this.queue.add(() => this.autoDownloadVariantAnalysisResult(scannedRepo, variantAnalysisSummary, token));
|
||||
}
|
||||
|
||||
public downloadsQueueSize(): number {
|
||||
return this.queue.pending;
|
||||
}
|
||||
|
||||
public getVariantAnalysisStorageLocation(variantAnalysisId: number): string {
|
||||
return path.join(
|
||||
this.storagePath,
|
||||
`${variantAnalysisId}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a directory for storing results for a variant analysis.
|
||||
* This directory contains a timestamp file, which will be
|
||||
* used by the query history manager to determine when the directory
|
||||
* should be deleted.
|
||||
*/
|
||||
private async prepareStorageDirectory(variantAnalysisId: number): Promise<void> {
|
||||
await createTimestampFile(this.getVariantAnalysisStorageLocation(variantAnalysisId));
|
||||
}
|
||||
|
||||
public async promptOpenVariantAnalysis() {
|
||||
const credentials = await Credentials.initialize(this.ctx);
|
||||
if (!credentials) { throw Error('Error authenticating with GitHub'); }
|
||||
|
||||
const controllerRepo = await getControllerRepo(credentials);
|
||||
|
||||
const variantAnalysisIdString = await window.showInputBox({
|
||||
title: 'Enter the variant analysis ID',
|
||||
});
|
||||
if (!variantAnalysisIdString) {
|
||||
return;
|
||||
}
|
||||
const variantAnalysisId = parseInt(variantAnalysisIdString, 10);
|
||||
|
||||
const variantAnalysisResponse = await ghApiClient.getVariantAnalysis(credentials, controllerRepo.id, variantAnalysisId);
|
||||
|
||||
const processedVariantAnalysis = processUpdatedVariantAnalysis({
|
||||
// We don't really know these values, so just fill in some placeholder values
|
||||
query: {
|
||||
name: `Variant analysis ${variantAnalysisId}`,
|
||||
filePath: `variant_analysis_${variantAnalysisId}.ql`,
|
||||
language: variantAnalysisResponse.query_language as VariantAnalysisQueryLanguage,
|
||||
text: '',
|
||||
},
|
||||
databases: {},
|
||||
executionStartTime: 0,
|
||||
}, variantAnalysisResponse);
|
||||
|
||||
void commands.executeCommand('codeQL.openVariantAnalysisView', processedVariantAnalysis.id);
|
||||
void commands.executeCommand('codeQL.monitorVariantAnalysis', processedVariantAnalysis);
|
||||
|
||||
this._onVariantAnalysisAdded.fire(processedVariantAnalysis);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ExtensionContext, CancellationToken, commands, EventEmitter } from 'vscode';
|
||||
import { Credentials } from '../authentication';
|
||||
import { Logger } from '../logging';
|
||||
import * as ghApiClient from './gh-api/gh-api-client';
|
||||
|
||||
import { VariantAnalysis, VariantAnalysisStatus } from './shared/variant-analysis';
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisApiResponse
|
||||
VariantAnalysis as VariantAnalysisApiResponse,
|
||||
VariantAnalysisScannedRepository
|
||||
} from './gh-api/variant-analysis';
|
||||
import { VariantAnalysisMonitorResult } from './shared/variant-analysis-monitor-result';
|
||||
import { processFailureReason, processUpdatedVariantAnalysis } from './variant-analysis-processor';
|
||||
@@ -22,7 +22,6 @@ export class VariantAnalysisMonitor extends DisposableObject {
|
||||
|
||||
constructor(
|
||||
private readonly extensionContext: ExtensionContext,
|
||||
private readonly logger: Logger
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -41,6 +40,8 @@ export class VariantAnalysisMonitor extends DisposableObject {
|
||||
let attemptCount = 0;
|
||||
const scannedReposDownloaded: number[] = [];
|
||||
|
||||
this._onVariantAnalysisChange.fire(variantAnalysis);
|
||||
|
||||
while (attemptCount <= VariantAnalysisMonitor.maxAttemptCount) {
|
||||
await this.sleep(VariantAnalysisMonitor.sleepTime);
|
||||
|
||||
@@ -57,6 +58,9 @@ export class VariantAnalysisMonitor extends DisposableObject {
|
||||
if (variantAnalysisSummary.failure_reason) {
|
||||
variantAnalysis.status = VariantAnalysisStatus.Failed;
|
||||
variantAnalysis.failureReason = processFailureReason(variantAnalysisSummary.failure_reason);
|
||||
|
||||
this._onVariantAnalysisChange.fire(variantAnalysis);
|
||||
|
||||
return {
|
||||
status: 'Failed',
|
||||
error: `Variant Analysis has failed: ${variantAnalysisSummary.failure_reason}`,
|
||||
@@ -68,16 +72,8 @@ export class VariantAnalysisMonitor extends DisposableObject {
|
||||
|
||||
this._onVariantAnalysisChange.fire(variantAnalysis);
|
||||
|
||||
void this.logger.log('****** Retrieved variant analysis' + JSON.stringify(variantAnalysisSummary));
|
||||
|
||||
if (variantAnalysisSummary.scanned_repositories) {
|
||||
variantAnalysisSummary.scanned_repositories.forEach(scannedRepo => {
|
||||
if (!scannedReposDownloaded.includes(scannedRepo.repository.id) && scannedRepo.analysis_status === 'succeeded') {
|
||||
void commands.executeCommand('codeQL.autoDownloadVariantAnalysisResult', scannedRepo, variantAnalysisSummary);
|
||||
scannedReposDownloaded.push(scannedRepo.repository.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
const downloadedRepos = this.downloadVariantAnalysisResults(variantAnalysisSummary, scannedReposDownloaded);
|
||||
scannedReposDownloaded.push(...downloadedRepos);
|
||||
|
||||
if (variantAnalysisSummary.status === 'completed') {
|
||||
break;
|
||||
@@ -89,6 +85,46 @@ export class VariantAnalysisMonitor extends DisposableObject {
|
||||
return { status: 'CompletedSuccessfully', scannedReposDownloaded: scannedReposDownloaded };
|
||||
}
|
||||
|
||||
private scheduleForDownload(
|
||||
scannedRepo: VariantAnalysisScannedRepository,
|
||||
variantAnalysisSummary: VariantAnalysisApiResponse
|
||||
) {
|
||||
void commands.executeCommand('codeQL.autoDownloadVariantAnalysisResult', scannedRepo, variantAnalysisSummary);
|
||||
}
|
||||
|
||||
private shouldDownload(
|
||||
scannedRepo: VariantAnalysisScannedRepository,
|
||||
alreadyDownloaded: number[]
|
||||
): boolean {
|
||||
return !alreadyDownloaded.includes(scannedRepo.repository.id) && scannedRepo.analysis_status === 'succeeded';
|
||||
}
|
||||
|
||||
private getReposToDownload(
|
||||
variantAnalysisSummary: VariantAnalysisApiResponse,
|
||||
alreadyDownloaded: number[]
|
||||
): VariantAnalysisScannedRepository[] {
|
||||
if (variantAnalysisSummary.scanned_repositories) {
|
||||
return variantAnalysisSummary.scanned_repositories.filter(scannedRepo => this.shouldDownload(scannedRepo, alreadyDownloaded));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private downloadVariantAnalysisResults(
|
||||
variantAnalysisSummary: VariantAnalysisApiResponse,
|
||||
scannedReposDownloaded: number[]
|
||||
): number[] {
|
||||
const repoResultsToDownload = this.getReposToDownload(variantAnalysisSummary, scannedReposDownloaded);
|
||||
const downloadedRepos: number[] = [];
|
||||
|
||||
repoResultsToDownload.forEach(scannedRepo => {
|
||||
downloadedRepos.push(scannedRepo.repository.id);
|
||||
this.scheduleForDownload(scannedRepo, variantAnalysisSummary);
|
||||
});
|
||||
|
||||
return downloadedRepos;
|
||||
}
|
||||
|
||||
private async sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -27,14 +27,16 @@ export function processVariantAnalysis(
|
||||
query: {
|
||||
name: submission.query.name,
|
||||
filePath: submission.query.filePath,
|
||||
language: submission.query.language
|
||||
language: submission.query.language,
|
||||
text: submission.query.text,
|
||||
},
|
||||
databases: submission.databases,
|
||||
executionStartTime: submission.startTime
|
||||
}, response);
|
||||
}
|
||||
|
||||
export function processUpdatedVariantAnalysis(
|
||||
previousVariantAnalysis: Pick<VariantAnalysis, 'query' | 'databases'>,
|
||||
previousVariantAnalysis: Pick<VariantAnalysis, 'query' | 'databases' | 'executionStartTime'>,
|
||||
response: ApiVariantAnalysis
|
||||
): VariantAnalysis {
|
||||
let scannedRepos: VariantAnalysisScannedRepository[] = [];
|
||||
@@ -51,13 +53,13 @@ export function processUpdatedVariantAnalysis(
|
||||
const variantAnalysis: VariantAnalysis = {
|
||||
id: response.id,
|
||||
controllerRepoId: response.controller_repo.id,
|
||||
query: {
|
||||
name: previousVariantAnalysis.query.name,
|
||||
filePath: previousVariantAnalysis.query.filePath,
|
||||
language: previousVariantAnalysis.query.language
|
||||
},
|
||||
query: previousVariantAnalysis.query,
|
||||
databases: previousVariantAnalysis.databases,
|
||||
executionStartTime: previousVariantAnalysis.executionStartTime,
|
||||
createdAt: response.created_at,
|
||||
updatedAt: response.updated_at,
|
||||
status: processApiStatus(response.status),
|
||||
completedAt: response.completed_at,
|
||||
actionsWorkflowRunId: response.actions_workflow_run_id,
|
||||
scannedRepos: scannedRepos,
|
||||
skippedRepos: skippedRepos
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { Credentials } from '../authentication';
|
||||
import { Logger } from '../logging';
|
||||
import { AnalysisAlert, AnalysisRawResults } from './shared/analysis-result';
|
||||
import { sarifParser } from '../sarif-parser';
|
||||
import { extractAnalysisAlerts } from './sarif-processing';
|
||||
import { CodeQLCliServer } from '../cli';
|
||||
import { extractRawResults } from './bqrs-processing';
|
||||
import { VariantAnalysisScannedRepositoryResult } from './shared/variant-analysis';
|
||||
import { DisposableObject, DisposeHandler } from '../pure/disposable-object';
|
||||
import { VariantAnalysisRepoTask } from './gh-api/variant-analysis';
|
||||
import * as ghApiClient from './gh-api/gh-api-client';
|
||||
import { EventEmitter } from 'vscode';
|
||||
import { unzipFile } from '../pure/zip';
|
||||
|
||||
type CacheKey = `${number}/${string}`;
|
||||
|
||||
const createCacheKey = (variantAnalysisId: number, repositoryFullName: string): CacheKey => `${variantAnalysisId}/${repositoryFullName}`;
|
||||
|
||||
export type ResultDownloadedEvent = {
|
||||
variantAnalysisId: number;
|
||||
repoTask: VariantAnalysisRepoTask;
|
||||
}
|
||||
|
||||
export class VariantAnalysisResultsManager extends DisposableObject {
|
||||
private static readonly REPO_TASK_FILENAME = 'repo_task.json';
|
||||
private static readonly RESULTS_DIRECTORY = 'results';
|
||||
|
||||
private readonly cachedResults: Map<CacheKey, VariantAnalysisScannedRepositoryResult>;
|
||||
|
||||
private readonly _onResultDownloaded = this.push(new EventEmitter<ResultDownloadedEvent>());
|
||||
readonly onResultDownloaded = this._onResultDownloaded.event;
|
||||
|
||||
private readonly _onResultLoaded = this.push(new EventEmitter<VariantAnalysisScannedRepositoryResult>());
|
||||
readonly onResultLoaded = this._onResultLoaded.event;
|
||||
|
||||
constructor(
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
private readonly logger: Logger,
|
||||
) {
|
||||
super();
|
||||
this.cachedResults = new Map();
|
||||
}
|
||||
|
||||
public async download(
|
||||
credentials: Credentials,
|
||||
variantAnalysisId: number,
|
||||
repoTask: VariantAnalysisRepoTask,
|
||||
variantAnalysisStoragePath: string,
|
||||
): Promise<void> {
|
||||
if (!repoTask.artifact_url) {
|
||||
throw new Error('Missing artifact URL');
|
||||
}
|
||||
|
||||
const resultDirectory = this.getRepoStorageDirectory(variantAnalysisStoragePath, repoTask.repository.full_name);
|
||||
|
||||
const result = await ghApiClient.getVariantAnalysisRepoResult(
|
||||
credentials,
|
||||
repoTask.artifact_url
|
||||
);
|
||||
|
||||
if (!(await fs.pathExists(resultDirectory))) {
|
||||
await fs.mkdir(resultDirectory, { recursive: true });
|
||||
}
|
||||
|
||||
await fs.outputJson(path.join(resultDirectory, VariantAnalysisResultsManager.REPO_TASK_FILENAME), repoTask);
|
||||
|
||||
const zipFilePath = path.join(resultDirectory, 'results.zip');
|
||||
const unzippedFilesDirectory = path.join(resultDirectory, VariantAnalysisResultsManager.RESULTS_DIRECTORY);
|
||||
|
||||
fs.writeFileSync(zipFilePath, Buffer.from(result));
|
||||
await unzipFile(zipFilePath, unzippedFilesDirectory);
|
||||
|
||||
this._onResultDownloaded.fire({
|
||||
variantAnalysisId,
|
||||
repoTask,
|
||||
});
|
||||
}
|
||||
|
||||
public async loadResults(
|
||||
variantAnalysisId: number,
|
||||
variantAnalysisStoragePath: string,
|
||||
repositoryFullName: string
|
||||
): Promise<VariantAnalysisScannedRepositoryResult> {
|
||||
const result = this.cachedResults.get(createCacheKey(variantAnalysisId, repositoryFullName));
|
||||
|
||||
return result ?? await this.loadResultsIntoMemory(variantAnalysisId, variantAnalysisStoragePath, repositoryFullName);
|
||||
}
|
||||
|
||||
private async loadResultsIntoMemory(
|
||||
variantAnalysisId: number,
|
||||
variantAnalysisStoragePath: string,
|
||||
repositoryFullName: string,
|
||||
): Promise<VariantAnalysisScannedRepositoryResult> {
|
||||
const result = await this.loadResultsFromStorage(variantAnalysisId, variantAnalysisStoragePath, repositoryFullName);
|
||||
this.cachedResults.set(createCacheKey(variantAnalysisId, repositoryFullName), result);
|
||||
this._onResultLoaded.fire(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async loadResultsFromStorage(
|
||||
variantAnalysisId: number,
|
||||
variantAnalysisStoragePath: string,
|
||||
repositoryFullName: string,
|
||||
): Promise<VariantAnalysisScannedRepositoryResult> {
|
||||
if (!(await this.isVariantAnalysisRepoDownloaded(variantAnalysisStoragePath, repositoryFullName))) {
|
||||
throw new Error('Variant analysis results not downloaded');
|
||||
}
|
||||
|
||||
const storageDirectory = this.getRepoStorageDirectory(variantAnalysisStoragePath, repositoryFullName);
|
||||
|
||||
const repoTask: VariantAnalysisRepoTask = await fs.readJson(path.join(storageDirectory, VariantAnalysisResultsManager.REPO_TASK_FILENAME));
|
||||
|
||||
if (!repoTask.database_commit_sha || !repoTask.source_location_prefix) {
|
||||
throw new Error('Missing database commit SHA');
|
||||
}
|
||||
|
||||
const fileLinkPrefix = this.createGitHubDotcomFileLinkPrefix(repoTask.repository.full_name, repoTask.database_commit_sha);
|
||||
|
||||
const resultsDirectory = path.join(storageDirectory, VariantAnalysisResultsManager.RESULTS_DIRECTORY);
|
||||
const sarifPath = path.join(resultsDirectory, 'results.sarif');
|
||||
const bqrsPath = path.join(resultsDirectory, 'results.bqrs');
|
||||
if (await fs.pathExists(sarifPath)) {
|
||||
const interpretedResults = await this.readSarifResults(sarifPath, fileLinkPrefix);
|
||||
|
||||
return {
|
||||
variantAnalysisId,
|
||||
repositoryId: repoTask.repository.id,
|
||||
interpretedResults,
|
||||
};
|
||||
}
|
||||
|
||||
if (await fs.pathExists(bqrsPath)) {
|
||||
const rawResults = await this.readBqrsResults(bqrsPath, fileLinkPrefix, repoTask.source_location_prefix);
|
||||
|
||||
return {
|
||||
variantAnalysisId,
|
||||
repositoryId: repoTask.repository.id,
|
||||
rawResults,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Missing results file');
|
||||
}
|
||||
|
||||
private async isVariantAnalysisRepoDownloaded(
|
||||
variantAnalysisStoragePath: string,
|
||||
repositoryFullName: string,
|
||||
): Promise<boolean> {
|
||||
return await fs.pathExists(this.getRepoStorageDirectory(variantAnalysisStoragePath, repositoryFullName));
|
||||
}
|
||||
|
||||
private async readBqrsResults(filePath: string, fileLinkPrefix: string, sourceLocationPrefix: string): Promise<AnalysisRawResults> {
|
||||
return await extractRawResults(this.cliServer, this.logger, filePath, fileLinkPrefix, sourceLocationPrefix);
|
||||
}
|
||||
|
||||
private async readSarifResults(filePath: string, fileLinkPrefix: string): Promise<AnalysisAlert[]> {
|
||||
const sarifLog = await sarifParser(filePath);
|
||||
|
||||
const processedSarif = extractAnalysisAlerts(sarifLog, fileLinkPrefix);
|
||||
if (processedSarif.errors.length) {
|
||||
void this.logger.log(`Error processing SARIF file: ${os.EOL}${processedSarif.errors.join(os.EOL)}`);
|
||||
}
|
||||
|
||||
return processedSarif.alerts;
|
||||
}
|
||||
|
||||
public getRepoStorageDirectory(variantAnalysisStoragePath: string, fullName: string): string {
|
||||
return path.join(
|
||||
variantAnalysisStoragePath,
|
||||
fullName
|
||||
);
|
||||
}
|
||||
|
||||
private createGitHubDotcomFileLinkPrefix(fullName: string, sha: string): string {
|
||||
return `https://github.com/${fullName}/blob/${sha}`;
|
||||
}
|
||||
|
||||
public dispose(disposeHandler?: DisposeHandler) {
|
||||
super.dispose(disposeHandler);
|
||||
|
||||
this.cachedResults.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { VariantAnalysis } from './shared/variant-analysis';
|
||||
|
||||
export interface VariantAnalysisViewInterface {
|
||||
variantAnalysisId: number;
|
||||
openView(): Promise<void>;
|
||||
@@ -6,4 +8,6 @@ export interface VariantAnalysisViewInterface {
|
||||
export interface VariantAnalysisViewManager<T extends VariantAnalysisViewInterface> {
|
||||
registerView(view: T): void;
|
||||
unregisterView(view: T): void;
|
||||
|
||||
getVariantAnalysis(variantAnalysisId: number): Promise<VariantAnalysis | undefined>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { ExtensionContext, WebviewPanel, WebviewPanelSerializer } from 'vscode';
|
||||
import { VariantAnalysisView } from './variant-analysis-view';
|
||||
import { VariantAnalysisState } from '../pure/interface-types';
|
||||
import { VariantAnalysisViewManager } from './variant-analysis-view-manager';
|
||||
|
||||
export class VariantAnalysisViewSerializer implements WebviewPanelSerializer {
|
||||
private resolvePromises: ((value: VariantAnalysisViewManager<VariantAnalysisView>) => void)[] = [];
|
||||
|
||||
private manager?: VariantAnalysisViewManager<VariantAnalysisView>;
|
||||
|
||||
public constructor(
|
||||
private readonly ctx: ExtensionContext,
|
||||
) { }
|
||||
|
||||
onExtensionLoaded(manager: VariantAnalysisViewManager<VariantAnalysisView>): void {
|
||||
this.manager = manager;
|
||||
|
||||
this.resolvePromises.forEach((resolve) => resolve(manager));
|
||||
this.resolvePromises = [];
|
||||
}
|
||||
|
||||
async deserializeWebviewPanel(webviewPanel: WebviewPanel, state: unknown): Promise<void> {
|
||||
if (!state || typeof state !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!('variantAnalysisId' in state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const variantAnalysisState: VariantAnalysisState = state as VariantAnalysisState;
|
||||
|
||||
const manager = await this.waitForExtensionFullyLoaded();
|
||||
|
||||
const view = new VariantAnalysisView(this.ctx, variantAnalysisState.variantAnalysisId, manager);
|
||||
await view.restoreView(webviewPanel);
|
||||
}
|
||||
|
||||
private waitForExtensionFullyLoaded(): Promise<VariantAnalysisViewManager<VariantAnalysisView>> {
|
||||
if (this.manager) {
|
||||
return Promise.resolve(this.manager);
|
||||
}
|
||||
|
||||
return new Promise<VariantAnalysisViewManager<VariantAnalysisView>>((resolve) => {
|
||||
this.resolvePromises.push(resolve);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
import { ExtensionContext, ViewColumn } from 'vscode';
|
||||
import { commands, ExtensionContext, Uri, ViewColumn, window as Window, workspace } from 'vscode';
|
||||
import { URLSearchParams } from 'url';
|
||||
import { AbstractWebview, WebviewPanelConfig } from '../abstract-webview';
|
||||
import { WebviewMessage } from '../interface-utils';
|
||||
import { logger } from '../logging';
|
||||
import { VariantAnalysisViewInterface, VariantAnalysisViewManager } from './variant-analysis-view-manager';
|
||||
import { VariantAnalysis } from './shared/variant-analysis';
|
||||
import { FromVariantAnalysisMessage, ToVariantAnalysisMessage } from '../pure/interface-types';
|
||||
import { assertNever } from '../pure/helpers-pure';
|
||||
import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
VariantAnalysisScannedRepositoryState,
|
||||
} from './shared/variant-analysis';
|
||||
import { VariantAnalysisViewInterface, VariantAnalysisViewManager } from './variant-analysis-view-manager';
|
||||
import { showAndLogWarningMessage } from '../helpers';
|
||||
|
||||
export class VariantAnalysisView extends AbstractWebview<ToVariantAnalysisMessage, FromVariantAnalysisMessage> implements VariantAnalysisViewInterface {
|
||||
public static readonly viewType = 'codeQL.variantAnalysis';
|
||||
|
||||
public constructor(
|
||||
ctx: ExtensionContext,
|
||||
public readonly variantAnalysisId: number,
|
||||
@@ -19,6 +27,8 @@ export class VariantAnalysisView extends AbstractWebview<ToVariantAnalysisMessag
|
||||
|
||||
public async openView() {
|
||||
this.getPanel().reveal(undefined, true);
|
||||
|
||||
await this.waitForPanelLoaded();
|
||||
}
|
||||
|
||||
public async updateView(variantAnalysis: VariantAnalysis): Promise<void> {
|
||||
@@ -32,13 +42,35 @@ export class VariantAnalysisView extends AbstractWebview<ToVariantAnalysisMessag
|
||||
});
|
||||
}
|
||||
|
||||
public async updateRepoState(repoState: VariantAnalysisScannedRepositoryState): Promise<void> {
|
||||
if (!this.isShowingPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.postMessage({
|
||||
t: 'setRepoStates',
|
||||
repoStates: [repoState],
|
||||
});
|
||||
}
|
||||
|
||||
public async sendRepositoryResults(repositoryResult: VariantAnalysisScannedRepositoryResult[]): Promise<void> {
|
||||
if (!this.isShowingPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.postMessage({
|
||||
t: 'setRepoResults',
|
||||
repoResults: repositoryResult,
|
||||
});
|
||||
}
|
||||
|
||||
protected getPanelConfig(): WebviewPanelConfig {
|
||||
return {
|
||||
viewId: 'variantAnalysisView',
|
||||
viewId: VariantAnalysisView.viewType,
|
||||
title: `CodeQL Query Results for ${this.variantAnalysisId}`,
|
||||
viewColumn: ViewColumn.Active,
|
||||
preserveFocus: true,
|
||||
view: 'variant-analysis'
|
||||
view: 'variant-analysis',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,7 +78,85 @@ export class VariantAnalysisView extends AbstractWebview<ToVariantAnalysisMessag
|
||||
this.manager.unregisterView(this);
|
||||
}
|
||||
|
||||
protected async onMessage(msg: WebviewMessage): Promise<void> {
|
||||
void logger.log('Received message on variant analysis view: ' + msg.t);
|
||||
protected async onMessage(msg: FromVariantAnalysisMessage): Promise<void> {
|
||||
switch (msg.t) {
|
||||
case 'viewLoaded':
|
||||
await this.onWebViewLoaded();
|
||||
|
||||
break;
|
||||
case 'stopVariantAnalysis':
|
||||
void logger.log(`Stop variant analysis: ${msg.variantAnalysisId}`);
|
||||
break;
|
||||
case 'requestRepositoryResults':
|
||||
void commands.executeCommand('codeQL.loadVariantAnalysisRepoResults', this.variantAnalysisId, msg.repositoryFullName);
|
||||
break;
|
||||
case 'openQueryFile':
|
||||
await this.openQueryFile();
|
||||
break;
|
||||
case 'openQueryText':
|
||||
await this.openQueryText();
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onWebViewLoaded() {
|
||||
super.onWebViewLoaded();
|
||||
|
||||
void logger.log('Variant analysis view loaded');
|
||||
|
||||
const variantAnalysis = await this.manager.getVariantAnalysis(this.variantAnalysisId);
|
||||
|
||||
if (!variantAnalysis) {
|
||||
void showAndLogWarningMessage('Unable to load variant analysis');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.postMessage({
|
||||
t: 'setVariantAnalysis',
|
||||
variantAnalysis,
|
||||
});
|
||||
}
|
||||
|
||||
private async openQueryFile(): Promise<void> {
|
||||
const variantAnalysis = await this.manager.getVariantAnalysis(this.variantAnalysisId);
|
||||
|
||||
if (!variantAnalysis) {
|
||||
void showAndLogWarningMessage('Could not open variant analysis query file');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const textDocument = await workspace.openTextDocument(variantAnalysis.query.filePath);
|
||||
await Window.showTextDocument(textDocument, ViewColumn.One);
|
||||
} catch (error) {
|
||||
void showAndLogWarningMessage(`Could not open file: ${variantAnalysis.query.filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async openQueryText(): Promise<void> {
|
||||
const variantAnalysis = await this.manager.getVariantAnalysis(this.variantAnalysisId);
|
||||
if (!variantAnalysis) {
|
||||
void showAndLogWarningMessage('Could not open variant analysis query text. Variant analysis not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = variantAnalysis.query.filePath;
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
variantAnalysisId: variantAnalysis.id.toString(),
|
||||
});
|
||||
const uri = Uri.from({
|
||||
scheme: 'codeql-variant-analysis',
|
||||
path: filename,
|
||||
query: params.toString(),
|
||||
});
|
||||
const doc = await workspace.openTextDocument(uri);
|
||||
await Window.showTextDocument(doc, { preview: false });
|
||||
} catch (error) {
|
||||
void showAndLogWarningMessage('Could not open variant analysis query text. Failed to open text document.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,11 +192,19 @@ export class QueryEvaluationInfo {
|
||||
if (await this.hasDil()) {
|
||||
return this.dilPath;
|
||||
}
|
||||
const compiledQuery = path.join(this.querySaveDir, 'compiledQuery.qlo');
|
||||
const compiledQuery = this.compileQueryPath;
|
||||
if (!(await fs.pathExists(compiledQuery))) {
|
||||
throw new Error(
|
||||
`Cannot create DIL because compiled query is missing. ${compiledQuery}`
|
||||
);
|
||||
if (await cliServer.cliConstraints.supportsNewQueryServer()) {
|
||||
// This could be from the new query server
|
||||
// in which case we expect the qlo to be missing so we should ignore it
|
||||
throw new Error(
|
||||
`DIL was not found. Expected location: '${this.dilPath}'`
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Cannot create DIL because compiled query is missing. ${compiledQuery}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await cliServer.generateDil(compiledQuery, this.dilPath);
|
||||
@@ -210,6 +218,9 @@ export class QueryEvaluationInfo {
|
||||
return fs.pathExists(this.evalLogPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the structured evaluator log to the query evaluation info.
|
||||
*/
|
||||
async addQueryLogs(queryInfo: LocalQueryInfo, cliServer: CodeQLCliServer, logger: Logger) {
|
||||
queryInfo.evalLogLocation = this.evalLogPath;
|
||||
queryInfo.evalLogSummaryLocation = await this.generateHumanReadableLogSummary(cliServer);
|
||||
@@ -362,9 +373,9 @@ export interface QueryWithResults {
|
||||
readonly query: QueryEvaluationInfo;
|
||||
readonly logFileLocation?: string;
|
||||
readonly dispose: () => void;
|
||||
readonly sucessful?: boolean;
|
||||
readonly successful?: boolean;
|
||||
readonly message?: string;
|
||||
readonly result?: legacyMessages.EvaluationResult
|
||||
readonly result: legacyMessages.EvaluationResult;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,11 @@ import bodyImage from './images/update-css-variables-body.png';
|
||||
|
||||
Welcome to the Storybook for **CodeQL for Visual Studio Code**! This Storybook contains stories for components and pages in the extension.
|
||||
|
||||
### Switching themes
|
||||
|
||||
To switch between VSCode Dark+ and Light+ themes, use the button in the toolbar. This will not work on this document, so you'll only see
|
||||
the changes applied to a different story.
|
||||
|
||||
### Writing stories
|
||||
|
||||
To create new stories, copy an existing story in the `src/stories` directory and modify it to use your component or page. Please note that
|
||||
@@ -29,7 +34,7 @@ for the WebView UI Toolkit can be found [here](https://microsoft.github.io/vscod
|
||||
|
||||
### Updating VSCode CSS variables
|
||||
|
||||
The VSCode CSS variables that are injected into the Storybook preview are defined in the `src/stories/vscode-theme.css` file. They need to be
|
||||
The VSCode CSS variables that are injected into the Storybook preview are defined in the `src/stories/vscode-theme-dark.css` file. They need to be
|
||||
updated manually if new variables are added to VSCode. It can also be updated if you would like to manually preview a different theme. To update
|
||||
these variables, follow these steps:
|
||||
|
||||
@@ -47,9 +52,11 @@ expand all CSS variables.
|
||||
|
||||
<img src={stylesImage} />
|
||||
|
||||
7. Copy all variables to the `src/stories/vscode-theme.css` file.
|
||||
7. Copy all variables to the `src/stories/vscode-theme-dark.css` file.
|
||||
8. Now, select the `<body>` element which is a direct child of the `<html>` element.
|
||||
9. This time, you do not need to copy the variables. Instead, copy the styles on the `<body>` element to the `src/stories/vscode-theme.css` file.
|
||||
9. This time, you do not need to copy the variables. Instead, copy the styles on the `<body>` element to the `src/stories/vscode-theme-dark.css` file.
|
||||
See the image below for which styles need to be copied.
|
||||
|
||||
<img src={bodyImage} />
|
||||
|
||||
The same process can also be followed for updating the `src/stories/vscode-theme-light.css` file, but make sure to select the **Light+** theme.
|
||||
|
||||
@@ -3,7 +3,10 @@ import React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
|
||||
import { VariantAnalysisContainer } from '../../view/variant-analysis/VariantAnalysisContainer';
|
||||
import { VariantAnalysisRepoStatus } from '../../remote-queries/shared/variant-analysis';
|
||||
import {
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisScannedRepositoryDownloadStatus,
|
||||
} from '../../remote-queries/shared/variant-analysis';
|
||||
import { AnalysisAlert, AnalysisRawResults } from '../../remote-queries/shared/analysis-result';
|
||||
|
||||
import analysesResults from '../remote-queries/data/analysesResultsMessage.json';
|
||||
@@ -62,6 +65,14 @@ Canceled.args = {
|
||||
status: VariantAnalysisRepoStatus.Canceled,
|
||||
};
|
||||
|
||||
export const SucceededDownloading = Template.bind({});
|
||||
SucceededDownloading.args = {
|
||||
...Pending.args,
|
||||
status: VariantAnalysisRepoStatus.Succeeded,
|
||||
resultCount: 198,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
|
||||
};
|
||||
|
||||
export const InterpretedResults = Template.bind({});
|
||||
InterpretedResults.args = {
|
||||
...Pending.args,
|
||||
|
||||
@@ -1,16 +1,232 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
|
||||
import { VariantAnalysis as VariantAnalysisComponent } from '../../view/variant-analysis/VariantAnalysis';
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisDomainModel,
|
||||
VariantAnalysisQueryLanguage,
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisScannedRepositoryDownloadStatus,
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
VariantAnalysisScannedRepositoryState,
|
||||
VariantAnalysisStatus
|
||||
} from '../../remote-queries/shared/variant-analysis';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Variant Analysis',
|
||||
component: VariantAnalysisComponent,
|
||||
} as ComponentMeta<typeof VariantAnalysisComponent>;
|
||||
|
||||
const Template: ComponentStory<typeof VariantAnalysisComponent> = () => (
|
||||
<VariantAnalysisComponent />
|
||||
const Template: ComponentStory<typeof VariantAnalysisComponent> = (args) => (
|
||||
<VariantAnalysisComponent {...args} />
|
||||
);
|
||||
|
||||
export const VariantAnalysis = Template.bind({});
|
||||
const variantAnalysis: VariantAnalysisDomainModel = {
|
||||
id: 1,
|
||||
controllerRepoId: 1,
|
||||
actionsWorkflowRunId: 789263,
|
||||
query: {
|
||||
name: 'Example query',
|
||||
filePath: 'example.ql',
|
||||
language: VariantAnalysisQueryLanguage.Javascript,
|
||||
},
|
||||
databases: {},
|
||||
status: VariantAnalysisStatus.InProgress,
|
||||
scannedRepos: [
|
||||
{
|
||||
repository: {
|
||||
id: 1,
|
||||
fullName: 'octodemo/hello-world-1',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 2,
|
||||
fullName: 'octodemo/hello-world-2',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 3,
|
||||
fullName: 'octodemo/hello-world-3',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 4,
|
||||
fullName: 'octodemo/hello-world-4',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Pending,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 5,
|
||||
fullName: 'octodemo/hello-world-5',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Failed,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 6,
|
||||
fullName: 'octodemo/hello-world-6',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.InProgress,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 7,
|
||||
fullName: 'octodemo/hello-world-7',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Pending,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 8,
|
||||
fullName: 'octodemo/hello-world-8',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Pending,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 9,
|
||||
fullName: 'octodemo/hello-world-9',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Pending,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 10,
|
||||
fullName: 'octodemo/hello-world-10',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Pending,
|
||||
},
|
||||
],
|
||||
skippedRepos: {
|
||||
notFoundRepos: {
|
||||
repositoryCount: 2,
|
||||
repositories: [
|
||||
{
|
||||
fullName: 'octodemo/hello-globe'
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-planet'
|
||||
}
|
||||
]
|
||||
},
|
||||
noCodeqlDbRepos: {
|
||||
repositoryCount: 4,
|
||||
repositories: [
|
||||
{
|
||||
id: 100,
|
||||
fullName: 'octodemo/no-db-1'
|
||||
},
|
||||
{
|
||||
id: 101,
|
||||
fullName: 'octodemo/no-db-2'
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
fullName: 'octodemo/no-db-3'
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
fullName: 'octodemo/no-db-4'
|
||||
}
|
||||
]
|
||||
},
|
||||
overLimitRepos: {
|
||||
repositoryCount: 1,
|
||||
repositories: [
|
||||
{
|
||||
id: 201,
|
||||
fullName: 'octodemo/over-limit-1'
|
||||
}
|
||||
]
|
||||
},
|
||||
accessMismatchRepos: {
|
||||
repositoryCount: 1,
|
||||
repositories: [
|
||||
{
|
||||
id: 205,
|
||||
fullName: 'octodemo/private'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const repoStates: VariantAnalysisScannedRepositoryState[] = [
|
||||
{
|
||||
repositoryId: 1,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded
|
||||
},
|
||||
{
|
||||
repositoryId: 2,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
|
||||
},
|
||||
{
|
||||
repositoryId: 3,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
|
||||
},
|
||||
];
|
||||
|
||||
const repoResults: VariantAnalysisScannedRepositoryResult[] = [
|
||||
{
|
||||
variantAnalysisId: 1,
|
||||
repositoryId: 1,
|
||||
rawResults: {
|
||||
schema: {
|
||||
name: '#select',
|
||||
rows: 1,
|
||||
columns: [
|
||||
{
|
||||
kind: 'i'
|
||||
}
|
||||
]
|
||||
},
|
||||
resultSet: {
|
||||
schema: {
|
||||
name: '#select',
|
||||
rows: 1,
|
||||
columns: [
|
||||
{
|
||||
kind: 'i'
|
||||
}
|
||||
]
|
||||
},
|
||||
rows: [
|
||||
[
|
||||
60688
|
||||
]
|
||||
]
|
||||
},
|
||||
fileLinkPrefix: 'https://github.com/octodemo/hello-world-1/blob/59a2a6c7d9dde7a6ecb77c2f7e8197d6925c143b',
|
||||
sourceLocationPrefix: '/home/runner/work/bulk-builder/bulk-builder',
|
||||
capped: false
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export const Loading = Template.bind({});
|
||||
Loading.args = {};
|
||||
|
||||
export const FullExample = Template.bind({});
|
||||
FullExample.args = {
|
||||
variantAnalysis,
|
||||
repoStates,
|
||||
repoResults,
|
||||
};
|
||||
|
||||
@@ -100,14 +100,17 @@ Example.args = {
|
||||
},
|
||||
repositoryResults: [
|
||||
{
|
||||
variantAnalysisId: 1,
|
||||
repositoryId: 63537249,
|
||||
interpretedResults: interpretedResultsForRepo('facebook/create-react-app'),
|
||||
},
|
||||
{
|
||||
variantAnalysisId: 1,
|
||||
repositoryId: 167174,
|
||||
interpretedResults: interpretedResultsForRepo('jquery/jquery'),
|
||||
},
|
||||
{
|
||||
variantAnalysisId: 1,
|
||||
repositoryId: 237159,
|
||||
interpretedResults: interpretedResultsForRepo('expressjs/express'),
|
||||
}
|
||||
|
||||
@@ -628,3 +628,10 @@ body {
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used for setting the background on the Storybook preview.
|
||||
*/
|
||||
body {
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
635
extensions/ql-vscode/src/stories/vscode-theme-light.css
Normal file
635
extensions/ql-vscode/src/stories/vscode-theme-light.css
Normal file
@@ -0,0 +1,635 @@
|
||||
/*
|
||||
* These were copied from VSCode Light+ theme.
|
||||
*
|
||||
* To update these, open a webview in VSCode, open the webview developer tools and find the
|
||||
* iframe hosting the webview. The <html> element will have a style attribute that contains
|
||||
* the CSS variables. Copy these to this file.
|
||||
*/
|
||||
:root {
|
||||
--vscode-font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
--vscode-font-weight: normal;
|
||||
--vscode-font-size: 13px;
|
||||
--vscode-editor-font-family: Menlo, Monaco, "Courier New", monospace;
|
||||
--vscode-editor-font-weight: normal;
|
||||
--vscode-editor-font-size: 12px;
|
||||
--vscode-foreground: #616161;
|
||||
--vscode-disabledForeground: rgba(97, 97, 97, 0.5);
|
||||
--vscode-errorForeground: #a1260d;
|
||||
--vscode-descriptionForeground: #717171;
|
||||
--vscode-icon-foreground: #424242;
|
||||
--vscode-focusBorder: #0090f1;
|
||||
--vscode-textSeparator-foreground: rgba(0, 0, 0, 0.18);
|
||||
--vscode-textLink-foreground: #006ab1;
|
||||
--vscode-textLink-activeForeground: #006ab1;
|
||||
--vscode-textPreformat-foreground: #a31515;
|
||||
--vscode-textBlockQuote-background: rgba(127, 127, 127, 0.1);
|
||||
--vscode-textBlockQuote-border: rgba(0, 122, 204, 0.5);
|
||||
--vscode-textCodeBlock-background: rgba(220, 220, 220, 0.4);
|
||||
--vscode-widget-shadow: rgba(0, 0, 0, 0.16);
|
||||
--vscode-input-background: #ffffff;
|
||||
--vscode-input-foreground: #616161;
|
||||
--vscode-inputOption-activeBorder: rgba(0, 122, 204, 0);
|
||||
--vscode-inputOption-hoverBackground: rgba(184, 184, 184, 0.31);
|
||||
--vscode-inputOption-activeBackground: rgba(0, 144, 241, 0.2);
|
||||
--vscode-inputOption-activeForeground: #000000;
|
||||
--vscode-input-placeholderForeground: #767676;
|
||||
--vscode-inputValidation-infoBackground: #d6ecf2;
|
||||
--vscode-inputValidation-infoBorder: #007acc;
|
||||
--vscode-inputValidation-warningBackground: #f6f5d2;
|
||||
--vscode-inputValidation-warningBorder: #b89500;
|
||||
--vscode-inputValidation-errorBackground: #f2dede;
|
||||
--vscode-inputValidation-errorBorder: #be1100;
|
||||
--vscode-dropdown-background: #ffffff;
|
||||
--vscode-dropdown-border: #cecece;
|
||||
--vscode-checkbox-background: #ffffff;
|
||||
--vscode-checkbox-border: #cecece;
|
||||
--vscode-button-foreground: #ffffff;
|
||||
--vscode-button-separator: rgba(255, 255, 255, 0.4);
|
||||
--vscode-button-background: #007acc;
|
||||
--vscode-button-hoverBackground: #0062a3;
|
||||
--vscode-button-secondaryForeground: #ffffff;
|
||||
--vscode-button-secondaryBackground: #5f6a79;
|
||||
--vscode-button-secondaryHoverBackground: #4c5561;
|
||||
--vscode-badge-background: #c4c4c4;
|
||||
--vscode-badge-foreground: #333333;
|
||||
--vscode-scrollbar-shadow: #dddddd;
|
||||
--vscode-scrollbarSlider-background: rgba(100, 100, 100, 0.4);
|
||||
--vscode-scrollbarSlider-hoverBackground: rgba(100, 100, 100, 0.7);
|
||||
--vscode-scrollbarSlider-activeBackground: rgba(0, 0, 0, 0.6);
|
||||
--vscode-progressBar-background: #0e70c0;
|
||||
--vscode-editorError-foreground: #e51400;
|
||||
--vscode-editorWarning-foreground: #bf8803;
|
||||
--vscode-editorInfo-foreground: #1a85ff;
|
||||
--vscode-editorHint-foreground: #6c6c6c;
|
||||
--vscode-sash-hoverBorder: #0090f1;
|
||||
--vscode-editor-background: #ffffff;
|
||||
--vscode-editor-foreground: #000000;
|
||||
--vscode-editorStickyScroll-background: #ffffff;
|
||||
--vscode-editorStickyScrollHover-background: #f0f0f0;
|
||||
--vscode-editorWidget-background: #f3f3f3;
|
||||
--vscode-editorWidget-foreground: #616161;
|
||||
--vscode-editorWidget-border: #c8c8c8;
|
||||
--vscode-quickInput-background: #f3f3f3;
|
||||
--vscode-quickInput-foreground: #616161;
|
||||
--vscode-quickInputTitle-background: rgba(0, 0, 0, 0.06);
|
||||
--vscode-pickerGroup-foreground: #0066bf;
|
||||
--vscode-pickerGroup-border: #cccedb;
|
||||
--vscode-keybindingLabel-background: rgba(221, 221, 221, 0.4);
|
||||
--vscode-keybindingLabel-foreground: #555555;
|
||||
--vscode-keybindingLabel-border: rgba(204, 204, 204, 0.4);
|
||||
--vscode-keybindingLabel-bottomBorder: rgba(187, 187, 187, 0.4);
|
||||
--vscode-editor-selectionBackground: #add6ff;
|
||||
--vscode-editor-inactiveSelectionBackground: #e5ebf1;
|
||||
--vscode-editor-selectionHighlightBackground: rgba(173, 214, 255, 0.5);
|
||||
--vscode-editor-findMatchBackground: #a8ac94;
|
||||
--vscode-editor-findMatchHighlightBackground: rgba(234, 92, 0, 0.33);
|
||||
--vscode-editor-findRangeHighlightBackground: rgba(180, 180, 180, 0.3);
|
||||
--vscode-searchEditor-findMatchBackground: rgba(234, 92, 0, 0.22);
|
||||
--vscode-editor-hoverHighlightBackground: rgba(173, 214, 255, 0.15);
|
||||
--vscode-editorHoverWidget-background: #f3f3f3;
|
||||
--vscode-editorHoverWidget-foreground: #616161;
|
||||
--vscode-editorHoverWidget-border: #c8c8c8;
|
||||
--vscode-editorHoverWidget-statusBarBackground: #e7e7e7;
|
||||
--vscode-editorLink-activeForeground: #0000ff;
|
||||
--vscode-editorInlayHint-foreground: #333333;
|
||||
--vscode-editorInlayHint-background: rgba(196, 196, 196, 0.6);
|
||||
--vscode-editorInlayHint-typeForeground: #333333;
|
||||
--vscode-editorInlayHint-typeBackground: rgba(196, 196, 196, 0.6);
|
||||
--vscode-editorInlayHint-parameterForeground: #333333;
|
||||
--vscode-editorInlayHint-parameterBackground: rgba(196, 196, 196, 0.6);
|
||||
--vscode-editorLightBulb-foreground: #ddb100;
|
||||
--vscode-editorLightBulbAutoFix-foreground: #007acc;
|
||||
--vscode-diffEditor-insertedTextBackground: rgba(156, 204, 44, 0.25);
|
||||
--vscode-diffEditor-removedTextBackground: rgba(255, 0, 0, 0.2);
|
||||
--vscode-diffEditor-insertedLineBackground: rgba(155, 185, 85, 0.2);
|
||||
--vscode-diffEditor-removedLineBackground: rgba(255, 0, 0, 0.2);
|
||||
--vscode-diffEditor-diagonalFill: rgba(34, 34, 34, 0.2);
|
||||
--vscode-list-focusOutline: #0090f1;
|
||||
--vscode-list-focusAndSelectionOutline: #90c2f9;
|
||||
--vscode-list-activeSelectionBackground: #0060c0;
|
||||
--vscode-list-activeSelectionForeground: #ffffff;
|
||||
--vscode-list-activeSelectionIconForeground: #ffffff;
|
||||
--vscode-list-inactiveSelectionBackground: #e4e6f1;
|
||||
--vscode-list-hoverBackground: #e8e8e8;
|
||||
--vscode-list-dropBackground: #d6ebff;
|
||||
--vscode-list-highlightForeground: #0066bf;
|
||||
--vscode-list-focusHighlightForeground: #bbe7ff;
|
||||
--vscode-list-invalidItemForeground: #b89500;
|
||||
--vscode-list-errorForeground: #b01011;
|
||||
--vscode-list-warningForeground: #855f00;
|
||||
--vscode-listFilterWidget-background: #f3f3f3;
|
||||
--vscode-listFilterWidget-outline: rgba(0, 0, 0, 0);
|
||||
--vscode-listFilterWidget-noMatchesOutline: #be1100;
|
||||
--vscode-listFilterWidget-shadow: rgba(0, 0, 0, 0.16);
|
||||
--vscode-list-filterMatchBackground: rgba(234, 92, 0, 0.33);
|
||||
--vscode-tree-indentGuidesStroke: #a9a9a9;
|
||||
--vscode-tree-tableColumnsBorder: rgba(97, 97, 97, 0.13);
|
||||
--vscode-tree-tableOddRowsBackground: rgba(97, 97, 97, 0.04);
|
||||
--vscode-list-deemphasizedForeground: #8e8e90;
|
||||
--vscode-quickInputList-focusForeground: #ffffff;
|
||||
--vscode-quickInputList-focusIconForeground: #ffffff;
|
||||
--vscode-quickInputList-focusBackground: #0060c0;
|
||||
--vscode-menu-foreground: #616161;
|
||||
--vscode-menu-background: #ffffff;
|
||||
--vscode-menu-selectionForeground: #ffffff;
|
||||
--vscode-menu-selectionBackground: #0060c0;
|
||||
--vscode-menu-separatorBackground: #d4d4d4;
|
||||
--vscode-toolbar-hoverBackground: rgba(184, 184, 184, 0.31);
|
||||
--vscode-toolbar-activeBackground: rgba(166, 166, 166, 0.31);
|
||||
--vscode-editor-snippetTabstopHighlightBackground: rgba(10, 50, 100, 0.2);
|
||||
--vscode-editor-snippetFinalTabstopHighlightBorder: rgba(10, 50, 100, 0.5);
|
||||
--vscode-breadcrumb-foreground: rgba(97, 97, 97, 0.8);
|
||||
--vscode-breadcrumb-background: #ffffff;
|
||||
--vscode-breadcrumb-focusForeground: #4e4e4e;
|
||||
--vscode-breadcrumb-activeSelectionForeground: #4e4e4e;
|
||||
--vscode-breadcrumbPicker-background: #f3f3f3;
|
||||
--vscode-merge-currentHeaderBackground: rgba(64, 200, 174, 0.5);
|
||||
--vscode-merge-currentContentBackground: rgba(64, 200, 174, 0.2);
|
||||
--vscode-merge-incomingHeaderBackground: rgba(64, 166, 255, 0.5);
|
||||
--vscode-merge-incomingContentBackground: rgba(64, 166, 255, 0.2);
|
||||
--vscode-merge-commonHeaderBackground: rgba(96, 96, 96, 0.4);
|
||||
--vscode-merge-commonContentBackground: rgba(96, 96, 96, 0.16);
|
||||
--vscode-editorOverviewRuler-currentContentForeground: rgba(
|
||||
64,
|
||||
200,
|
||||
174,
|
||||
0.5
|
||||
);
|
||||
--vscode-editorOverviewRuler-incomingContentForeground: rgba(
|
||||
64,
|
||||
166,
|
||||
255,
|
||||
0.5
|
||||
);
|
||||
--vscode-editorOverviewRuler-commonContentForeground: rgba(96, 96, 96, 0.4);
|
||||
--vscode-editorOverviewRuler-findMatchForeground: rgba(209, 134, 22, 0.49);
|
||||
--vscode-editorOverviewRuler-selectionHighlightForeground: rgba(
|
||||
160,
|
||||
160,
|
||||
160,
|
||||
0.8
|
||||
);
|
||||
--vscode-minimap-findMatchHighlight: #d18616;
|
||||
--vscode-minimap-selectionOccurrenceHighlight: #c9c9c9;
|
||||
--vscode-minimap-selectionHighlight: #add6ff;
|
||||
--vscode-minimap-errorHighlight: rgba(255, 18, 18, 0.7);
|
||||
--vscode-minimap-warningHighlight: #bf8803;
|
||||
--vscode-minimap-foregroundOpacity: #000000;
|
||||
--vscode-minimapSlider-background: rgba(100, 100, 100, 0.2);
|
||||
--vscode-minimapSlider-hoverBackground: rgba(100, 100, 100, 0.35);
|
||||
--vscode-minimapSlider-activeBackground: rgba(0, 0, 0, 0.3);
|
||||
--vscode-problemsErrorIcon-foreground: #e51400;
|
||||
--vscode-problemsWarningIcon-foreground: #bf8803;
|
||||
--vscode-problemsInfoIcon-foreground: #1a85ff;
|
||||
--vscode-charts-foreground: #616161;
|
||||
--vscode-charts-lines: rgba(97, 97, 97, 0.5);
|
||||
--vscode-charts-red: #e51400;
|
||||
--vscode-charts-blue: #1a85ff;
|
||||
--vscode-charts-yellow: #bf8803;
|
||||
--vscode-charts-orange: #d18616;
|
||||
--vscode-charts-green: #388a34;
|
||||
--vscode-charts-purple: #652d90;
|
||||
--vscode-editor-lineHighlightBorder: #eeeeee;
|
||||
--vscode-editor-rangeHighlightBackground: rgba(253, 255, 0, 0.2);
|
||||
--vscode-editor-symbolHighlightBackground: rgba(234, 92, 0, 0.33);
|
||||
--vscode-editorCursor-foreground: #000000;
|
||||
--vscode-editorWhitespace-foreground: rgba(51, 51, 51, 0.2);
|
||||
--vscode-editorIndentGuide-background: #d3d3d3;
|
||||
--vscode-editorIndentGuide-activeBackground: #939393;
|
||||
--vscode-editorLineNumber-foreground: #237893;
|
||||
--vscode-editorActiveLineNumber-foreground: #0b216f;
|
||||
--vscode-editorLineNumber-activeForeground: #0b216f;
|
||||
--vscode-editorRuler-foreground: #d3d3d3;
|
||||
--vscode-editorCodeLens-foreground: #919191;
|
||||
--vscode-editorBracketMatch-background: rgba(0, 100, 0, 0.1);
|
||||
--vscode-editorBracketMatch-border: #b9b9b9;
|
||||
--vscode-editorOverviewRuler-border: rgba(127, 127, 127, 0.3);
|
||||
--vscode-editorGutter-background: #ffffff;
|
||||
--vscode-editorUnnecessaryCode-opacity: rgba(0, 0, 0, 0.47);
|
||||
--vscode-editorGhostText-foreground: rgba(0, 0, 0, 0.47);
|
||||
--vscode-editorOverviewRuler-rangeHighlightForeground: rgba(0, 122, 204, 0.6);
|
||||
--vscode-editorOverviewRuler-errorForeground: rgba(255, 18, 18, 0.7);
|
||||
--vscode-editorOverviewRuler-warningForeground: #bf8803;
|
||||
--vscode-editorOverviewRuler-infoForeground: #1a85ff;
|
||||
--vscode-editorBracketHighlight-foreground1: #0431fa;
|
||||
--vscode-editorBracketHighlight-foreground2: #319331;
|
||||
--vscode-editorBracketHighlight-foreground3: #7b3814;
|
||||
--vscode-editorBracketHighlight-foreground4: rgba(0, 0, 0, 0);
|
||||
--vscode-editorBracketHighlight-foreground5: rgba(0, 0, 0, 0);
|
||||
--vscode-editorBracketHighlight-foreground6: rgba(0, 0, 0, 0);
|
||||
--vscode-editorBracketHighlight-unexpectedBracket\.foreground: rgba(
|
||||
255,
|
||||
18,
|
||||
18,
|
||||
0.8
|
||||
);
|
||||
--vscode-editorBracketPairGuide-background1: rgba(0, 0, 0, 0);
|
||||
--vscode-editorBracketPairGuide-background2: rgba(0, 0, 0, 0);
|
||||
--vscode-editorBracketPairGuide-background3: rgba(0, 0, 0, 0);
|
||||
--vscode-editorBracketPairGuide-background4: rgba(0, 0, 0, 0);
|
||||
--vscode-editorBracketPairGuide-background5: rgba(0, 0, 0, 0);
|
||||
--vscode-editorBracketPairGuide-background6: rgba(0, 0, 0, 0);
|
||||
--vscode-editorBracketPairGuide-activeBackground1: rgba(0, 0, 0, 0);
|
||||
--vscode-editorBracketPairGuide-activeBackground2: rgba(0, 0, 0, 0);
|
||||
--vscode-editorBracketPairGuide-activeBackground3: rgba(0, 0, 0, 0);
|
||||
--vscode-editorBracketPairGuide-activeBackground4: rgba(0, 0, 0, 0);
|
||||
--vscode-editorBracketPairGuide-activeBackground5: rgba(0, 0, 0, 0);
|
||||
--vscode-editorBracketPairGuide-activeBackground6: rgba(0, 0, 0, 0);
|
||||
--vscode-editorUnicodeHighlight-border: #cea33d;
|
||||
--vscode-editorUnicodeHighlight-background: rgba(206, 163, 61, 0.08);
|
||||
--vscode-symbolIcon-arrayForeground: #616161;
|
||||
--vscode-symbolIcon-booleanForeground: #616161;
|
||||
--vscode-symbolIcon-classForeground: #d67e00;
|
||||
--vscode-symbolIcon-colorForeground: #616161;
|
||||
--vscode-symbolIcon-constantForeground: #616161;
|
||||
--vscode-symbolIcon-constructorForeground: #652d90;
|
||||
--vscode-symbolIcon-enumeratorForeground: #d67e00;
|
||||
--vscode-symbolIcon-enumeratorMemberForeground: #007acc;
|
||||
--vscode-symbolIcon-eventForeground: #d67e00;
|
||||
--vscode-symbolIcon-fieldForeground: #007acc;
|
||||
--vscode-symbolIcon-fileForeground: #616161;
|
||||
--vscode-symbolIcon-folderForeground: #616161;
|
||||
--vscode-symbolIcon-functionForeground: #652d90;
|
||||
--vscode-symbolIcon-interfaceForeground: #007acc;
|
||||
--vscode-symbolIcon-keyForeground: #616161;
|
||||
--vscode-symbolIcon-keywordForeground: #616161;
|
||||
--vscode-symbolIcon-methodForeground: #652d90;
|
||||
--vscode-symbolIcon-moduleForeground: #616161;
|
||||
--vscode-symbolIcon-namespaceForeground: #616161;
|
||||
--vscode-symbolIcon-nullForeground: #616161;
|
||||
--vscode-symbolIcon-numberForeground: #616161;
|
||||
--vscode-symbolIcon-objectForeground: #616161;
|
||||
--vscode-symbolIcon-operatorForeground: #616161;
|
||||
--vscode-symbolIcon-packageForeground: #616161;
|
||||
--vscode-symbolIcon-propertyForeground: #616161;
|
||||
--vscode-symbolIcon-referenceForeground: #616161;
|
||||
--vscode-symbolIcon-snippetForeground: #616161;
|
||||
--vscode-symbolIcon-stringForeground: #616161;
|
||||
--vscode-symbolIcon-structForeground: #616161;
|
||||
--vscode-symbolIcon-textForeground: #616161;
|
||||
--vscode-symbolIcon-typeParameterForeground: #616161;
|
||||
--vscode-symbolIcon-unitForeground: #616161;
|
||||
--vscode-symbolIcon-variableForeground: #007acc;
|
||||
--vscode-editorHoverWidget-highlightForeground: #0066bf;
|
||||
--vscode-editorOverviewRuler-bracketMatchForeground: #a0a0a0;
|
||||
--vscode-editor-foldBackground: rgba(173, 214, 255, 0.3);
|
||||
--vscode-editorGutter-foldingControlForeground: #424242;
|
||||
--vscode-editor-linkedEditingBackground: rgba(255, 0, 0, 0.3);
|
||||
--vscode-editor-wordHighlightBackground: rgba(87, 87, 87, 0.25);
|
||||
--vscode-editor-wordHighlightStrongBackground: rgba(14, 99, 156, 0.25);
|
||||
--vscode-editorOverviewRuler-wordHighlightForeground: rgba(
|
||||
160,
|
||||
160,
|
||||
160,
|
||||
0.8
|
||||
);
|
||||
--vscode-editorOverviewRuler-wordHighlightStrongForeground: rgba(
|
||||
192,
|
||||
160,
|
||||
192,
|
||||
0.8
|
||||
);
|
||||
--vscode-peekViewTitle-background: rgba(26, 133, 255, 0.1);
|
||||
--vscode-peekViewTitleLabel-foreground: #000000;
|
||||
--vscode-peekViewTitleDescription-foreground: #616161;
|
||||
--vscode-peekView-border: #1a85ff;
|
||||
--vscode-peekViewResult-background: #f3f3f3;
|
||||
--vscode-peekViewResult-lineForeground: #646465;
|
||||
--vscode-peekViewResult-fileForeground: #1e1e1e;
|
||||
--vscode-peekViewResult-selectionBackground: rgba(51, 153, 255, 0.2);
|
||||
--vscode-peekViewResult-selectionForeground: #6c6c6c;
|
||||
--vscode-peekViewEditor-background: #f2f8fc;
|
||||
--vscode-peekViewEditorGutter-background: #f2f8fc;
|
||||
--vscode-peekViewResult-matchHighlightBackground: rgba(234, 92, 0, 0.3);
|
||||
--vscode-peekViewEditor-matchHighlightBackground: rgba(245, 216, 2, 0.87);
|
||||
--vscode-editorMarkerNavigationError-background: #e51400;
|
||||
--vscode-editorMarkerNavigationError-headerBackground: rgba(229, 20, 0, 0.1);
|
||||
--vscode-editorMarkerNavigationWarning-background: #bf8803;
|
||||
--vscode-editorMarkerNavigationWarning-headerBackground: rgba(
|
||||
191,
|
||||
136,
|
||||
3,
|
||||
0.1
|
||||
);
|
||||
--vscode-editorMarkerNavigationInfo-background: #1a85ff;
|
||||
--vscode-editorMarkerNavigationInfo-headerBackground: rgba(26, 133, 255, 0.1);
|
||||
--vscode-editorMarkerNavigation-background: #ffffff;
|
||||
--vscode-editorSuggestWidget-background: #f3f3f3;
|
||||
--vscode-editorSuggestWidget-border: #c8c8c8;
|
||||
--vscode-editorSuggestWidget-foreground: #000000;
|
||||
--vscode-editorSuggestWidget-selectedForeground: #ffffff;
|
||||
--vscode-editorSuggestWidget-selectedIconForeground: #ffffff;
|
||||
--vscode-editorSuggestWidget-selectedBackground: #0060c0;
|
||||
--vscode-editorSuggestWidget-highlightForeground: #0066bf;
|
||||
--vscode-editorSuggestWidget-focusHighlightForeground: #bbe7ff;
|
||||
--vscode-editorSuggestWidgetStatus-foreground: rgba(0, 0, 0, 0.5);
|
||||
--vscode-tab-activeBackground: #ffffff;
|
||||
--vscode-tab-unfocusedActiveBackground: #ffffff;
|
||||
--vscode-tab-inactiveBackground: #ececec;
|
||||
--vscode-tab-unfocusedInactiveBackground: #ececec;
|
||||
--vscode-tab-activeForeground: #333333;
|
||||
--vscode-tab-inactiveForeground: rgba(51, 51, 51, 0.7);
|
||||
--vscode-tab-unfocusedActiveForeground: rgba(51, 51, 51, 0.7);
|
||||
--vscode-tab-unfocusedInactiveForeground: rgba(51, 51, 51, 0.35);
|
||||
--vscode-tab-border: #f3f3f3;
|
||||
--vscode-tab-lastPinnedBorder: rgba(97, 97, 97, 0.19);
|
||||
--vscode-tab-activeModifiedBorder: #33aaee;
|
||||
--vscode-tab-inactiveModifiedBorder: rgba(51, 170, 238, 0.5);
|
||||
--vscode-tab-unfocusedActiveModifiedBorder: rgba(51, 170, 238, 0.7);
|
||||
--vscode-tab-unfocusedInactiveModifiedBorder: rgba(51, 170, 238, 0.25);
|
||||
--vscode-editorPane-background: #ffffff;
|
||||
--vscode-editorGroupHeader-tabsBackground: #f3f3f3;
|
||||
--vscode-editorGroupHeader-noTabsBackground: #ffffff;
|
||||
--vscode-editorGroup-border: #e7e7e7;
|
||||
--vscode-editorGroup-dropBackground: rgba(38, 119, 203, 0.18);
|
||||
--vscode-editorGroup-dropIntoPromptForeground: #616161;
|
||||
--vscode-editorGroup-dropIntoPromptBackground: #f3f3f3;
|
||||
--vscode-sideBySideEditor-horizontalBorder: #e7e7e7;
|
||||
--vscode-sideBySideEditor-verticalBorder: #e7e7e7;
|
||||
--vscode-panel-background: #ffffff;
|
||||
--vscode-panel-border: rgba(128, 128, 128, 0.35);
|
||||
--vscode-panelTitle-activeForeground: #424242;
|
||||
--vscode-panelTitle-inactiveForeground: rgba(66, 66, 66, 0.75);
|
||||
--vscode-panelTitle-activeBorder: #424242;
|
||||
--vscode-panelInput-border: #dddddd;
|
||||
--vscode-panel-dropBorder: #424242;
|
||||
--vscode-panelSection-dropBackground: rgba(38, 119, 203, 0.18);
|
||||
--vscode-panelSectionHeader-background: rgba(128, 128, 128, 0.2);
|
||||
--vscode-panelSection-border: rgba(128, 128, 128, 0.35);
|
||||
--vscode-banner-background: #004386;
|
||||
--vscode-banner-foreground: #ffffff;
|
||||
--vscode-banner-iconForeground: #1a85ff;
|
||||
--vscode-statusBar-foreground: #ffffff;
|
||||
--vscode-statusBar-noFolderForeground: #ffffff;
|
||||
--vscode-statusBar-background: #007acc;
|
||||
--vscode-statusBar-noFolderBackground: #68217a;
|
||||
--vscode-statusBar-focusBorder: #ffffff;
|
||||
--vscode-statusBarItem-activeBackground: rgba(255, 255, 255, 0.18);
|
||||
--vscode-statusBarItem-focusBorder: #ffffff;
|
||||
--vscode-statusBarItem-hoverBackground: rgba(255, 255, 255, 0.12);
|
||||
--vscode-statusBarItem-compactHoverBackground: rgba(255, 255, 255, 0.2);
|
||||
--vscode-statusBarItem-prominentForeground: #ffffff;
|
||||
--vscode-statusBarItem-prominentBackground: rgba(0, 0, 0, 0.5);
|
||||
--vscode-statusBarItem-prominentHoverBackground: rgba(0, 0, 0, 0.3);
|
||||
--vscode-statusBarItem-errorBackground: #c72e0f;
|
||||
--vscode-statusBarItem-errorForeground: #ffffff;
|
||||
--vscode-statusBarItem-warningBackground: #725102;
|
||||
--vscode-statusBarItem-warningForeground: #ffffff;
|
||||
--vscode-activityBar-background: #2c2c2c;
|
||||
--vscode-activityBar-foreground: #ffffff;
|
||||
--vscode-activityBar-inactiveForeground: rgba(255, 255, 255, 0.4);
|
||||
--vscode-activityBar-activeBorder: #ffffff;
|
||||
--vscode-activityBar-dropBorder: #ffffff;
|
||||
--vscode-activityBarBadge-background: #007acc;
|
||||
--vscode-activityBarBadge-foreground: #ffffff;
|
||||
--vscode-activityBarItem-settingsProfilesForeground: rgba(255, 255, 255, 0.4);
|
||||
--vscode-activityBarItem-settingsProfilesHoverForeground: #ffffff;
|
||||
--vscode-activityBarItem-settingsProfilesBackground: #4d4d4d;
|
||||
--vscode-statusBarItem-remoteBackground: #16825d;
|
||||
--vscode-statusBarItem-remoteForeground: #ffffff;
|
||||
--vscode-extensionBadge-remoteBackground: #007acc;
|
||||
--vscode-extensionBadge-remoteForeground: #ffffff;
|
||||
--vscode-sideBar-background: #f3f3f3;
|
||||
--vscode-sideBarTitle-foreground: #6f6f6f;
|
||||
--vscode-sideBar-dropBackground: rgba(38, 119, 203, 0.18);
|
||||
--vscode-sideBarSectionHeader-background: rgba(0, 0, 0, 0);
|
||||
--vscode-sideBarSectionHeader-border: rgba(97, 97, 97, 0.19);
|
||||
--vscode-titleBar-activeForeground: #333333;
|
||||
--vscode-titleBar-inactiveForeground: rgba(51, 51, 51, 0.6);
|
||||
--vscode-titleBar-activeBackground: #dddddd;
|
||||
--vscode-titleBar-inactiveBackground: rgba(221, 221, 221, 0.6);
|
||||
--vscode-menubar-selectionForeground: #333333;
|
||||
--vscode-menubar-selectionBackground: rgba(184, 184, 184, 0.31);
|
||||
--vscode-notifications-foreground: #616161;
|
||||
--vscode-notifications-background: #f3f3f3;
|
||||
--vscode-notificationLink-foreground: #006ab1;
|
||||
--vscode-notificationCenterHeader-background: #e7e7e7;
|
||||
--vscode-notifications-border: #e7e7e7;
|
||||
--vscode-notificationsErrorIcon-foreground: #e51400;
|
||||
--vscode-notificationsWarningIcon-foreground: #bf8803;
|
||||
--vscode-notificationsInfoIcon-foreground: #1a85ff;
|
||||
--vscode-commandCenter-foreground: #333333;
|
||||
--vscode-commandCenter-activeForeground: #333333;
|
||||
--vscode-commandCenter-activeBackground: rgba(184, 184, 184, 0.31);
|
||||
--vscode-commandCenter-border: rgba(128, 128, 128, 0.35);
|
||||
--vscode-editorCommentsWidget-resolvedBorder: rgba(97, 97, 97, 0.5);
|
||||
--vscode-editorCommentsWidget-unresolvedBorder: #1a85ff;
|
||||
--vscode-editorCommentsWidget-rangeBackground: rgba(26, 133, 255, 0.1);
|
||||
--vscode-editorCommentsWidget-rangeBorder: rgba(26, 133, 255, 0.4);
|
||||
--vscode-editorCommentsWidget-rangeActiveBackground: rgba(26, 133, 255, 0.1);
|
||||
--vscode-editorCommentsWidget-rangeActiveBorder: rgba(26, 133, 255, 0.4);
|
||||
--vscode-editorGutter-commentRangeForeground: #d5d8e9;
|
||||
--vscode-debugToolBar-background: #f3f3f3;
|
||||
--vscode-debugIcon-startForeground: #388a34;
|
||||
--vscode-editor-stackFrameHighlightBackground: rgba(255, 255, 102, 0.45);
|
||||
--vscode-editor-focusedStackFrameHighlightBackground: rgba(
|
||||
206,
|
||||
231,
|
||||
206,
|
||||
0.45
|
||||
);
|
||||
--vscode-mergeEditor-change\.background: rgba(155, 185, 85, 0.2);
|
||||
--vscode-mergeEditor-change\.word\.background: rgba(156, 204, 44, 0.4);
|
||||
--vscode-mergeEditor-conflict\.unhandledUnfocused\.border: rgba(
|
||||
255,
|
||||
166,
|
||||
0,
|
||||
0.48
|
||||
);
|
||||
--vscode-mergeEditor-conflict\.unhandledFocused\.border: #ffa600;
|
||||
--vscode-mergeEditor-conflict\.handledUnfocused\.border: rgba(
|
||||
134,
|
||||
134,
|
||||
134,
|
||||
0.29
|
||||
);
|
||||
--vscode-mergeEditor-conflict\.handledFocused\.border: rgba(
|
||||
193,
|
||||
193,
|
||||
193,
|
||||
0.8
|
||||
);
|
||||
--vscode-mergeEditor-conflict\.handled\.minimapOverViewRuler: rgba(
|
||||
173,
|
||||
172,
|
||||
168,
|
||||
0.93
|
||||
);
|
||||
--vscode-mergeEditor-conflict\.unhandled\.minimapOverViewRuler: #fcba03;
|
||||
--vscode-mergeEditor-conflictingLines\.background: rgba(255, 234, 0, 0.28);
|
||||
--vscode-settings-headerForeground: #444444;
|
||||
--vscode-settings-modifiedItemIndicator: #66afe0;
|
||||
--vscode-settings-headerBorder: rgba(128, 128, 128, 0.35);
|
||||
--vscode-settings-sashBorder: rgba(128, 128, 128, 0.35);
|
||||
--vscode-settings-dropdownBackground: #ffffff;
|
||||
--vscode-settings-dropdownBorder: #cecece;
|
||||
--vscode-settings-dropdownListBorder: #c8c8c8;
|
||||
--vscode-settings-checkboxBackground: #ffffff;
|
||||
--vscode-settings-checkboxBorder: #cecece;
|
||||
--vscode-settings-textInputBackground: #ffffff;
|
||||
--vscode-settings-textInputForeground: #616161;
|
||||
--vscode-settings-textInputBorder: #cecece;
|
||||
--vscode-settings-numberInputBackground: #ffffff;
|
||||
--vscode-settings-numberInputForeground: #616161;
|
||||
--vscode-settings-numberInputBorder: #cecece;
|
||||
--vscode-settings-focusedRowBackground: rgba(232, 232, 232, 0.6);
|
||||
--vscode-settings-rowHoverBackground: rgba(232, 232, 232, 0.3);
|
||||
--vscode-settings-focusedRowBorder: rgba(0, 0, 0, 0.12);
|
||||
--vscode-terminal-foreground: #333333;
|
||||
--vscode-terminal-selectionBackground: #add6ff;
|
||||
--vscode-terminal-inactiveSelectionBackground: #e5ebf1;
|
||||
--vscode-terminalCommandDecoration-defaultBackground: rgba(0, 0, 0, 0.25);
|
||||
--vscode-terminalCommandDecoration-successBackground: #2090d3;
|
||||
--vscode-terminalCommandDecoration-errorBackground: #e51400;
|
||||
--vscode-terminalOverviewRuler-cursorForeground: rgba(160, 160, 160, 0.8);
|
||||
--vscode-terminal-border: rgba(128, 128, 128, 0.35);
|
||||
--vscode-terminal-findMatchBackground: #a8ac94;
|
||||
--vscode-terminal-findMatchHighlightBackground: rgba(234, 92, 0, 0.33);
|
||||
--vscode-terminalOverviewRuler-findMatchForeground: rgba(209, 134, 22, 0.49);
|
||||
--vscode-terminal-dropBackground: rgba(38, 119, 203, 0.18);
|
||||
--vscode-testing-iconFailed: #f14c4c;
|
||||
--vscode-testing-iconErrored: #f14c4c;
|
||||
--vscode-testing-iconPassed: #73c991;
|
||||
--vscode-testing-runAction: #73c991;
|
||||
--vscode-testing-iconQueued: #cca700;
|
||||
--vscode-testing-iconUnset: #848484;
|
||||
--vscode-testing-iconSkipped: #848484;
|
||||
--vscode-testing-peekBorder: #e51400;
|
||||
--vscode-testing-peekHeaderBackground: rgba(229, 20, 0, 0.1);
|
||||
--vscode-testing-message\.error\.decorationForeground: #e51400;
|
||||
--vscode-testing-message\.error\.lineBackground: rgba(255, 0, 0, 0.2);
|
||||
--vscode-testing-message\.info\.decorationForeground: rgba(0, 0, 0, 0.5);
|
||||
--vscode-welcomePage-tileBackground: #f3f3f3;
|
||||
--vscode-welcomePage-tileHoverBackground: #dbdbdb;
|
||||
--vscode-welcomePage-tileShadow: rgba(0, 0, 0, 0.16);
|
||||
--vscode-welcomePage-progress\.background: #ffffff;
|
||||
--vscode-welcomePage-progress\.foreground: #006ab1;
|
||||
--vscode-debugExceptionWidget-border: #a31515;
|
||||
--vscode-debugExceptionWidget-background: #f1dfde;
|
||||
--vscode-ports-iconRunningProcessForeground: #369432;
|
||||
--vscode-statusBar-debuggingBackground: #cc6633;
|
||||
--vscode-statusBar-debuggingForeground: #ffffff;
|
||||
--vscode-editor-inlineValuesForeground: rgba(0, 0, 0, 0.5);
|
||||
--vscode-editor-inlineValuesBackground: rgba(255, 200, 0, 0.2);
|
||||
--vscode-editorGutter-modifiedBackground: #2090d3;
|
||||
--vscode-editorGutter-addedBackground: #48985d;
|
||||
--vscode-editorGutter-deletedBackground: #e51400;
|
||||
--vscode-minimapGutter-modifiedBackground: #2090d3;
|
||||
--vscode-minimapGutter-addedBackground: #48985d;
|
||||
--vscode-minimapGutter-deletedBackground: #e51400;
|
||||
--vscode-editorOverviewRuler-modifiedForeground: rgba(32, 144, 211, 0.6);
|
||||
--vscode-editorOverviewRuler-addedForeground: rgba(72, 152, 93, 0.6);
|
||||
--vscode-editorOverviewRuler-deletedForeground: rgba(229, 20, 0, 0.6);
|
||||
--vscode-debugIcon-breakpointForeground: #e51400;
|
||||
--vscode-debugIcon-breakpointDisabledForeground: #848484;
|
||||
--vscode-debugIcon-breakpointUnverifiedForeground: #848484;
|
||||
--vscode-debugIcon-breakpointCurrentStackframeForeground: #be8700;
|
||||
--vscode-debugIcon-breakpointStackframeForeground: #89d185;
|
||||
--vscode-notebook-cellBorderColor: #e8e8e8;
|
||||
--vscode-notebook-focusedEditorBorder: #0090f1;
|
||||
--vscode-notebookStatusSuccessIcon-foreground: #388a34;
|
||||
--vscode-notebookStatusErrorIcon-foreground: #a1260d;
|
||||
--vscode-notebookStatusRunningIcon-foreground: #616161;
|
||||
--vscode-notebook-cellToolbarSeparator: rgba(128, 128, 128, 0.35);
|
||||
--vscode-notebook-selectedCellBackground: rgba(200, 221, 241, 0.31);
|
||||
--vscode-notebook-selectedCellBorder: #e8e8e8;
|
||||
--vscode-notebook-focusedCellBorder: #0090f1;
|
||||
--vscode-notebook-inactiveFocusedCellBorder: #e8e8e8;
|
||||
--vscode-notebook-cellStatusBarItemHoverBackground: rgba(0, 0, 0, 0.08);
|
||||
--vscode-notebook-cellInsertionIndicator: #0090f1;
|
||||
--vscode-notebookScrollbarSlider-background: rgba(100, 100, 100, 0.4);
|
||||
--vscode-notebookScrollbarSlider-hoverBackground: rgba(100, 100, 100, 0.7);
|
||||
--vscode-notebookScrollbarSlider-activeBackground: rgba(0, 0, 0, 0.6);
|
||||
--vscode-notebook-symbolHighlightBackground: rgba(253, 255, 0, 0.2);
|
||||
--vscode-notebook-cellEditorBackground: #f3f3f3;
|
||||
--vscode-notebook-editorBackground: #ffffff;
|
||||
--vscode-keybindingTable-headerBackground: rgba(97, 97, 97, 0.04);
|
||||
--vscode-keybindingTable-rowsBackground: rgba(97, 97, 97, 0.04);
|
||||
--vscode-scm-providerBorder: #c8c8c8;
|
||||
--vscode-searchEditor-textInputBorder: #cecece;
|
||||
--vscode-debugTokenExpression-name: #9b46b0;
|
||||
--vscode-debugTokenExpression-value: rgba(108, 108, 108, 0.8);
|
||||
--vscode-debugTokenExpression-string: #a31515;
|
||||
--vscode-debugTokenExpression-boolean: #0000ff;
|
||||
--vscode-debugTokenExpression-number: #098658;
|
||||
--vscode-debugTokenExpression-error: #e51400;
|
||||
--vscode-debugView-exceptionLabelForeground: #ffffff;
|
||||
--vscode-debugView-exceptionLabelBackground: #a31515;
|
||||
--vscode-debugView-stateLabelForeground: #616161;
|
||||
--vscode-debugView-stateLabelBackground: rgba(136, 136, 136, 0.27);
|
||||
--vscode-debugView-valueChangedHighlight: #569cd6;
|
||||
--vscode-debugConsole-infoForeground: #1a85ff;
|
||||
--vscode-debugConsole-warningForeground: #bf8803;
|
||||
--vscode-debugConsole-errorForeground: #a1260d;
|
||||
--vscode-debugConsole-sourceForeground: #616161;
|
||||
--vscode-debugConsoleInputIcon-foreground: #616161;
|
||||
--vscode-debugIcon-pauseForeground: #007acc;
|
||||
--vscode-debugIcon-stopForeground: #a1260d;
|
||||
--vscode-debugIcon-disconnectForeground: #a1260d;
|
||||
--vscode-debugIcon-restartForeground: #388a34;
|
||||
--vscode-debugIcon-stepOverForeground: #007acc;
|
||||
--vscode-debugIcon-stepIntoForeground: #007acc;
|
||||
--vscode-debugIcon-stepOutForeground: #007acc;
|
||||
--vscode-debugIcon-continueForeground: #007acc;
|
||||
--vscode-debugIcon-stepBackForeground: #007acc;
|
||||
--vscode-extensionButton-prominentBackground: #007acc;
|
||||
--vscode-extensionButton-prominentForeground: #ffffff;
|
||||
--vscode-extensionButton-prominentHoverBackground: #0062a3;
|
||||
--vscode-extensionIcon-starForeground: #df6100;
|
||||
--vscode-extensionIcon-verifiedForeground: #006ab1;
|
||||
--vscode-extensionIcon-preReleaseForeground: #1d9271;
|
||||
--vscode-extensionIcon-sponsorForeground: #b51e78;
|
||||
--vscode-terminal-ansiBlack: #000000;
|
||||
--vscode-terminal-ansiRed: #cd3131;
|
||||
--vscode-terminal-ansiGreen: #00bc00;
|
||||
--vscode-terminal-ansiYellow: #949800;
|
||||
--vscode-terminal-ansiBlue: #0451a5;
|
||||
--vscode-terminal-ansiMagenta: #bc05bc;
|
||||
--vscode-terminal-ansiCyan: #0598bc;
|
||||
--vscode-terminal-ansiWhite: #555555;
|
||||
--vscode-terminal-ansiBrightBlack: #666666;
|
||||
--vscode-terminal-ansiBrightRed: #cd3131;
|
||||
--vscode-terminal-ansiBrightGreen: #14ce14;
|
||||
--vscode-terminal-ansiBrightYellow: #b5ba00;
|
||||
--vscode-terminal-ansiBrightBlue: #0451a5;
|
||||
--vscode-terminal-ansiBrightMagenta: #bc05bc;
|
||||
--vscode-terminal-ansiBrightCyan: #0598bc;
|
||||
--vscode-terminal-ansiBrightWhite: #a5a5a5;
|
||||
--vscode-interactive-activeCodeBorder: #1a85ff;
|
||||
--vscode-interactive-inactiveCodeBorder: #e4e6f1;
|
||||
--vscode-gitDecoration-addedResourceForeground: #587c0c;
|
||||
--vscode-gitDecoration-modifiedResourceForeground: #895503;
|
||||
--vscode-gitDecoration-deletedResourceForeground: #ad0707;
|
||||
--vscode-gitDecoration-renamedResourceForeground: #007100;
|
||||
--vscode-gitDecoration-untrackedResourceForeground: #007100;
|
||||
--vscode-gitDecoration-ignoredResourceForeground: #8e8e90;
|
||||
--vscode-gitDecoration-stageModifiedResourceForeground: #895503;
|
||||
--vscode-gitDecoration-stageDeletedResourceForeground: #ad0707;
|
||||
--vscode-gitDecoration-conflictingResourceForeground: #ad0707;
|
||||
--vscode-gitDecoration-submoduleResourceForeground: #1258a7;
|
||||
--vscode-issues-newIssueDecoration: rgba(0, 0, 0, 0.28);
|
||||
--vscode-issues-open: #22863a;
|
||||
--vscode-issues-closed: #cb2431;
|
||||
--vscode-pullRequests-notification: #1a85ff;
|
||||
--vscode-testExplorer-errorDecorationBackground: #f2dede;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is copied in the same way, but from the <body> element
|
||||
*/
|
||||
body {
|
||||
background-color: transparent;
|
||||
color: var(--vscode-editor-foreground);
|
||||
font-family: var(--vscode-font-family);
|
||||
font-weight: var(--vscode-font-weight);
|
||||
font-size: var(--vscode-font-size);
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used for setting the background on the Storybook preview.
|
||||
*/
|
||||
body {
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export function Compare(_: Record<string, never>): JSX.Element {
|
||||
const hasRows = comparison.rows && (comparison.rows.to.length || comparison.rows.from.length);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', (evt: MessageEvent) => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToCompareViewMessage = evt.data;
|
||||
switch (msg.t) {
|
||||
@@ -43,8 +43,13 @@ export function Compare(_: Record<string, never>): JSX.Element {
|
||||
const origin = evt.origin.replace(/\n|\r/g, '');
|
||||
console.error(`Invalid event origin ${origin}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
window.addEventListener('message', listener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', listener);
|
||||
};
|
||||
}, []);
|
||||
if (!comparison) {
|
||||
return <div>Waiting for results to load.</div>;
|
||||
}
|
||||
|
||||
@@ -395,7 +395,7 @@ export function RemoteQueries(): JSX.Element {
|
||||
const [sort, setSort] = useState<Sort>('name');
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', (evt: MessageEvent) => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToRemoteQueriesMessage = evt.data;
|
||||
if (msg.t === 'setRemoteQueryResult') {
|
||||
@@ -408,8 +408,13 @@ export function RemoteQueries(): JSX.Element {
|
||||
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>;
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VSCodeBadge, VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react';
|
||||
import { isCompletedAnalysisRepoStatus, VariantAnalysisRepoStatus } from '../../remote-queries/shared/variant-analysis';
|
||||
import {
|
||||
isCompletedAnalysisRepoStatus,
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisScannedRepositoryDownloadStatus
|
||||
} from '../../remote-queries/shared/variant-analysis';
|
||||
import { formatDecimal } from '../../pure/number';
|
||||
import { Codicon, ErrorIcon, LoadingIcon, SuccessIcon, WarningIcon } from '../common';
|
||||
import { Repository } from '../../remote-queries/shared/repository';
|
||||
import { AnalysisAlert, AnalysisRawResults } from '../../remote-queries/shared/analysis-result';
|
||||
import { vscode } from '../vscode-api';
|
||||
import { AnalyzedRepoItemContent } from './AnalyzedRepoItemContent';
|
||||
|
||||
// This will ensure that these icons have a className which we can use in the TitleContainer
|
||||
@@ -62,6 +67,7 @@ export type RepoRowProps = {
|
||||
// Only fullName is required
|
||||
repository: Partial<Repository> & Pick<Repository, 'fullName'>;
|
||||
status?: VariantAnalysisRepoStatus;
|
||||
downloadStatus?: VariantAnalysisScannedRepositoryDownloadStatus;
|
||||
resultCount?: number;
|
||||
|
||||
interpretedResults?: AnalysisAlert[];
|
||||
@@ -71,17 +77,42 @@ export type RepoRowProps = {
|
||||
export const RepoRow = ({
|
||||
repository,
|
||||
status,
|
||||
downloadStatus,
|
||||
resultCount,
|
||||
interpretedResults,
|
||||
rawResults,
|
||||
}: RepoRowProps) => {
|
||||
const [isExpanded, setExpanded] = useState(false);
|
||||
const resultsLoaded = !!interpretedResults || !!rawResults;
|
||||
const [resultsLoading, setResultsLoading] = useState(false);
|
||||
|
||||
const toggleExpanded = useCallback(() => {
|
||||
setExpanded(oldIsExpanded => !oldIsExpanded);
|
||||
}, []);
|
||||
const toggleExpanded = useCallback(async () => {
|
||||
if (resultsLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resultsLoaded || status !== VariantAnalysisRepoStatus.Succeeded) {
|
||||
setExpanded(oldIsExpanded => !oldIsExpanded);
|
||||
return;
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
t: 'requestRepositoryResults',
|
||||
repositoryFullName: repository.fullName,
|
||||
});
|
||||
|
||||
setResultsLoading(true);
|
||||
}, [resultsLoading, resultsLoaded, repository.fullName, status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resultsLoaded && resultsLoading) {
|
||||
setResultsLoading(false);
|
||||
setExpanded(true);
|
||||
}
|
||||
}, [resultsLoaded, resultsLoading]);
|
||||
|
||||
const disabled = !status || !isCompletedAnalysisRepoStatus(status);
|
||||
const expandableContentLoaded = status && (status !== VariantAnalysisRepoStatus.Succeeded || resultsLoaded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -99,8 +130,9 @@ export const RepoRow = ({
|
||||
{status === VariantAnalysisRepoStatus.InProgress && <LoadingIcon label="In progress" />}
|
||||
{!status && <WarningIcon />}
|
||||
</span>
|
||||
{downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.InProgress && <LoadingIcon label="Downloading" />}
|
||||
</TitleContainer>
|
||||
{isExpanded && status &&
|
||||
{isExpanded && expandableContentLoaded &&
|
||||
<AnalyzedRepoItemContent status={status} interpretedResults={interpretedResults} rawResults={rawResults} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,207 +1,78 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { ToVariantAnalysisMessage } from '../../pure/interface-types';
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisDomainModel,
|
||||
VariantAnalysisQueryLanguage,
|
||||
VariantAnalysisRepoStatus, VariantAnalysisScannedRepositoryResult,
|
||||
VariantAnalysisStatus
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
VariantAnalysisScannedRepositoryState,
|
||||
} from '../../remote-queries/shared/variant-analysis';
|
||||
import { VariantAnalysisContainer } from './VariantAnalysisContainer';
|
||||
import { VariantAnalysisHeader } from './VariantAnalysisHeader';
|
||||
import { VariantAnalysisOutcomePanels } from './VariantAnalysisOutcomePanels';
|
||||
import { VariantAnalysisLoading } from './VariantAnalysisLoading';
|
||||
import { ToVariantAnalysisMessage } from '../../pure/interface-types';
|
||||
import { vscode } from '../vscode-api';
|
||||
|
||||
const variantAnalysis: VariantAnalysisDomainModel = {
|
||||
id: 1,
|
||||
controllerRepoId: 1,
|
||||
actionsWorkflowRunId: 789263,
|
||||
query: {
|
||||
name: 'Example query',
|
||||
filePath: 'example.ql',
|
||||
language: VariantAnalysisQueryLanguage.Javascript,
|
||||
},
|
||||
databases: {},
|
||||
status: VariantAnalysisStatus.InProgress,
|
||||
scannedRepos: [
|
||||
{
|
||||
repository: {
|
||||
id: 1,
|
||||
fullName: 'octodemo/hello-world-1',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 2,
|
||||
fullName: 'octodemo/hello-world-2',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Canceled,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 3,
|
||||
fullName: 'octodemo/hello-world-3',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.TimedOut,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 4,
|
||||
fullName: 'octodemo/hello-world-4',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Failed,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 5,
|
||||
fullName: 'octodemo/hello-world-5',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.InProgress,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 6,
|
||||
fullName: 'octodemo/hello-world-6',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.InProgress,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 7,
|
||||
fullName: 'octodemo/hello-world-7',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Pending,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 8,
|
||||
fullName: 'octodemo/hello-world-8',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Pending,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 9,
|
||||
fullName: 'octodemo/hello-world-9',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Pending,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 10,
|
||||
fullName: 'octodemo/hello-world-10',
|
||||
private: false,
|
||||
},
|
||||
analysisStatus: VariantAnalysisRepoStatus.Pending,
|
||||
},
|
||||
],
|
||||
skippedRepos: {
|
||||
notFoundRepos: {
|
||||
repositoryCount: 9999,
|
||||
repositories: [
|
||||
{
|
||||
fullName: 'octodemo/hello-globe'
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-planet'
|
||||
}
|
||||
]
|
||||
},
|
||||
noCodeqlDbRepos: {
|
||||
repositoryCount: 4,
|
||||
repositories: [
|
||||
{
|
||||
id: 100,
|
||||
fullName: 'octodemo/no-db-1',
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
id: 101,
|
||||
fullName: 'octodemo/no-db-2',
|
||||
private: true,
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
fullName: 'octodemo/no-db-3',
|
||||
private: true,
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
fullName: 'octodemo/no-db-4',
|
||||
private: false,
|
||||
}
|
||||
]
|
||||
},
|
||||
overLimitRepos: {
|
||||
repositoryCount: 1,
|
||||
repositories: [
|
||||
{
|
||||
id: 201,
|
||||
fullName: 'octodemo/over-limit-1'
|
||||
}
|
||||
]
|
||||
},
|
||||
accessMismatchRepos: {
|
||||
repositoryCount: 1,
|
||||
repositories: [
|
||||
{
|
||||
id: 205,
|
||||
fullName: 'octodemo/private'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
type Props = {
|
||||
variantAnalysis?: VariantAnalysisDomainModel;
|
||||
repoStates?: VariantAnalysisScannedRepositoryState[];
|
||||
repoResults?: VariantAnalysisScannedRepositoryResult[];
|
||||
}
|
||||
|
||||
const openQueryFile = () => {
|
||||
vscode.postMessage({
|
||||
t: 'openQueryFile',
|
||||
});
|
||||
};
|
||||
|
||||
const repositoryResults: VariantAnalysisScannedRepositoryResult[] = [
|
||||
{
|
||||
repositoryId: 1,
|
||||
rawResults: {
|
||||
schema: {
|
||||
name: '#select',
|
||||
rows: 1,
|
||||
columns: [
|
||||
{
|
||||
kind: 'i'
|
||||
}
|
||||
]
|
||||
},
|
||||
resultSet: {
|
||||
schema: {
|
||||
name: '#select',
|
||||
rows: 1,
|
||||
columns: [
|
||||
{
|
||||
kind: 'i'
|
||||
}
|
||||
]
|
||||
},
|
||||
rows: [
|
||||
[
|
||||
60688
|
||||
]
|
||||
]
|
||||
},
|
||||
fileLinkPrefix: 'https://github.com/octodemo/hello-world-1/blob/59a2a6c7d9dde7a6ecb77c2f7e8197d6925c143b',
|
||||
sourceLocationPrefix: '/home/runner/work/bulk-builder/bulk-builder',
|
||||
capped: false
|
||||
}
|
||||
}
|
||||
];
|
||||
const openQueryText = () => {
|
||||
vscode.postMessage({
|
||||
t: 'openQueryText',
|
||||
});
|
||||
};
|
||||
|
||||
function getContainerContents(variantAnalysis: VariantAnalysisDomainModel) {
|
||||
if (variantAnalysis.actionsWorkflowRunId === undefined) {
|
||||
export function VariantAnalysis({
|
||||
variantAnalysis: initialVariantAnalysis,
|
||||
repoStates: initialRepoStates = [],
|
||||
repoResults: initialRepoResults = [],
|
||||
}: Props): JSX.Element {
|
||||
const [variantAnalysis, setVariantAnalysis] = useState<VariantAnalysisDomainModel | undefined>(initialVariantAnalysis);
|
||||
const [repoStates, setRepoStates] = useState<VariantAnalysisScannedRepositoryState[]>(initialRepoStates);
|
||||
const [repoResults, setRepoResults] = useState<VariantAnalysisScannedRepositoryResult[]>(initialRepoResults);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToVariantAnalysisMessage = evt.data;
|
||||
if (msg.t === 'setVariantAnalysis') {
|
||||
setVariantAnalysis(msg.variantAnalysis);
|
||||
vscode.setState({
|
||||
variantAnalysisId: msg.variantAnalysis.id,
|
||||
});
|
||||
} else if (msg.t === 'setRepoResults') {
|
||||
setRepoResults(oldRepoResults => {
|
||||
const newRepoIds = msg.repoResults.map(r => r.repositoryId);
|
||||
return [...oldRepoResults.filter(v => !newRepoIds.includes(v.repositoryId)), ...msg.repoResults];
|
||||
});
|
||||
} else if (msg.t === 'setRepoStates') {
|
||||
setRepoStates(oldRepoStates => {
|
||||
const newRepoIds = msg.repoStates.map(r => r.repositoryId);
|
||||
return [...oldRepoStates.filter(v => !newRepoIds.includes(v.repositoryId)), ...msg.repoStates];
|
||||
});
|
||||
}
|
||||
} 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 (variantAnalysis?.actionsWorkflowRunId === undefined) {
|
||||
return <VariantAnalysisLoading />;
|
||||
}
|
||||
|
||||
@@ -209,8 +80,8 @@ function getContainerContents(variantAnalysis: VariantAnalysisDomainModel) {
|
||||
<>
|
||||
<VariantAnalysisHeader
|
||||
variantAnalysis={variantAnalysis}
|
||||
onOpenQueryFileClick={() => console.log('Open query')}
|
||||
onViewQueryTextClick={() => console.log('View query')}
|
||||
onOpenQueryFileClick={openQueryFile}
|
||||
onViewQueryTextClick={openQueryText}
|
||||
onStopQueryClick={() => console.log('Stop query')}
|
||||
onCopyRepositoryListClick={() => console.log('Copy repository list')}
|
||||
onExportResultsClick={() => console.log('Export results')}
|
||||
@@ -218,39 +89,9 @@ function getContainerContents(variantAnalysis: VariantAnalysisDomainModel) {
|
||||
/>
|
||||
<VariantAnalysisOutcomePanels
|
||||
variantAnalysis={variantAnalysis}
|
||||
repositoryResults={repositoryResults}
|
||||
repositoryStates={repoStates}
|
||||
repositoryResults={repoResults}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
variantAnalysis?: VariantAnalysisDomainModel;
|
||||
}
|
||||
|
||||
export function VariantAnalysis({
|
||||
variantAnalysis: initialVariantAnalysis = variantAnalysis,
|
||||
}: Props): JSX.Element {
|
||||
const [variantAnalysis, setVariantAnalysis] = useState<VariantAnalysisDomainModel>(initialVariantAnalysis);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToVariantAnalysisMessage = evt.data;
|
||||
if (msg.t === 'setVariantAnalysis') {
|
||||
setVariantAnalysis(msg.variantAnalysis);
|
||||
}
|
||||
} else {
|
||||
// sanitize origin
|
||||
const origin = evt.origin.replace(/\n|\r/g, '');
|
||||
console.error(`Invalid event origin ${origin}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<VariantAnalysisContainer>
|
||||
{getContainerContents(variantAnalysis)}
|
||||
</VariantAnalysisContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VariantAnalysis, VariantAnalysisScannedRepositoryResult } from '../../remote-queries/shared/variant-analysis';
|
||||
import { RepoRow } from './RepoRow';
|
||||
import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
VariantAnalysisScannedRepositoryState
|
||||
} from '../../remote-queries/shared/variant-analysis';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const Container = styled.div`
|
||||
@@ -12,13 +16,23 @@ const Container = styled.div`
|
||||
|
||||
export type VariantAnalysisAnalyzedReposProps = {
|
||||
variantAnalysis: VariantAnalysis;
|
||||
repositoryStates?: VariantAnalysisScannedRepositoryState[];
|
||||
repositoryResults?: VariantAnalysisScannedRepositoryResult[];
|
||||
}
|
||||
|
||||
export const VariantAnalysisAnalyzedRepos = ({
|
||||
variantAnalysis,
|
||||
repositoryStates,
|
||||
repositoryResults,
|
||||
}: VariantAnalysisAnalyzedReposProps) => {
|
||||
const repositoryStateById = useMemo(() => {
|
||||
const map = new Map<number, VariantAnalysisScannedRepositoryState>();
|
||||
repositoryStates?.forEach((repository) => {
|
||||
map.set(repository.repositoryId, repository);
|
||||
});
|
||||
return map;
|
||||
}, [repositoryStates]);
|
||||
|
||||
const repositoryResultsById = useMemo(() => {
|
||||
const map = new Map<number, VariantAnalysisScannedRepositoryResult>();
|
||||
repositoryResults?.forEach((repository) => {
|
||||
@@ -30,6 +44,7 @@ export const VariantAnalysisAnalyzedRepos = ({
|
||||
return (
|
||||
<Container>
|
||||
{variantAnalysis.scannedRepos?.map(repository => {
|
||||
const state = repositoryStateById.get(repository.repository.id);
|
||||
const results = repositoryResultsById.get(repository.repository.id);
|
||||
|
||||
return (
|
||||
@@ -37,6 +52,7 @@ export const VariantAnalysisAnalyzedRepos = ({
|
||||
key={repository.repository.id}
|
||||
repository={repository.repository}
|
||||
status={repository.analysisStatus}
|
||||
downloadStatus={state?.downloadStatus}
|
||||
resultCount={repository.resultCount}
|
||||
interpretedResults={results?.interpretedResults}
|
||||
rawResults={results?.rawResults}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
@@ -9,13 +10,11 @@ import {
|
||||
import { QueryDetails } from './QueryDetails';
|
||||
import { VariantAnalysisActions } from './VariantAnalysisActions';
|
||||
import { VariantAnalysisStats } from './VariantAnalysisStats';
|
||||
import { parseDate } from '../../pure/date';
|
||||
|
||||
export type VariantAnalysisHeaderProps = {
|
||||
variantAnalysis: VariantAnalysis;
|
||||
|
||||
duration?: number | undefined;
|
||||
completedAt?: Date | undefined;
|
||||
|
||||
onOpenQueryFileClick: () => void;
|
||||
onViewQueryTextClick: () => void;
|
||||
|
||||
@@ -40,8 +39,6 @@ const Row = styled.div`
|
||||
|
||||
export const VariantAnalysisHeader = ({
|
||||
variantAnalysis,
|
||||
duration,
|
||||
completedAt,
|
||||
onOpenQueryFileClick,
|
||||
onViewQueryTextClick,
|
||||
onStopQueryClick,
|
||||
@@ -67,7 +64,7 @@ export const VariantAnalysisHeader = ({
|
||||
<Row>
|
||||
<QueryDetails
|
||||
queryName={variantAnalysis.query.name}
|
||||
queryFileName={variantAnalysis.query.filePath}
|
||||
queryFileName={path.basename(variantAnalysis.query.filePath)}
|
||||
onOpenQueryFileClick={onOpenQueryFileClick}
|
||||
onViewQueryTextClick={onViewQueryTextClick}
|
||||
/>
|
||||
@@ -84,8 +81,8 @@ export const VariantAnalysisHeader = ({
|
||||
completedRepositoryCount={completedRepositoryCount}
|
||||
resultCount={resultCount}
|
||||
hasWarnings={hasSkippedRepos}
|
||||
duration={duration}
|
||||
completedAt={completedAt}
|
||||
createdAt={parseDate(variantAnalysis.createdAt)}
|
||||
completedAt={parseDate(variantAnalysis.completedAt)}
|
||||
onViewLogsClick={onViewLogsClick}
|
||||
/>
|
||||
</Container>
|
||||
|
||||
@@ -2,13 +2,18 @@ import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VSCodeBadge, VSCodePanels, VSCodePanelTab, VSCodePanelView } from '@vscode/webview-ui-toolkit/react';
|
||||
import { formatDecimal } from '../../pure/number';
|
||||
import { VariantAnalysis, VariantAnalysisScannedRepositoryResult } from '../../remote-queries/shared/variant-analysis';
|
||||
import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
VariantAnalysisScannedRepositoryState
|
||||
} from '../../remote-queries/shared/variant-analysis';
|
||||
import { VariantAnalysisAnalyzedRepos } from './VariantAnalysisAnalyzedRepos';
|
||||
import { Alert } from '../common';
|
||||
import { VariantAnalysisSkippedRepositoriesTab } from './VariantAnalysisSkippedRepositoriesTab';
|
||||
|
||||
export type VariantAnalysisOutcomePanelProps = {
|
||||
variantAnalysis: VariantAnalysis;
|
||||
repositoryStates?: VariantAnalysisScannedRepositoryState[];
|
||||
repositoryResults?: VariantAnalysisScannedRepositoryResult[];
|
||||
};
|
||||
|
||||
@@ -34,6 +39,7 @@ const WarningsContainer = styled.div`
|
||||
|
||||
export const VariantAnalysisOutcomePanels = ({
|
||||
variantAnalysis,
|
||||
repositoryStates,
|
||||
repositoryResults,
|
||||
}: VariantAnalysisOutcomePanelProps) => {
|
||||
const noCodeqlDbRepos = variantAnalysis.skippedRepos?.noCodeqlDbRepos;
|
||||
@@ -64,7 +70,11 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
return (
|
||||
<>
|
||||
{warnings}
|
||||
<VariantAnalysisAnalyzedRepos variantAnalysis={variantAnalysis} repositoryResults={repositoryResults} />
|
||||
<VariantAnalysisAnalyzedRepos
|
||||
variantAnalysis={variantAnalysis}
|
||||
repositoryStates={repositoryStates}
|
||||
repositoryResults={repositoryResults}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -89,7 +99,13 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
<VSCodeBadge appearance="secondary">{formatDecimal(noCodeqlDbRepos.repositoryCount)}</VSCodeBadge>
|
||||
</Tab>
|
||||
)}
|
||||
<VSCodePanelView><VariantAnalysisAnalyzedRepos variantAnalysis={variantAnalysis} repositoryResults={repositoryResults} /></VSCodePanelView>
|
||||
<VSCodePanelView>
|
||||
<VariantAnalysisAnalyzedRepos
|
||||
variantAnalysis={variantAnalysis}
|
||||
repositoryStates={repositoryStates}
|
||||
repositoryResults={repositoryResults}
|
||||
/>
|
||||
</VSCodePanelView>
|
||||
{notFoundRepos?.repositoryCount &&
|
||||
<VSCodePanelView>
|
||||
<VariantAnalysisSkippedRepositoriesTab
|
||||
|
||||
@@ -17,7 +17,7 @@ export type VariantAnalysisStatsProps = {
|
||||
hasWarnings?: boolean;
|
||||
|
||||
resultCount?: number | undefined;
|
||||
duration?: number | undefined;
|
||||
createdAt: Date;
|
||||
completedAt?: Date | undefined;
|
||||
|
||||
onViewLogsClick: () => void;
|
||||
@@ -35,7 +35,7 @@ export const VariantAnalysisStats = ({
|
||||
completedRepositoryCount = 0,
|
||||
hasWarnings,
|
||||
resultCount,
|
||||
duration,
|
||||
createdAt,
|
||||
completedAt,
|
||||
onViewLogsClick,
|
||||
}: VariantAnalysisStatsProps) => {
|
||||
@@ -59,6 +59,14 @@ export const VariantAnalysisStats = ({
|
||||
return 'Succeeded';
|
||||
}, [variantAnalysisStatus, hasWarnings]);
|
||||
|
||||
const duration = useMemo(() => {
|
||||
if (!completedAt) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return completedAt.getTime() - createdAt.getTime();
|
||||
}, [completedAt, createdAt]);
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<StatItem title="Results">
|
||||
|
||||
@@ -164,6 +164,56 @@ describe(RepoRow.name, () => {
|
||||
screen.getByText('Error: Timed out');
|
||||
});
|
||||
|
||||
it('can expand the repo item when succeeded and loaded', async () => {
|
||||
render({
|
||||
status: VariantAnalysisRepoStatus.Succeeded,
|
||||
interpretedResults: [],
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole('button', {
|
||||
expanded: false
|
||||
}));
|
||||
|
||||
expect(screen.getByRole('button', {
|
||||
expanded: true,
|
||||
})).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can expand the repo item when succeeded and not loaded', async () => {
|
||||
const { rerender } = render({
|
||||
status: VariantAnalysisRepoStatus.Succeeded,
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole('button', {
|
||||
expanded: false
|
||||
}));
|
||||
|
||||
expect((window as any).vsCodeApi.postMessage).toHaveBeenCalledWith({
|
||||
t: 'requestRepositoryResults',
|
||||
repositoryFullName: 'octodemo/hello-world-1',
|
||||
});
|
||||
|
||||
expect(screen.getByRole('button', {
|
||||
expanded: false,
|
||||
})).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<RepoRow
|
||||
repository={{
|
||||
id: 1,
|
||||
fullName: 'octodemo/hello-world-1',
|
||||
private: false,
|
||||
}}
|
||||
status={VariantAnalysisRepoStatus.Succeeded}
|
||||
interpretedResults={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', {
|
||||
expanded: true,
|
||||
})).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not allow expanding the repo item when status is undefined', async () => {
|
||||
render({
|
||||
status: undefined,
|
||||
|
||||
@@ -17,8 +17,12 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
name: 'Example query',
|
||||
filePath: 'example.ql',
|
||||
language: VariantAnalysisQueryLanguage.Javascript,
|
||||
text: 'import javascript\nselect 1',
|
||||
},
|
||||
databases: {},
|
||||
executionStartTime: 1611234567890,
|
||||
createdAt: '2021-01-21T13:09:27.890Z',
|
||||
updatedAt: '2021-01-21T13:09:27.890Z',
|
||||
status: VariantAnalysisStatus.InProgress,
|
||||
scannedRepos: [
|
||||
{
|
||||
@@ -78,6 +82,7 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
render({
|
||||
repositoryResults: [
|
||||
{
|
||||
variantAnalysisId: 1,
|
||||
repositoryId: 2,
|
||||
interpretedResults: [
|
||||
{
|
||||
|
||||
@@ -16,8 +16,12 @@ describe(VariantAnalysisOutcomePanels.name, () => {
|
||||
name: 'Example query',
|
||||
filePath: 'example.ql',
|
||||
language: VariantAnalysisQueryLanguage.Javascript,
|
||||
text: 'import javascript\nselect 1',
|
||||
},
|
||||
databases: {},
|
||||
executionStartTime: 1611234567890,
|
||||
createdAt: '2021-01-21T13:09:27.890Z',
|
||||
updatedAt: '2021-01-21T13:09:27.890Z',
|
||||
status: VariantAnalysisStatus.InProgress,
|
||||
scannedRepos: [
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ describe(VariantAnalysisStats.name, () => {
|
||||
variantAnalysisStatus={VariantAnalysisStatus.InProgress}
|
||||
totalRepositoryCount={10}
|
||||
onViewLogsClick={onViewLogsClick}
|
||||
createdAt={new Date()}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -31,6 +32,7 @@ describe(VariantAnalysisStats.name, () => {
|
||||
render({ resultCount: 123456 });
|
||||
|
||||
expect(screen.getByText('123,456')).toBeInTheDocument();
|
||||
expect(screen.queryAllByText('-').length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders the number of repositories as a formatted number', () => {
|
||||
@@ -100,4 +102,30 @@ describe(VariantAnalysisStats.name, () => {
|
||||
expect(screen.getByText('Succeeded')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Succeeded warnings')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render the duration when the completedAt is not set', () => {
|
||||
render({ createdAt: new Date('2021-05-01T00:00:00Z') });
|
||||
|
||||
expect(screen.queryAllByText('-').length).toBe(2);
|
||||
expect(screen.queryByText('Less than a second')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the duration when it is less than a second', () => {
|
||||
render({ createdAt: new Date('2021-05-01T00:00:00Z'), completedAt: new Date('2021-05-01T00:00:00Z') });
|
||||
|
||||
expect(screen.getByText('Less than a second')).toBeInTheDocument();
|
||||
expect(screen.queryAllByText('-').length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders the duration when it is less than a minute', () => {
|
||||
render({ createdAt: new Date('2021-05-01T00:00:00Z'), completedAt: new Date('2021-05-01T00:00:34Z') });
|
||||
|
||||
expect(screen.getByText('34 seconds')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the duration when it is more than a minute', () => {
|
||||
render({ createdAt: new Date('2021-05-01T00:00:00Z'), completedAt: new Date('2021-05-01T00:10:22Z') });
|
||||
|
||||
expect(screen.getByText('10 minutes')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { FromCompareViewMessage, FromRemoteQueriesMessage, FromResultsViewMsg } from '../pure/interface-types';
|
||||
import {
|
||||
FromCompareViewMessage,
|
||||
FromRemoteQueriesMessage,
|
||||
FromResultsViewMsg,
|
||||
FromVariantAnalysisMessage, VariantAnalysisState
|
||||
} from '../pure/interface-types';
|
||||
|
||||
export interface VsCodeApi {
|
||||
/**
|
||||
* Post message back to vscode extension.
|
||||
*/
|
||||
postMessage(msg: FromResultsViewMsg | FromCompareViewMessage | FromRemoteQueriesMessage): void;
|
||||
postMessage(msg: FromResultsViewMsg | FromCompareViewMessage | FromRemoteQueriesMessage | FromVariantAnalysisMessage): void;
|
||||
|
||||
/**
|
||||
* Set state of the webview.
|
||||
*/
|
||||
setState(state: VariantAnalysisState): void;
|
||||
}
|
||||
|
||||
declare const acquireVsCodeApi: () => VsCodeApi;
|
||||
|
||||
Binary file not shown.
@@ -4,9 +4,11 @@ import * as fs from 'fs-extra';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import { fail } from 'assert';
|
||||
import { commands, ConfigurationTarget, extensions, workspace } from 'vscode';
|
||||
import { commands, extensions, workspace } from 'vscode';
|
||||
import { CodeQLExtensionInterface } from '../../extension';
|
||||
import { DatabaseManager } from '../../databases';
|
||||
import { getTestSetting } from '../test-config';
|
||||
import { CUSTOM_CODEQL_PATH_SETTING } from '../../config';
|
||||
|
||||
// This file contains helpers shared between actual tests.
|
||||
|
||||
@@ -58,7 +60,7 @@ export default function(mocha: Mocha) {
|
||||
// Set the CLI version here before activation to ensure we don't accidentally try to download a cli
|
||||
(mocha.options as any).globalSetup.push(
|
||||
async () => {
|
||||
await workspace.getConfiguration().update('codeQL.cli.executablePath', process.env.CLI_PATH, ConfigurationTarget.Global);
|
||||
await getTestSetting(CUSTOM_CODEQL_PATH_SETTING)?.setInitialTestValue(process.env.CLI_PATH);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
import { expect } from 'chai';
|
||||
import * as path from 'path';
|
||||
import * as tmp from 'tmp';
|
||||
import { CancellationTokenSource } from 'vscode-jsonrpc';
|
||||
import * as messages from '../../pure/new-messages';
|
||||
import * as qsClient from '../../query-server/queryserver-client';
|
||||
import * as cli from '../../cli';
|
||||
import { CellValue } from '../../pure/bqrs-cli-types';
|
||||
import { extensions, Uri } from 'vscode';
|
||||
import { CodeQLExtensionInterface } from '../../extension';
|
||||
import { fail } from 'assert';
|
||||
import { skipIfNoCodeQL } from '../ensureCli';
|
||||
import { QueryServerClient } from '../../query-server/queryserver-client';
|
||||
import { logger, ProgressReporter } from '../../logging';
|
||||
import { QueryResultType } from '../../pure/new-messages';
|
||||
import { cleanDatabases, dbLoc, storagePath } from './global.helper';
|
||||
import { importArchiveDatabase } from '../../databaseFetcher';
|
||||
|
||||
|
||||
const baseDir = path.join(__dirname, '../../../test/data');
|
||||
|
||||
const tmpDir = tmp.dirSync({ prefix: 'query_test_', keep: false, unsafeCleanup: true });
|
||||
|
||||
const RESULTS_PATH = path.join(tmpDir.name, 'results.bqrs');
|
||||
|
||||
const source = new CancellationTokenSource();
|
||||
const token = source.token;
|
||||
|
||||
class Checkpoint<T> {
|
||||
private res: () => void;
|
||||
private rej: (e: Error) => void;
|
||||
private promise: Promise<T>;
|
||||
|
||||
constructor() {
|
||||
this.res = () => { /**/ };
|
||||
this.rej = () => { /**/ };
|
||||
this.promise = new Promise((res, rej) => {
|
||||
this.res = res as () => Record<string, never>;
|
||||
this.rej = rej;
|
||||
});
|
||||
}
|
||||
|
||||
async done(): Promise<T> {
|
||||
return this.promise;
|
||||
}
|
||||
|
||||
async resolve(): Promise<void> {
|
||||
await (this.res)();
|
||||
}
|
||||
|
||||
async reject(e: Error): Promise<void> {
|
||||
await (this.rej)(e);
|
||||
}
|
||||
}
|
||||
|
||||
type ResultSets = {
|
||||
[name: string]: CellValue[][];
|
||||
}
|
||||
|
||||
type QueryTestCase = {
|
||||
queryPath: string;
|
||||
expectedResultSets: ResultSets;
|
||||
}
|
||||
|
||||
// Test cases: queries to run and their expected results.
|
||||
const queryTestCases: QueryTestCase[] = [
|
||||
{
|
||||
queryPath: path.join(baseDir, 'query.ql'),
|
||||
expectedResultSets: {
|
||||
'#select': [[42, 3.14159, 'hello world', true]]
|
||||
}
|
||||
},
|
||||
{
|
||||
queryPath: path.join(baseDir, 'compute-default-strings.ql'),
|
||||
expectedResultSets: {
|
||||
'#select': [[{ label: '(no string representation)' }]]
|
||||
}
|
||||
},
|
||||
{
|
||||
queryPath: path.join(baseDir, 'multiple-result-sets.ql'),
|
||||
expectedResultSets: {
|
||||
'edges': [[1, 2], [2, 3]],
|
||||
'#select': [['s']]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const nullProgressReporter: ProgressReporter = { report: () => { /** ignore */ } };
|
||||
|
||||
describe('using the new query server', function() {
|
||||
before(function() {
|
||||
skipIfNoCodeQL(this);
|
||||
});
|
||||
|
||||
// Note this does not work with arrow functions as the test case bodies:
|
||||
// ensure they are all written with standard anonymous functions.
|
||||
this.timeout(20000);
|
||||
|
||||
let qs: qsClient.QueryServerClient;
|
||||
let cliServer: cli.CodeQLCliServer;
|
||||
let db: string;
|
||||
before(async () => {
|
||||
try {
|
||||
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
|
||||
if ('cliServer' in extension && 'databaseManager' in extension) {
|
||||
cliServer = extension.cliServer;
|
||||
|
||||
cliServer.quiet = true;
|
||||
if (!(await cliServer.cliConstraints.supportsNewQueryServer())) {
|
||||
this.ctx.skip();
|
||||
}
|
||||
qs = new QueryServerClient({
|
||||
codeQlPath: (await extension.distributionManager.getCodeQlPathWithoutVersionCheck()) || '',
|
||||
debug: false,
|
||||
cacheSize: 0,
|
||||
numThreads: 1,
|
||||
saveCache: false,
|
||||
timeoutSecs: 0
|
||||
}, cliServer, {
|
||||
contextStoragePath: tmpDir.name,
|
||||
logger
|
||||
}, task => task(nullProgressReporter, new CancellationTokenSource().token));
|
||||
await qs.startQueryServer();
|
||||
|
||||
// Unlike the old query sevre the new one wants a database and the empty direcrtory is not valid.
|
||||
// Add a database, but make sure the database manager is empty first
|
||||
await cleanDatabases(extension.databaseManager);
|
||||
const uri = Uri.file(dbLoc);
|
||||
const maybeDbItem = await importArchiveDatabase(
|
||||
uri.toString(true),
|
||||
extension.databaseManager,
|
||||
storagePath,
|
||||
() => { /**ignore progress */ },
|
||||
token,
|
||||
);
|
||||
|
||||
if (!maybeDbItem) {
|
||||
throw new Error('Could not import database');
|
||||
}
|
||||
db = maybeDbItem.databaseUri.fsPath;
|
||||
} else {
|
||||
throw new Error('Extension not initialized. Make sure cli is downloaded and installed properly.');
|
||||
}
|
||||
} catch (e) {
|
||||
fail(e as Error);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
for (const queryTestCase of queryTestCases) {
|
||||
const queryName = path.basename(queryTestCase.queryPath);
|
||||
const evaluationSucceeded = new Checkpoint<void>();
|
||||
const parsedResults = new Checkpoint<void>();
|
||||
|
||||
it('should register the database', async () => {
|
||||
await qs.sendRequest(messages.registerDatabases, { databases: [db] }, token, (() => { /**/ }) as any);
|
||||
});
|
||||
|
||||
|
||||
it(`should be able to run query ${queryName}`, async function() {
|
||||
try {
|
||||
const params: messages.RunQueryParams = {
|
||||
db,
|
||||
queryPath: queryTestCase.queryPath,
|
||||
outputPath: RESULTS_PATH,
|
||||
additionalPacks: [],
|
||||
externalInputs: {},
|
||||
singletonExternalInputs: {},
|
||||
target: { query: {} }
|
||||
};
|
||||
const result = await qs.sendRequest(messages.runQuery, params, token, () => { /**/ });
|
||||
expect(result.resultType).to.equal(QueryResultType.SUCCESS);
|
||||
await evaluationSucceeded.resolve();
|
||||
}
|
||||
catch (e) {
|
||||
await evaluationSucceeded.reject(e as Error);
|
||||
}
|
||||
});
|
||||
|
||||
const actualResultSets: ResultSets = {};
|
||||
it(`should be able to parse results of query ${queryName}`, async function() {
|
||||
await evaluationSucceeded.done();
|
||||
const info = await cliServer.bqrsInfo(RESULTS_PATH);
|
||||
|
||||
for (const resultSet of info['result-sets']) {
|
||||
const decoded = await cliServer.bqrsDecode(RESULTS_PATH, resultSet.name);
|
||||
actualResultSets[resultSet.name] = decoded.tuples;
|
||||
}
|
||||
await parsedResults.resolve();
|
||||
});
|
||||
|
||||
it(`should have correct results for query ${queryName}`, async function() {
|
||||
await parsedResults.done();
|
||||
expect(actualResultSets!).not.to.be.empty;
|
||||
expect(Object.keys(actualResultSets!).sort()).to.eql(Object.keys(queryTestCase.expectedResultSets).sort());
|
||||
for (const name in queryTestCase.expectedResultSets) {
|
||||
expect(actualResultSets![name]).to.eql(queryTestCase.expectedResultSets[name], `Results for query predicate ${name} do not match`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -112,7 +112,7 @@ describe('Queries', function() {
|
||||
);
|
||||
|
||||
// just check that the query was successful
|
||||
expect((await result).sucessful).to.eq(true);
|
||||
expect((await result).successful).to.eq(true);
|
||||
} catch (e) {
|
||||
console.error('Test Failed');
|
||||
fail(e as Error);
|
||||
@@ -132,7 +132,7 @@ describe('Queries', function() {
|
||||
token
|
||||
);
|
||||
|
||||
expect(result.sucessful).to.eq(true);
|
||||
expect(result.successful).to.eq(true);
|
||||
} catch (e) {
|
||||
console.error('Test Failed');
|
||||
fail(e as Error);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { assert, expect } from 'chai';
|
||||
import * as path from 'path';
|
||||
import * as sinon from 'sinon';
|
||||
import { CancellationTokenSource, extensions, QuickPickItem, Uri, window } from 'vscode';
|
||||
import { CancellationTokenSource, ExtensionContext, extensions, QuickPickItem, Uri, window } from 'vscode';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import * as yaml from 'js-yaml';
|
||||
@@ -21,6 +21,9 @@ import {
|
||||
import { Repository } from '../../../remote-queries/gh-api/repository';
|
||||
import { VariantAnalysisStatus } from '../../../remote-queries/shared/variant-analysis';
|
||||
import { createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
|
||||
import { createMockExtensionContext } from '../../no-workspace';
|
||||
import { VariantAnalysisManager } from '../../../remote-queries/variant-analysis-manager';
|
||||
import { OutputChannelLogger } from '../../../logging';
|
||||
|
||||
describe('Remote queries', function() {
|
||||
const baseDir = path.join(__dirname, '../../../../src/vscode-tests/cli-integration');
|
||||
@@ -37,6 +40,9 @@ describe('Remote queries', function() {
|
||||
let showQuickPickSpy: sinon.SinonStub;
|
||||
let getRepositoryFromNwoStub: sinon.SinonStub;
|
||||
let liveResultsStub: sinon.SinonStub;
|
||||
let ctx: ExtensionContext;
|
||||
let logger: any;
|
||||
let variantAnalysisManager: VariantAnalysisManager;
|
||||
|
||||
// use `function` so we have access to `this`
|
||||
beforeEach(async function() {
|
||||
@@ -49,6 +55,10 @@ describe('Remote queries', function() {
|
||||
throw new Error('Extension not initialized. Make sure cli is downloaded and installed properly.');
|
||||
}
|
||||
|
||||
ctx = createMockExtensionContext();
|
||||
logger = new OutputChannelLogger('test-logger');
|
||||
variantAnalysisManager = new VariantAnalysisManager(ctx, cli, 'fake-storage-dir', logger);
|
||||
|
||||
if (!(await cli.cliConstraints.supportsRemoteQueries())) {
|
||||
console.log(`Remote queries are not supported on CodeQL CLI v${CliVersionConstraint.CLI_VERSION_REMOTE_QUERIES
|
||||
}. Skipping this test.`);
|
||||
@@ -56,14 +66,7 @@ describe('Remote queries', function() {
|
||||
}
|
||||
credentials = {} as unknown as Credentials;
|
||||
|
||||
cancellationTokenSource = {
|
||||
token: {
|
||||
isCancellationRequested: false,
|
||||
onCancellationRequested: sandbox.stub()
|
||||
},
|
||||
cancel: sandbox.stub(),
|
||||
dispose: sandbox.stub()
|
||||
};
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
progress = sandbox.spy();
|
||||
// Should not have asked for a language
|
||||
@@ -94,7 +97,7 @@ describe('Remote queries', function() {
|
||||
it('should run a remote query that is part of a qlpack', async () => {
|
||||
const fileUri = getFile('data-remote-qlpack/in-pack.ql');
|
||||
|
||||
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token);
|
||||
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager);
|
||||
expect(querySubmissionResult).to.be.ok;
|
||||
const queryPackRootDir = querySubmissionResult!.queryDirPath!;
|
||||
printDirectoryContents(queryPackRootDir);
|
||||
@@ -155,7 +158,7 @@ describe('Remote queries', function() {
|
||||
it('should run a remote query that is not part of a qlpack', async () => {
|
||||
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
|
||||
|
||||
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token);
|
||||
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager);
|
||||
expect(querySubmissionResult).to.be.ok;
|
||||
const queryPackRootDir = querySubmissionResult!.queryDirPath!;
|
||||
|
||||
@@ -218,7 +221,7 @@ describe('Remote queries', function() {
|
||||
it('should run a remote query that is nested inside a qlpack', async () => {
|
||||
const fileUri = getFile('data-remote-qlpack-nested/subfolder/in-pack.ql');
|
||||
|
||||
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token);
|
||||
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager);
|
||||
expect(querySubmissionResult).to.be.ok;
|
||||
const queryPackRootDir = querySubmissionResult!.queryDirPath!;
|
||||
|
||||
@@ -280,9 +283,9 @@ describe('Remote queries', function() {
|
||||
it('should cancel a run before uploading', async () => {
|
||||
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
|
||||
|
||||
const promise = runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token);
|
||||
const promise = runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager);
|
||||
|
||||
cancellationTokenSource.token.isCancellationRequested = true;
|
||||
cancellationTokenSource.cancel();
|
||||
|
||||
try {
|
||||
await promise;
|
||||
@@ -306,7 +309,7 @@ describe('Remote queries', function() {
|
||||
it('should run a variant analysis that is part of a qlpack', async () => {
|
||||
const fileUri = getFile('data-remote-qlpack/in-pack.ql');
|
||||
|
||||
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token);
|
||||
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager);
|
||||
expect(querySubmissionResult).to.be.ok;
|
||||
const variantAnalysis = querySubmissionResult!.variantAnalysis!;
|
||||
expect(variantAnalysis.id).to.be.equal(mockApiResponse.id);
|
||||
@@ -319,7 +322,7 @@ describe('Remote queries', function() {
|
||||
it('should run a remote query that is not part of a qlpack', async () => {
|
||||
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
|
||||
|
||||
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token);
|
||||
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager);
|
||||
expect(querySubmissionResult).to.be.ok;
|
||||
const variantAnalysis = querySubmissionResult!.variantAnalysis!;
|
||||
expect(variantAnalysis.id).to.be.equal(mockApiResponse.id);
|
||||
@@ -332,7 +335,7 @@ describe('Remote queries', function() {
|
||||
it('should run a remote query that is nested inside a qlpack', async () => {
|
||||
const fileUri = getFile('data-remote-qlpack-nested/subfolder/in-pack.ql');
|
||||
|
||||
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token);
|
||||
const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager);
|
||||
expect(querySubmissionResult).to.be.ok;
|
||||
const variantAnalysis = querySubmissionResult!.variantAnalysis!;
|
||||
expect(variantAnalysis.id).to.be.equal(mockApiResponse.id);
|
||||
@@ -345,9 +348,9 @@ describe('Remote queries', function() {
|
||||
it('should cancel a run before uploading', async () => {
|
||||
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
|
||||
|
||||
const promise = runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token);
|
||||
const promise = runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager);
|
||||
|
||||
cancellationTokenSource.token.isCancellationRequested = true;
|
||||
cancellationTokenSource.cancel();
|
||||
|
||||
try {
|
||||
await promise;
|
||||
|
||||
@@ -7,18 +7,23 @@ import * as config from '../../../config';
|
||||
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
|
||||
import { Credentials } from '../../../authentication';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { VariantAnalysisManager } from '../../../remote-queries/variant-analysis-manager';
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisApiResponse,
|
||||
VariantAnalysisRepoTask,
|
||||
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository
|
||||
} from '../../../remote-queries/gh-api/variant-analysis';
|
||||
import { createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
|
||||
import { createMockScannedRepos } from '../../factories/remote-queries/gh-api/scanned-repositories';
|
||||
import { createMockVariantAnalysisRepoTask } from '../../factories/remote-queries/gh-api/variant-analysis-repo-task';
|
||||
import { CodeQLCliServer } from '../../../cli';
|
||||
import { storagePath } from '../global.helper';
|
||||
|
||||
describe('Variant Analysis Manager', async function() {
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
let cli: CodeQLCliServer;
|
||||
let cancellationTokenSource: CancellationTokenSource;
|
||||
let variantAnalysisManager: VariantAnalysisManager;
|
||||
let variantAnalysis: VariantAnalysisApiResponse;
|
||||
@@ -33,21 +38,15 @@ describe('Variant Analysis Manager', async function() {
|
||||
sandbox.stub(fs, 'mkdirSync');
|
||||
sandbox.stub(fs, 'writeFile');
|
||||
|
||||
cancellationTokenSource = {
|
||||
token: {
|
||||
isCancellationRequested: false,
|
||||
onCancellationRequested: sandbox.stub()
|
||||
},
|
||||
cancel: sandbox.stub(),
|
||||
dispose: sandbox.stub()
|
||||
};
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
scannedRepos = createMockScannedRepos();
|
||||
variantAnalysis = createMockApiResponse('in_progress', scannedRepos);
|
||||
|
||||
try {
|
||||
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
|
||||
variantAnalysisManager = new VariantAnalysisManager(extension.ctx, logger);
|
||||
cli = extension.cliServer;
|
||||
variantAnalysisManager = new VariantAnalysisManager(extension.ctx, cli, storagePath, logger);
|
||||
} catch (e) {
|
||||
fail(e as Error);
|
||||
}
|
||||
@@ -75,6 +74,7 @@ describe('Variant Analysis Manager', async function() {
|
||||
|
||||
describe('when credentials are valid', async () => {
|
||||
let getOctokitStub: sinon.SinonStub;
|
||||
let arrayBuffer: ArrayBuffer;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockCredentials = {
|
||||
@@ -83,16 +83,18 @@ describe('Variant Analysis Manager', async function() {
|
||||
})
|
||||
} as unknown as Credentials;
|
||||
sandbox.stub(Credentials, 'initialize').resolves(mockCredentials);
|
||||
|
||||
const sourceFilePath = path.join(__dirname, '../../../../src/vscode-tests/cli-integration/data/variant-analysis-results.zip');
|
||||
arrayBuffer = fs.readFileSync(sourceFilePath).buffer;
|
||||
});
|
||||
|
||||
describe('when the artifact_url is missing', async () => {
|
||||
beforeEach(async () => {
|
||||
const dummyRepoTask = createMockVariantAnalysisRepoTask();
|
||||
delete dummyRepoTask.artifact_url;
|
||||
getVariantAnalysisRepoStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepo').resolves(dummyRepoTask);
|
||||
|
||||
const dummyResult = 'this-is-a-repo-result';
|
||||
getVariantAnalysisRepoResultStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepoResult').resolves(dummyResult);
|
||||
getVariantAnalysisRepoStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepo').resolves(dummyRepoTask);
|
||||
getVariantAnalysisRepoResultStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepoResult').resolves(arrayBuffer);
|
||||
});
|
||||
|
||||
it('should not try to download the result', async () => {
|
||||
@@ -107,16 +109,17 @@ describe('Variant Analysis Manager', async function() {
|
||||
});
|
||||
|
||||
describe('when the artifact_url is present', async () => {
|
||||
beforeEach(async () => {
|
||||
const dummyRepoTask = createMockVariantAnalysisRepoTask();
|
||||
getVariantAnalysisRepoStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepo').resolves(dummyRepoTask);
|
||||
let dummyRepoTask: VariantAnalysisRepoTask;
|
||||
|
||||
const dummyResult = 'this-is-a-repo-result';
|
||||
getVariantAnalysisRepoResultStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepoResult').resolves(dummyResult);
|
||||
beforeEach(async () => {
|
||||
dummyRepoTask = createMockVariantAnalysisRepoTask();
|
||||
|
||||
getVariantAnalysisRepoStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepo').resolves(dummyRepoTask);
|
||||
getVariantAnalysisRepoResultStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepoResult').resolves(arrayBuffer);
|
||||
});
|
||||
|
||||
it('should return early if variant analysis is cancelled', async () => {
|
||||
cancellationTokenSource.token.isCancellationRequested = true;
|
||||
cancellationTokenSource.cancel();
|
||||
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
@@ -147,14 +150,15 @@ describe('Variant Analysis Manager', async function() {
|
||||
expect(getVariantAnalysisRepoResultStub.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it('should save the result to disk', async () => {
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token
|
||||
);
|
||||
it('should pop download tasks off the queue', async () => {
|
||||
const getResultsSpy = sandbox.spy(variantAnalysisManager, 'autoDownloadVariantAnalysisResult');
|
||||
|
||||
expect(getVariantAnalysisRepoResultStub.calledOnce).to.be.true;
|
||||
await variantAnalysisManager.enqueueDownload(scannedRepos[0], variantAnalysis, cancellationTokenSource.token);
|
||||
await variantAnalysisManager.enqueueDownload(scannedRepos[1], variantAnalysis, cancellationTokenSource.token);
|
||||
await variantAnalysisManager.enqueueDownload(scannedRepos[2], variantAnalysis, cancellationTokenSource.token);
|
||||
|
||||
expect(variantAnalysisManager.downloadsQueueSize()).to.equal(0);
|
||||
expect(getResultsSpy).to.have.been.calledThrice;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import * as sinon from 'sinon';
|
||||
import { expect } from 'chai';
|
||||
import { CancellationTokenSource, extensions } from 'vscode';
|
||||
import { CancellationTokenSource, commands, extensions } from 'vscode';
|
||||
import { CodeQLExtensionInterface } from '../../../extension';
|
||||
import { logger } from '../../../logging';
|
||||
import * as config from '../../../config';
|
||||
|
||||
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
|
||||
import { VariantAnalysisMonitor } from '../../../remote-queries/variant-analysis-monitor';
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisApiResponse,
|
||||
VariantAnalysisFailureReason,
|
||||
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository,
|
||||
VariantAnalysisFailureReason
|
||||
} from '../../../remote-queries/gh-api/variant-analysis';
|
||||
import { createFailedMockApiResponse, createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
|
||||
import { VariantAnalysis, VariantAnalysisStatus } from '../../../remote-queries/shared/variant-analysis';
|
||||
@@ -18,37 +17,38 @@ import { createMockScannedRepos } from '../../factories/remote-queries/gh-api/sc
|
||||
import { processFailureReason } from '../../../remote-queries/variant-analysis-processor';
|
||||
import { Credentials } from '../../../authentication';
|
||||
import { createMockVariantAnalysis } from '../../factories/remote-queries/shared/variant-analysis';
|
||||
import { VariantAnalysisManager } from '../../../remote-queries/variant-analysis-manager';
|
||||
|
||||
describe('Variant Analysis Monitor', async function() {
|
||||
this.timeout(60000);
|
||||
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
let extension: CodeQLExtensionInterface | Record<string, never>;
|
||||
let mockGetVariantAnalysis: sinon.SinonStub;
|
||||
let cancellationTokenSource: CancellationTokenSource;
|
||||
let variantAnalysisMonitor: VariantAnalysisMonitor;
|
||||
let variantAnalysis: VariantAnalysis;
|
||||
let variantAnalysisManager: VariantAnalysisManager;
|
||||
let mockGetDownloadResult: sinon.SinonStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.createSandbox();
|
||||
sandbox.stub(logger, 'log');
|
||||
sandbox.stub(config, 'isVariantAnalysisLiveResultsEnabled').returns(false);
|
||||
|
||||
cancellationTokenSource = {
|
||||
token: {
|
||||
isCancellationRequested: false,
|
||||
onCancellationRequested: sandbox.stub()
|
||||
},
|
||||
cancel: sandbox.stub(),
|
||||
dispose: sandbox.stub()
|
||||
};
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
variantAnalysis = createMockVariantAnalysis();
|
||||
|
||||
try {
|
||||
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
|
||||
variantAnalysisMonitor = new VariantAnalysisMonitor(extension.ctx, logger);
|
||||
extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
|
||||
variantAnalysisMonitor = new VariantAnalysisMonitor(extension.ctx);
|
||||
} catch (e) {
|
||||
fail(e as Error);
|
||||
}
|
||||
|
||||
variantAnalysisManager = extension.variantAnalysisManager;
|
||||
mockGetDownloadResult = sandbox.stub(variantAnalysisManager, 'autoDownloadVariantAnalysisResult');
|
||||
|
||||
limitNumberOfAttemptsToMonitor();
|
||||
});
|
||||
|
||||
@@ -79,7 +79,7 @@ describe('Variant Analysis Monitor', async function() {
|
||||
});
|
||||
|
||||
it('should return early if variant analysis is cancelled', async () => {
|
||||
cancellationTokenSource.token.isCancellationRequested = true;
|
||||
cancellationTokenSource.cancel();
|
||||
|
||||
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationTokenSource.token);
|
||||
|
||||
@@ -103,25 +103,61 @@ describe('Variant Analysis Monitor', async function() {
|
||||
expect(result.variantAnalysis?.status).to.equal(VariantAnalysisStatus.Failed);
|
||||
expect(result.variantAnalysis?.failureReason).to.equal(processFailureReason(mockFailedApiResponse.failure_reason as VariantAnalysisFailureReason));
|
||||
});
|
||||
|
||||
it('should emit `onVariantAnalysisChange`', async () => {
|
||||
const spy = sandbox.spy();
|
||||
variantAnalysisMonitor.onVariantAnalysisChange(spy);
|
||||
|
||||
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationTokenSource.token);
|
||||
|
||||
expect(spy).to.have.been.calledWith(result.variantAnalysis);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the variant analysis completes', async () => {
|
||||
describe('when the variant analysis is in progress', async () => {
|
||||
let mockApiResponse: VariantAnalysisApiResponse;
|
||||
let scannedRepos: ApiVariantAnalysisScannedRepository[];
|
||||
let succeededRepos: ApiVariantAnalysisScannedRepository[];
|
||||
|
||||
describe('when there are successfully scanned repos', async () => {
|
||||
beforeEach(async function() {
|
||||
scannedRepos = createMockScannedRepos(['pending', 'in_progress', 'succeeded']);
|
||||
scannedRepos = createMockScannedRepos(['pending', 'pending', 'in_progress', 'in_progress', 'succeeded', 'succeeded', 'succeeded']);
|
||||
mockApiResponse = createMockApiResponse('completed', scannedRepos);
|
||||
mockGetVariantAnalysis = sandbox.stub(ghApiClient, 'getVariantAnalysis').resolves(mockApiResponse);
|
||||
succeededRepos = scannedRepos.filter(r => r.analysis_status === 'succeeded');
|
||||
});
|
||||
|
||||
it('should succeed and return a list of scanned repo ids', async () => {
|
||||
const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationTokenSource.token);
|
||||
const scannedRepoIds = scannedRepos.filter(r => r.analysis_status == 'succeeded').map(r => r.repository.id);
|
||||
|
||||
expect(result.status).to.equal('CompletedSuccessfully');
|
||||
expect(result.scannedReposDownloaded).to.eql(scannedRepoIds);
|
||||
expect(result.scannedReposDownloaded).to.eql(succeededRepos.map(r => r.repository.id));
|
||||
});
|
||||
|
||||
it('should trigger a download extension command for each repo', async () => {
|
||||
const succeededRepos = scannedRepos.filter(r => r.analysis_status === 'succeeded');
|
||||
const commandSpy = sandbox.spy(commands, 'executeCommand');
|
||||
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationTokenSource.token);
|
||||
|
||||
expect(commandSpy).to.have.callCount(succeededRepos.length);
|
||||
|
||||
succeededRepos.forEach((succeededRepo, index) => {
|
||||
expect(commandSpy.getCall(index).args[0]).to.eq('codeQL.autoDownloadVariantAnalysisResult');
|
||||
expect(commandSpy.getCall(index).args[1]).to.eq(succeededRepo);
|
||||
expect(commandSpy.getCall(index).args[2]).to.eq(mockApiResponse);
|
||||
});
|
||||
});
|
||||
|
||||
it('should download all available results', async () => {
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationTokenSource.token);
|
||||
|
||||
expect(mockGetDownloadResult).to.have.callCount(succeededRepos.length);
|
||||
|
||||
succeededRepos.forEach((succeededRepo, index) => {
|
||||
expect(mockGetDownloadResult.getCall(index).args[0]).to.eq(succeededRepo);
|
||||
expect(mockGetDownloadResult.getCall(index).args[1]).to.eq(mockApiResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -140,6 +176,12 @@ describe('Variant Analysis Monitor', async function() {
|
||||
expect(result.status).to.equal('CompletedSuccessfully');
|
||||
expect(result.scannedReposDownloaded).to.eql([]);
|
||||
});
|
||||
|
||||
it('should not try to download any repos', async () => {
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationTokenSource.token);
|
||||
|
||||
expect(mockGetDownloadResult).to.not.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are no repos to scan', async () => {
|
||||
@@ -155,6 +197,12 @@ describe('Variant Analysis Monitor', async function() {
|
||||
expect(result.status).to.equal('CompletedSuccessfully');
|
||||
expect(result.scannedReposDownloaded).to.eql([]);
|
||||
});
|
||||
|
||||
it('should not try to download any repos', async () => {
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationTokenSource.token);
|
||||
|
||||
expect(mockGetDownloadResult).to.not.have.been.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,11 +31,16 @@ describe('Variant Analysis processor', function() {
|
||||
'filePath': 'query-file-path',
|
||||
'language': VariantAnalysisQueryLanguage.Javascript,
|
||||
'name': 'query-name',
|
||||
'text': mockSubmission.query.text,
|
||||
},
|
||||
'databases': {
|
||||
'repositories': ['1', '2', '3'],
|
||||
},
|
||||
'executionStartTime': mockSubmission.startTime,
|
||||
'createdAt': mockApiResponse.created_at,
|
||||
'updatedAt': mockApiResponse.updated_at,
|
||||
'status': 'succeeded',
|
||||
'completedAt': mockApiResponse.completed_at,
|
||||
'actionsWorkflowRunId': mockApiResponse.actions_workflow_run_id,
|
||||
'scannedRepos': [
|
||||
transformScannedRepo(VariantAnalysisRepoStatus.Succeeded, scannedRepos[0]),
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import * as sinon from 'sinon';
|
||||
import { expect } from 'chai';
|
||||
import { extensions } from 'vscode';
|
||||
import { CodeQLExtensionInterface } from '../../../extension';
|
||||
import { logger } from '../../../logging';
|
||||
import { Credentials } from '../../../authentication';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { VariantAnalysisResultsManager } from '../../../remote-queries/variant-analysis-results-manager';
|
||||
import { createMockVariantAnalysisRepoTask } from '../../factories/remote-queries/gh-api/variant-analysis-repo-task';
|
||||
import { CodeQLCliServer } from '../../../cli';
|
||||
import { storagePath } from '../global.helper';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
|
||||
import { VariantAnalysisRepoTask } from '../../../remote-queries/gh-api/variant-analysis';
|
||||
|
||||
describe(VariantAnalysisResultsManager.name, function() {
|
||||
this.timeout(10000);
|
||||
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
let cli: CodeQLCliServer;
|
||||
let variantAnalysisId: number;
|
||||
let variantAnalysisResultsManager: VariantAnalysisResultsManager;
|
||||
let getVariantAnalysisRepoResultStub: sinon.SinonStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.createSandbox();
|
||||
sandbox.stub(logger, 'log');
|
||||
sandbox.stub(fs, 'mkdirSync');
|
||||
sandbox.stub(fs, 'writeFile');
|
||||
|
||||
variantAnalysisId = faker.datatype.number();
|
||||
|
||||
try {
|
||||
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
|
||||
cli = extension.cliServer;
|
||||
variantAnalysisResultsManager = new VariantAnalysisResultsManager(cli, logger);
|
||||
} catch (e) {
|
||||
fail(e as Error);
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('download', () => {
|
||||
let getOctokitStub: sinon.SinonStub;
|
||||
let variantAnalysisStoragePath: string;
|
||||
const mockCredentials = {
|
||||
getOctokit: () => Promise.resolve({
|
||||
request: getOctokitStub
|
||||
})
|
||||
} as unknown as Credentials;
|
||||
|
||||
beforeEach(async () => {
|
||||
variantAnalysisStoragePath = path.join(storagePath, variantAnalysisId.toString());
|
||||
});
|
||||
|
||||
describe('when the artifact_url is missing', async () => {
|
||||
it('should not try to download the result', async () => {
|
||||
const dummyRepoTask = createMockVariantAnalysisRepoTask();
|
||||
delete dummyRepoTask.artifact_url;
|
||||
|
||||
try {
|
||||
await variantAnalysisResultsManager.download(
|
||||
mockCredentials,
|
||||
variantAnalysisId,
|
||||
dummyRepoTask,
|
||||
variantAnalysisStoragePath
|
||||
);
|
||||
|
||||
expect.fail('Expected an error to be thrown');
|
||||
} catch (e: any) {
|
||||
expect(e.message).to.equal('Missing artifact URL');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the artifact_url is present', async () => {
|
||||
let dummyRepoTask: VariantAnalysisRepoTask;
|
||||
let storageDirectory: string;
|
||||
let arrayBuffer: ArrayBuffer;
|
||||
|
||||
beforeEach(async () => {
|
||||
dummyRepoTask = createMockVariantAnalysisRepoTask();
|
||||
|
||||
storageDirectory = variantAnalysisResultsManager.getRepoStorageDirectory(variantAnalysisStoragePath, dummyRepoTask.repository.full_name);
|
||||
const sourceFilePath = path.join(__dirname, '../../../../src/vscode-tests/cli-integration/data/variant-analysis-results.zip');
|
||||
arrayBuffer = fs.readFileSync(sourceFilePath).buffer;
|
||||
|
||||
getVariantAnalysisRepoResultStub = sandbox
|
||||
.stub(ghApiClient, 'getVariantAnalysisRepoResult')
|
||||
.withArgs(mockCredentials, dummyRepoTask.artifact_url as string)
|
||||
.resolves(arrayBuffer);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
fs.removeSync(`${storageDirectory}/results.zip`);
|
||||
fs.removeSync(`${storageDirectory}/results`);
|
||||
});
|
||||
|
||||
it('should call the API to download the results', async () => {
|
||||
await variantAnalysisResultsManager.download(
|
||||
mockCredentials,
|
||||
variantAnalysisId,
|
||||
dummyRepoTask,
|
||||
variantAnalysisStoragePath
|
||||
);
|
||||
|
||||
expect(getVariantAnalysisRepoResultStub.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it('should save the results zip file to disk', async () => {
|
||||
await variantAnalysisResultsManager.download(
|
||||
mockCredentials,
|
||||
variantAnalysisId,
|
||||
dummyRepoTask,
|
||||
variantAnalysisStoragePath
|
||||
);
|
||||
|
||||
expect(fs.existsSync(`${storageDirectory}/results.zip`)).to.be.true;
|
||||
});
|
||||
|
||||
it('should unzip the results in a `results/` folder', async () => {
|
||||
await variantAnalysisResultsManager.download(
|
||||
mockCredentials,
|
||||
variantAnalysisId,
|
||||
dummyRepoTask,
|
||||
variantAnalysisStoragePath
|
||||
);
|
||||
|
||||
expect(fs.existsSync(`${storageDirectory}/results/results.sarif`)).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -44,7 +44,7 @@ const _10MB = _1MB * 10;
|
||||
|
||||
// CLI version to test. Hard code the latest as default. And be sure
|
||||
// to update the env if it is not otherwise set.
|
||||
const CLI_VERSION = process.env.CLI_VERSION || 'v2.11.0';
|
||||
const CLI_VERSION = process.env.CLI_VERSION || 'v2.11.1';
|
||||
process.env.CLI_VERSION = CLI_VERSION;
|
||||
|
||||
// Base dir where CLIs will be downloaded into
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
InitialQueryInfo,
|
||||
CompletedQueryInfo,
|
||||
CompletedLocalQueryInfo,
|
||||
LocalQueryInfo,
|
||||
} from '../../../query-results';
|
||||
|
||||
export function createMockLocalQueryInfo(
|
||||
startTime: string,
|
||||
userSpecifiedLabel?: string
|
||||
): LocalQueryInfo {
|
||||
return ({
|
||||
t: 'local',
|
||||
userSpecifiedLabel,
|
||||
startTime: startTime,
|
||||
getQueryFileName() {
|
||||
return 'query-file.ql';
|
||||
},
|
||||
getQueryName() {
|
||||
return 'query-name';
|
||||
},
|
||||
initialInfo: ({
|
||||
databaseInfo: {
|
||||
databaseUri: 'unused',
|
||||
name: 'db-name',
|
||||
},
|
||||
} as unknown) as InitialQueryInfo,
|
||||
completedQuery: ({
|
||||
resultCount: 456,
|
||||
statusString: 'in progress',
|
||||
} as unknown) as CompletedQueryInfo,
|
||||
} as unknown) as CompletedLocalQueryInfo;
|
||||
}
|
||||
@@ -28,6 +28,8 @@ export function createMockApiResponse(
|
||||
actor_id: faker.datatype.number(),
|
||||
query_language: VariantAnalysisQueryLanguage.Javascript,
|
||||
query_pack_url: 'https://example.com/foo',
|
||||
created_at: faker.date.recent().toISOString(),
|
||||
updated_at: faker.date.recent().toISOString(),
|
||||
status: status,
|
||||
actions_workflow_run_id: faker.datatype.number(),
|
||||
scanned_repositories: scannedRepos,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { RemoteQueryHistoryItem } from '../../../remote-queries/remote-query-history-item';
|
||||
|
||||
export function createMockRemoteQueryHistoryItem({
|
||||
date = new Date('2022-01-01T00:00:00.000Z'),
|
||||
resultCount = 16,
|
||||
userSpecifiedLabel = undefined,
|
||||
repositoryCount = 0,
|
||||
}: {
|
||||
date?: Date;
|
||||
resultCount?: number;
|
||||
userSpecifiedLabel?: string;
|
||||
repositoryCount?: number;
|
||||
}): RemoteQueryHistoryItem {
|
||||
return ({
|
||||
t: 'remote',
|
||||
userSpecifiedLabel,
|
||||
remoteQuery: {
|
||||
executionStartTime: date.getTime(),
|
||||
queryName: 'query-name',
|
||||
queryFilePath: 'query-file.ql',
|
||||
controllerRepository: {
|
||||
owner: 'github',
|
||||
name: 'vscode-codeql-integration-tests',
|
||||
},
|
||||
language: 'javascript',
|
||||
repositoryCount,
|
||||
},
|
||||
status: 'in progress',
|
||||
resultCount,
|
||||
} as unknown) as RemoteQueryHistoryItem;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export function createMockSubmission(): VariantAnalysisSubmission {
|
||||
name: 'query-name',
|
||||
filePath: 'query-file-path',
|
||||
language: VariantAnalysisQueryLanguage.Javascript,
|
||||
text: 'query-text',
|
||||
pack: 'base64-encoded-string',
|
||||
},
|
||||
databases: {
|
||||
|
||||
@@ -20,11 +20,15 @@ export function createMockVariantAnalysis(
|
||||
query: {
|
||||
name: 'a-query-name',
|
||||
filePath: 'a-query-file-path',
|
||||
language: VariantAnalysisQueryLanguage.Javascript
|
||||
language: VariantAnalysisQueryLanguage.Javascript,
|
||||
text: 'a-query-text',
|
||||
},
|
||||
databases: {
|
||||
repositories: ['1', '2', '3'],
|
||||
},
|
||||
executionStartTime: faker.datatype.number(),
|
||||
createdAt: faker.date.recent().toISOString(),
|
||||
updatedAt: faker.date.recent().toISOString(),
|
||||
status: status,
|
||||
actionsWorkflowRunId: faker.datatype.number(),
|
||||
scannedRepos: scannedRepos,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as path from 'path';
|
||||
import * as Mocha from 'mocha';
|
||||
import * as glob from 'glob';
|
||||
import * as glob from 'glob-promise';
|
||||
import { ensureCli } from './ensureCli';
|
||||
import { env } from 'vscode';
|
||||
import { testConfigHelper } from './test-config';
|
||||
|
||||
|
||||
// Use this handler to avoid swallowing unhandled rejections.
|
||||
@@ -57,44 +58,42 @@ export async function runTestsInDirectory(testsRoot: string, useCli = false): Pr
|
||||
|
||||
await ensureCli(useCli);
|
||||
|
||||
console.log(`Adding test cases and helpers from ${testsRoot}`);
|
||||
|
||||
const files = await glob('**/**.js', { cwd: testsRoot });
|
||||
|
||||
// Add test files to the test suite
|
||||
files
|
||||
.filter(f => f.endsWith('.test.js'))
|
||||
.forEach(f => {
|
||||
console.log(`Adding test file ${f}`);
|
||||
mocha.addFile(path.resolve(testsRoot, f));
|
||||
});
|
||||
|
||||
// Setup the config helper. This needs to run before other helpers so any config they setup
|
||||
// is restored.
|
||||
await testConfigHelper(mocha);
|
||||
|
||||
// Add helpers. Helper files add global setup and teardown blocks
|
||||
// for a test run.
|
||||
files
|
||||
.filter(f => f.endsWith('.helper.js'))
|
||||
.forEach(f => {
|
||||
console.log(`Executing helper ${f}`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const helper = require(path.resolve(testsRoot, f)).default;
|
||||
helper(mocha);
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Adding test cases and helpers from ${testsRoot}`);
|
||||
glob('**/**.js', { cwd: testsRoot }, (err, files) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
// Run the mocha test
|
||||
mocha.run(failures => {
|
||||
if (failures > 0) {
|
||||
reject(new Error(`${failures} tests failed.`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Add test files to the test suite
|
||||
files
|
||||
.filter(f => f.endsWith('.test.js'))
|
||||
.forEach(f => {
|
||||
console.log(`Adding test file ${f}`);
|
||||
mocha.addFile(path.resolve(testsRoot, f));
|
||||
});
|
||||
|
||||
// Add helpers. Helper files add global setup and teardown blocks
|
||||
// for a test run.
|
||||
files
|
||||
.filter(f => f.endsWith('.helper.js'))
|
||||
.forEach(f => {
|
||||
console.log(`Executing helper ${f}`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const helper = require(path.resolve(testsRoot, f)).default;
|
||||
helper(mocha);
|
||||
});
|
||||
|
||||
// Run the mocha test
|
||||
mocha.run(failures => {
|
||||
if (failures > 0) {
|
||||
reject(new Error(`${failures} tests failed.`));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,7 +77,9 @@ describe('databases', () => {
|
||||
},
|
||||
resolveDatabase: resolveDatabaseSpy
|
||||
} as unknown as CodeQLCliServer,
|
||||
{} as Logger,
|
||||
{
|
||||
log: () => { /**/ }
|
||||
} as unknown as Logger,
|
||||
);
|
||||
|
||||
// Unfortunately, during a test it is not possible to convert from
|
||||
|
||||
@@ -13,3 +13,12 @@ chai.use(sinonChai);
|
||||
export function run(): Promise<void> {
|
||||
return runTestsInDirectory(__dirname);
|
||||
}
|
||||
|
||||
process.addListener('unhandledRejection', (reason) => {
|
||||
if (reason instanceof Error && reason.message === 'Canceled') {
|
||||
console.log('Cancellation requested after the test has ended.');
|
||||
process.exit(0);
|
||||
} else {
|
||||
fail(String(reason));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@ import { env } from 'vscode';
|
||||
import { expect } from 'chai';
|
||||
import { QueryHistoryConfig } from '../../config';
|
||||
import { HistoryItemLabelProvider } from '../../history-item-label-provider';
|
||||
import { CompletedLocalQueryInfo, CompletedQueryInfo, InitialQueryInfo, QueryHistoryInfo } from '../../query-results';
|
||||
import { RemoteQueryHistoryItem } from '../../remote-queries/remote-query-history-item';
|
||||
import { createMockLocalQueryInfo } from '../factories/local-queries/local-query-history-item';
|
||||
import { createMockRemoteQueryHistoryItem } from '../factories/remote-queries/remote-query-history-item';
|
||||
|
||||
|
||||
describe('HistoryItemLabelProvider', () => {
|
||||
@@ -22,7 +22,7 @@ describe('HistoryItemLabelProvider', () => {
|
||||
|
||||
describe('local queries', () => {
|
||||
it('should interpolate query when user specified', () => {
|
||||
const fqi = createMockLocalQueryInfo('xxx');
|
||||
const fqi = createMockLocalQueryInfo(dateStr, 'xxx');
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).to.eq('xxx');
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('HistoryItemLabelProvider', () => {
|
||||
});
|
||||
|
||||
it('should interpolate query when not user specified', () => {
|
||||
const fqi = createMockLocalQueryInfo();
|
||||
const fqi = createMockLocalQueryInfo(dateStr);
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).to.eq('xxx query-name xxx');
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('HistoryItemLabelProvider', () => {
|
||||
});
|
||||
|
||||
it('should get query short label', () => {
|
||||
const fqi = createMockLocalQueryInfo('xxx');
|
||||
const fqi = createMockLocalQueryInfo(dateStr, 'xxx');
|
||||
|
||||
// fall back on user specified if one exists.
|
||||
expect(labelProvider.getShortLabel(fqi)).to.eq('xxx');
|
||||
@@ -56,35 +56,11 @@ describe('HistoryItemLabelProvider', () => {
|
||||
delete (fqi as any).userSpecifiedLabel;
|
||||
expect(labelProvider.getShortLabel(fqi)).to.eq('query-name');
|
||||
});
|
||||
|
||||
function createMockLocalQueryInfo(userSpecifiedLabel?: string) {
|
||||
return {
|
||||
t: 'local',
|
||||
userSpecifiedLabel,
|
||||
startTime: date.toLocaleString(env.language),
|
||||
getQueryFileName() {
|
||||
return 'query-file.ql';
|
||||
},
|
||||
getQueryName() {
|
||||
return 'query-name';
|
||||
},
|
||||
initialInfo: {
|
||||
databaseInfo: {
|
||||
databaseUri: 'unused',
|
||||
name: 'db-name'
|
||||
}
|
||||
} as unknown as InitialQueryInfo,
|
||||
completedQuery: {
|
||||
resultCount: 456,
|
||||
statusString: 'in progress',
|
||||
} as unknown as CompletedQueryInfo,
|
||||
} as unknown as CompletedLocalQueryInfo;
|
||||
}
|
||||
});
|
||||
|
||||
describe('remote queries', () => {
|
||||
it('should interpolate query when user specified', () => {
|
||||
const fqi = createMockRemoteQueryInfo({ userSpecifiedLabel: 'xxx' });
|
||||
const fqi = createMockRemoteQueryHistoryItem({ userSpecifiedLabel: 'xxx' });
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).to.eq('xxx');
|
||||
|
||||
@@ -96,7 +72,7 @@ describe('HistoryItemLabelProvider', () => {
|
||||
});
|
||||
|
||||
it('should interpolate query when not user-specified', () => {
|
||||
const fqi = createMockRemoteQueryInfo({});
|
||||
const fqi = createMockRemoteQueryHistoryItem({});
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).to.eq('xxx query-name (javascript) xxx');
|
||||
|
||||
@@ -109,14 +85,14 @@ describe('HistoryItemLabelProvider', () => {
|
||||
});
|
||||
|
||||
it('should use number of repositories instead of controller repo if available', () => {
|
||||
const fqi = createMockRemoteQueryInfo({ repositoryCount: 2 });
|
||||
const fqi = createMockRemoteQueryHistoryItem({ repositoryCount: 2 });
|
||||
|
||||
config.format = '%t %q %d %s %f %r %%';
|
||||
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name (javascript) 2 repositories in progress query-file.ql (16 results) %`);
|
||||
});
|
||||
|
||||
it('should get query short label', () => {
|
||||
const fqi = createMockRemoteQueryInfo({ userSpecifiedLabel: 'xxx' });
|
||||
const fqi = createMockRemoteQueryHistoryItem({ userSpecifiedLabel: 'xxx' });
|
||||
|
||||
// fall back on user specified if one exists.
|
||||
expect(labelProvider.getShortLabel(fqi)).to.eq('xxx');
|
||||
@@ -128,7 +104,7 @@ describe('HistoryItemLabelProvider', () => {
|
||||
|
||||
describe('when results are present', () => {
|
||||
it('should display results if there are any', () => {
|
||||
const fqi = createMockRemoteQueryInfo({ resultCount: 16, repositoryCount: 2 });
|
||||
const fqi = createMockRemoteQueryHistoryItem({ resultCount: 16, repositoryCount: 2 });
|
||||
config.format = '%t %q %d %s %f %r %%';
|
||||
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name (javascript) 2 repositories in progress query-file.ql (16 results) %`);
|
||||
});
|
||||
@@ -136,7 +112,7 @@ describe('HistoryItemLabelProvider', () => {
|
||||
|
||||
describe('when results are not present', () => {
|
||||
it('should skip displaying them', () => {
|
||||
const fqi = createMockRemoteQueryInfo({ resultCount: 0, repositoryCount: 2 });
|
||||
const fqi = createMockRemoteQueryHistoryItem({ resultCount: 0, repositoryCount: 2 });
|
||||
config.format = '%t %q %d %s %f %r %%';
|
||||
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name (javascript) 2 repositories in progress query-file.ql %`);
|
||||
});
|
||||
@@ -144,7 +120,7 @@ describe('HistoryItemLabelProvider', () => {
|
||||
|
||||
describe('when extra whitespace is present in the middle of the label', () => {
|
||||
it('should squash it down to a single whitespace', () => {
|
||||
const fqi = createMockRemoteQueryInfo({ resultCount: 0, repositoryCount: 2 });
|
||||
const fqi = createMockRemoteQueryHistoryItem({ resultCount: 0, repositoryCount: 2 });
|
||||
config.format = '%t %q %d %s %f %r %%';
|
||||
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name (javascript) 2 repositories in progress query-file.ql %`);
|
||||
});
|
||||
@@ -152,7 +128,7 @@ describe('HistoryItemLabelProvider', () => {
|
||||
|
||||
describe('when extra whitespace is present at the start of the label', () => {
|
||||
it('should squash it down to a single whitespace', () => {
|
||||
const fqi = createMockRemoteQueryInfo({ resultCount: 0, repositoryCount: 2 });
|
||||
const fqi = createMockRemoteQueryHistoryItem({ resultCount: 0, repositoryCount: 2 });
|
||||
config.format = ' %t %q %d %s %f %r %%';
|
||||
expect(labelProvider.getLabel(fqi)).to.eq(` ${dateStr} query-name (javascript) 2 repositories in progress query-file.ql %`);
|
||||
});
|
||||
@@ -160,38 +136,10 @@ describe('HistoryItemLabelProvider', () => {
|
||||
|
||||
describe('when extra whitespace is present at the end of the label', () => {
|
||||
it('should squash it down to a single whitespace', () => {
|
||||
const fqi = createMockRemoteQueryInfo({ resultCount: 0, repositoryCount: 2 });
|
||||
const fqi = createMockRemoteQueryHistoryItem({ resultCount: 0, repositoryCount: 2 });
|
||||
config.format = '%t %q %d %s %f %r %% ';
|
||||
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name (javascript) 2 repositories in progress query-file.ql % `);
|
||||
});
|
||||
});
|
||||
|
||||
function createMockRemoteQueryInfo({
|
||||
resultCount = 16,
|
||||
userSpecifiedLabel = undefined,
|
||||
repositoryCount = 0
|
||||
}: {
|
||||
resultCount?: number;
|
||||
userSpecifiedLabel?: string;
|
||||
repositoryCount?: number;
|
||||
}): QueryHistoryInfo {
|
||||
return {
|
||||
t: 'remote',
|
||||
userSpecifiedLabel,
|
||||
remoteQuery: {
|
||||
executionStartTime: date.getTime(),
|
||||
queryName: 'query-name',
|
||||
queryFilePath: 'query-file.ql',
|
||||
controllerRepository: {
|
||||
owner: 'github',
|
||||
name: 'vscode-codeql-integration-tests'
|
||||
},
|
||||
language: 'javascript',
|
||||
repositoryCount,
|
||||
},
|
||||
status: 'in progress',
|
||||
resultCount,
|
||||
} as unknown as RemoteQueryHistoryItem;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as vscode from 'vscode';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
import { logger } from '../../logging';
|
||||
import { registerQueryHistoryScubber } from '../../query-history-scrubber';
|
||||
import { registerQueryHistoryScrubber } from '../../query-history-scrubber';
|
||||
import { QueryHistoryManager, HistoryTreeDataProvider, SortOrder } from '../../query-history';
|
||||
import { QueryEvaluationInfo, QueryWithResults } from '../../run-queries-shared';
|
||||
import { QueryHistoryConfig, QueryHistoryConfigListener } from '../../config';
|
||||
@@ -20,6 +20,8 @@ import { RemoteQueriesManager } from '../../remote-queries/remote-queries-manage
|
||||
import { ResultsView } from '../../interface';
|
||||
import { EvalLogViewer } from '../../eval-log-viewer';
|
||||
import { QueryRunner } from '../../queryRunner';
|
||||
import { QueryResultType } from '../../pure/legacy-messages';
|
||||
import { VariantAnalysisManager } from '../../remote-queries/variant-analysis-manager';
|
||||
|
||||
describe('query-history', () => {
|
||||
const mockExtensionLocation = path.join(tmpDir.name, 'mock-extension-location');
|
||||
@@ -33,6 +35,7 @@ describe('query-history', () => {
|
||||
|
||||
let localQueriesResultsViewStub: ResultsView;
|
||||
let remoteQueriesManagerStub: RemoteQueriesManager;
|
||||
let variantAnalysisManagerStub: VariantAnalysisManager;
|
||||
|
||||
let tryOpenExternalFile: Function;
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
@@ -62,6 +65,10 @@ describe('query-history', () => {
|
||||
onRemoteQueryRemoved: sandbox.stub(),
|
||||
onRemoteQueryStatusUpdate: sandbox.stub()
|
||||
} as any as RemoteQueriesManager;
|
||||
|
||||
variantAnalysisManagerStub = {
|
||||
onVariantAnalysisAdded: sandbox.stub()
|
||||
} as any as VariantAnalysisManager;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -580,7 +587,7 @@ describe('query-history', () => {
|
||||
}
|
||||
});
|
||||
|
||||
function createMockFullQueryInfo(dbName = 'a', queryWitbResults?: QueryWithResults, isFail = false): LocalQueryInfo {
|
||||
function createMockFullQueryInfo(dbName = 'a', queryWithResults?: QueryWithResults, isFail = false): LocalQueryInfo {
|
||||
const fqi = new LocalQueryInfo(
|
||||
{
|
||||
databaseInfo: { name: dbName },
|
||||
@@ -592,8 +599,8 @@ describe('query-history', () => {
|
||||
} as vscode.CancellationTokenSource
|
||||
);
|
||||
|
||||
if (queryWitbResults) {
|
||||
fqi.completeThisQuery(queryWitbResults);
|
||||
if (queryWithResults) {
|
||||
fqi.completeThisQuery(queryWithResults);
|
||||
}
|
||||
if (isFail) {
|
||||
fqi.failureReason = 'failure reason';
|
||||
@@ -758,7 +765,7 @@ describe('query-history', () => {
|
||||
}
|
||||
|
||||
function registerScrubber(dir: string) {
|
||||
deregister = registerQueryHistoryScubber(
|
||||
deregister = registerQueryHistoryScrubber(
|
||||
ONE_HOUR_IN_MS,
|
||||
TWO_HOURS_IN_MS,
|
||||
LESS_THAN_ONE_DAY,
|
||||
@@ -784,9 +791,15 @@ describe('query-history', () => {
|
||||
hasInterpretedResults: () => Promise.resolve(hasInterpretedResults),
|
||||
deleteQuery: sandbox.stub(),
|
||||
} as unknown as QueryEvaluationInfo,
|
||||
sucessful: didRunSuccessfully,
|
||||
successful: didRunSuccessfully,
|
||||
message: 'foo',
|
||||
dispose: sandbox.spy()
|
||||
dispose: sandbox.spy(),
|
||||
result: {
|
||||
evaluationTime: 1,
|
||||
queryId: 0,
|
||||
runId: 0,
|
||||
resultType: QueryResultType.SUCCESS,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -796,6 +809,7 @@ describe('query-history', () => {
|
||||
{} as DatabaseManager,
|
||||
localQueriesResultsViewStub,
|
||||
remoteQueriesManagerStub,
|
||||
variantAnalysisManagerStub,
|
||||
{} as EvalLogViewer,
|
||||
'xxx',
|
||||
{
|
||||
|
||||
@@ -290,7 +290,7 @@ describe('query-results', () => {
|
||||
it('should handle an invalid query history version', async () => {
|
||||
const badPath = path.join(tmpDir.name, 'bad-query-history.json');
|
||||
fs.writeFileSync(badPath, JSON.stringify({
|
||||
version: 2,
|
||||
version: 3,
|
||||
queries: allHistory
|
||||
}), 'utf8');
|
||||
|
||||
@@ -325,9 +325,15 @@ describe('query-results', () => {
|
||||
|
||||
const result: QueryWithResults = {
|
||||
query: query.queryEvalInfo,
|
||||
sucessful: didRunSuccessfully,
|
||||
successful: didRunSuccessfully,
|
||||
message: 'foo',
|
||||
dispose: disposeSpy,
|
||||
result: {
|
||||
evaluationTime: 1,
|
||||
queryId: 0,
|
||||
runId: 0,
|
||||
resultType: QueryResultType.SUCCESS,
|
||||
}
|
||||
};
|
||||
|
||||
if (includeSpies) {
|
||||
@@ -337,7 +343,7 @@ describe('query-results', () => {
|
||||
return result;
|
||||
}
|
||||
|
||||
function createMockFullQueryInfo(dbName = 'a', queryWitbResults?: QueryWithResults, isFail = false): LocalQueryInfo {
|
||||
function createMockFullQueryInfo(dbName = 'a', queryWithResults?: QueryWithResults, isFail = false): LocalQueryInfo {
|
||||
const fqi = new LocalQueryInfo(
|
||||
{
|
||||
databaseInfo: {
|
||||
@@ -356,8 +362,8 @@ describe('query-results', () => {
|
||||
} as CancellationTokenSource
|
||||
);
|
||||
|
||||
if (queryWitbResults) {
|
||||
fqi.completeThisQuery(queryWitbResults);
|
||||
if (queryWithResults) {
|
||||
fqi.completeThisQuery(queryWithResults);
|
||||
}
|
||||
if (isFail) {
|
||||
fqi.failureReason = 'failure reason';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user