Merge pull request #1572 from github/koesie10/request-repo-results-message

Implement `requestRepoResults` message
This commit is contained in:
Koen Vlaswinkel
2022-10-11 16:48:34 +02:00
committed by GitHub
9 changed files with 161 additions and 23 deletions

View File

@@ -956,6 +956,12 @@ async function activateWithInstalledDistribution(
})
);
ctx.subscriptions.push(
commandRunner('codeQL.loadVariantAnalysisRepoResults', async (variantAnalysisId: number, repositoryFullName: string) => {
await variantAnalysisManager.loadResults(variantAnalysisId, repositoryFullName);
})
);
// The "openVariantAnalysisView" command is internal-only.
ctx.subscriptions.push(
commandRunner('codeQL.openVariantAnalysisView', async (variantAnalysisId: number) => {

View File

@@ -459,6 +459,11 @@ export interface SetRepoStatesMessage {
repoStates: VariantAnalysisScannedRepositoryState[];
}
export interface RequestRepositoryResultsMessage {
t: 'requestRepositoryResults';
repositoryFullName: string;
}
export type ToVariantAnalysisMessage =
| SetVariantAnalysisMessage
| SetRepoResultsMessage
@@ -466,4 +471,5 @@ export type ToVariantAnalysisMessage =
export type FromVariantAnalysisMessage =
| ViewLoadedMsg
| StopVariantAnalysisMessage;
| StopVariantAnalysisMessage
| RequestRepositoryResultsMessage;

View File

@@ -12,6 +12,7 @@ import {
import {
VariantAnalysis,
VariantAnalysisScannedRepositoryDownloadStatus,
VariantAnalysisScannedRepositoryResult,
VariantAnalysisScannedRepositoryState
} from './shared/variant-analysis';
import { getErrorMessage } from '../pure/helpers-pure';
@@ -26,6 +27,7 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
private readonly variantAnalysisMonitor: VariantAnalysisMonitor;
private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager;
private readonly variantAnalyses = new Map<number, VariantAnalysis>();
private readonly views = new Map<number, VariantAnalysisView>();
constructor(
@@ -39,6 +41,7 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
this.variantAnalysisMonitor.onVariantAnalysisChange(this.onVariantAnalysisUpdated.bind(this));
this.variantAnalysisResultsManager = this.push(new VariantAnalysisResultsManager(cliServer, storagePath, logger));
this.variantAnalysisResultsManager.onResultLoaded(this.onRepoResultLoaded.bind(this));
}
public async showView(variantAnalysisId: number): Promise<void> {
@@ -68,11 +71,22 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
return this.views.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, repositoryFullName);
}
private async onVariantAnalysisUpdated(variantAnalysis: VariantAnalysis | undefined): Promise<void> {
if (!variantAnalysis) {
return;
}
this.variantAnalyses.set(variantAnalysis.id, variantAnalysis);
await this.getView(variantAnalysis.id)?.updateView(variantAnalysis);
}
@@ -80,6 +94,10 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
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);
}

View File

@@ -41,6 +41,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);

View File

@@ -18,7 +18,7 @@ import { unzipFile } from '../pure/zip';
type CacheKey = `${number}/${string}`;
const createCacheKey = (variantAnalysisId: number, repoTask: VariantAnalysisRepoTask): CacheKey => `${variantAnalysisId}/${repoTask.repository.full_name}`;
const createCacheKey = (variantAnalysisId: number, repositoryFullName: string): CacheKey => `${variantAnalysisId}/${repositoryFullName}`;
export type ResultDownloadedEvent = {
variantAnalysisId: number;
@@ -26,6 +26,9 @@ export type ResultDownloadedEvent = {
}
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>());
@@ -63,8 +66,10 @@ export class VariantAnalysisResultsManager extends DisposableObject {
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, 'results');
const unzippedFilesDirectory = path.join(resultDirectory, VariantAnalysisResultsManager.RESULTS_DIRECTORY);
fs.writeFileSync(zipFilePath, Buffer.from(result));
await unzipFile(zipFilePath, unzippedFilesDirectory);
@@ -77,41 +82,44 @@ export class VariantAnalysisResultsManager extends DisposableObject {
public async loadResults(
variantAnalysisId: number,
repoTask: VariantAnalysisRepoTask
repositoryFullName: string
): Promise<VariantAnalysisScannedRepositoryResult> {
const result = this.cachedResults.get(createCacheKey(variantAnalysisId, repoTask));
const result = this.cachedResults.get(createCacheKey(variantAnalysisId, repositoryFullName));
return result ?? await this.loadResultsIntoMemory(variantAnalysisId, repoTask);
return result ?? await this.loadResultsIntoMemory(variantAnalysisId, repositoryFullName);
}
private async loadResultsIntoMemory(
variantAnalysisId: number,
repoTask: VariantAnalysisRepoTask,
repositoryFullName: string,
): Promise<VariantAnalysisScannedRepositoryResult> {
const result = await this.loadResultsFromStorage(variantAnalysisId, repoTask);
this.cachedResults.set(createCacheKey(variantAnalysisId, repoTask), result);
const result = await this.loadResultsFromStorage(variantAnalysisId, repositoryFullName);
this.cachedResults.set(createCacheKey(variantAnalysisId, repositoryFullName), result);
this._onResultLoaded.fire(result);
return result;
}
private async loadResultsFromStorage(
variantAnalysisId: number,
repoTask: VariantAnalysisRepoTask,
repositoryFullName: string,
): Promise<VariantAnalysisScannedRepositoryResult> {
if (!(await this.isVariantAnalysisRepoDownloaded(variantAnalysisId, repoTask))) {
if (!(await this.isVariantAnalysisRepoDownloaded(variantAnalysisId, repositoryFullName))) {
throw new Error('Variant analysis results not downloaded');
}
const storageDirectory = this.getRepoStorageDirectory(variantAnalysisId, 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 storageDirectory = this.getRepoStorageDirectory(variantAnalysisId, repoTask.repository.full_name);
const sarifPath = path.join(storageDirectory, 'results.sarif');
const bqrsPath = path.join(storageDirectory, 'results.bqrs');
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);
@@ -137,9 +145,9 @@ export class VariantAnalysisResultsManager extends DisposableObject {
private async isVariantAnalysisRepoDownloaded(
variantAnalysisId: number,
repoTask: VariantAnalysisRepoTask,
repositoryFullName: string,
): Promise<boolean> {
return await fs.pathExists(this.getRepoStorageDirectory(variantAnalysisId, repoTask.repository.full_name));
return await fs.pathExists(this.getRepoStorageDirectory(variantAnalysisId, repositoryFullName));
}
private async readBqrsResults(filePath: string, fileLinkPrefix: string, sourceLocationPrefix: string): Promise<AnalysisRawResults> {

View File

@@ -1,4 +1,4 @@
import { ExtensionContext, ViewColumn } from 'vscode';
import { commands, ExtensionContext, ViewColumn } from 'vscode';
import { AbstractWebview, WebviewPanelConfig } from '../abstract-webview';
import { logger } from '../logging';
import { FromVariantAnalysisMessage, ToVariantAnalysisMessage } from '../pure/interface-types';
@@ -7,6 +7,7 @@ import {
VariantAnalysis,
VariantAnalysisQueryLanguage,
VariantAnalysisRepoStatus,
VariantAnalysisScannedRepositoryResult,
VariantAnalysisScannedRepositoryState,
VariantAnalysisStatus
} from './shared/variant-analysis';
@@ -53,6 +54,17 @@ export class VariantAnalysisView extends AbstractWebview<ToVariantAnalysisMessag
});
}
public async sendRepositoryResults(repositoryResult: VariantAnalysisScannedRepositoryResult[]): Promise<void> {
if (!this.isShowingPanel) {
return;
}
await this.postMessage({
t: 'setRepoResults',
repoResults: repositoryResult,
});
}
protected getPanelConfig(): WebviewPanelConfig {
return {
viewId: VariantAnalysisView.viewType,
@@ -83,6 +95,9 @@ export class VariantAnalysisView extends AbstractWebview<ToVariantAnalysisMessag
case 'stopVariantAnalysis':
void logger.log(`Stop variant analysis: ${msg.variantAnalysisId}`);
break;
case 'requestRepositoryResults':
void commands.executeCommand('codeQL.loadVariantAnalysisRepoResults', this.variantAnalysisId, msg.repositoryFullName);
break;
default:
assertNever(msg);
}

View File

@@ -1,5 +1,5 @@
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 {
@@ -11,6 +11,7 @@ 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
@@ -82,12 +83,36 @@ export const RepoRow = ({
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>
@@ -107,7 +132,7 @@ export const RepoRow = ({
</span>
{downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.InProgress && <LoadingIcon label="Downloading" />}
</TitleContainer>
{isExpanded && status &&
{isExpanded && expandableContentLoaded &&
<AnalyzedRepoItemContent status={status} interpretedResults={interpretedResults} rawResults={rawResults} />}
</div>
);

View File

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

View File

@@ -14,3 +14,11 @@ Object.defineProperty(window, 'matchMedia', {
dispatchEvent: jest.fn(),
})),
});
// Store this on the window so we can mock it
(window as any).vsCodeApi = {
postMessage: jest.fn(),
setState: jest.fn(),
};
(window as any).acquireVsCodeApi = () => (window as any).vsCodeApi;