Merge pull request #1750 from github/koesie10/export-results
Add exporting of results from the context menu
This commit is contained in:
@@ -325,7 +325,7 @@
|
||||
"title": "CodeQL: Run Variant Analysis"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.exportVariantAnalysisResults",
|
||||
"command": "codeQL.exportSelectedVariantAnalysisResults",
|
||||
"title": "CodeQL: Export Variant Analysis Results"
|
||||
},
|
||||
{
|
||||
@@ -954,7 +954,7 @@
|
||||
"when": "config.codeQL.canary && config.codeQL.variantAnalysis.liveResults"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.exportVariantAnalysisResults",
|
||||
"command": "codeQL.exportSelectedVariantAnalysisResults",
|
||||
"when": "config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -98,7 +98,11 @@ import { RemoteQueryResult } from './remote-queries/remote-query-result';
|
||||
import { URLSearchParams } from 'url';
|
||||
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
|
||||
import { HistoryItemLabelProvider } from './history-item-label-provider';
|
||||
import { exportRemoteQueryResults } from './remote-queries/export-results';
|
||||
import {
|
||||
exportRemoteQueryResults,
|
||||
exportSelectedRemoteQueryResults,
|
||||
exportVariantAnalysisResults
|
||||
} from './remote-queries/export-results';
|
||||
import { RemoteQuery } from './remote-queries/remote-query';
|
||||
import { EvalLogViewer } from './eval-log-viewer';
|
||||
import { SummaryLanguageSupport } from './log-insights/summary-language-support';
|
||||
@@ -992,11 +996,23 @@ async function activateWithInstalledDistribution(
|
||||
}));
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.exportVariantAnalysisResults', async (queryId?: string) => {
|
||||
commandRunner('codeQL.exportSelectedVariantAnalysisResults', async () => {
|
||||
await exportSelectedRemoteQueryResults(qhm);
|
||||
})
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.exportRemoteQueryResults', async (queryId: string) => {
|
||||
await exportRemoteQueryResults(qhm, rqm, ctx, queryId);
|
||||
})
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.exportVariantAnalysisResults', async (variantAnalysisId: number) => {
|
||||
await exportVariantAnalysisResults(ctx, variantAnalysisManager, variantAnalysisId);
|
||||
})
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.loadVariantAnalysisRepoResults', async (variantAnalysisId: number, repositoryFullName: string) => {
|
||||
await variantAnalysisManager.loadResults(variantAnalysisId, repositoryFullName);
|
||||
|
||||
@@ -1267,8 +1267,22 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}
|
||||
}
|
||||
|
||||
async handleExportResults(): Promise<void> {
|
||||
await commands.executeCommand('codeQL.exportVariantAnalysisResults');
|
||||
async handleExportResults(
|
||||
singleItem: QueryHistoryInfo,
|
||||
multiSelect: QueryHistoryInfo[],
|
||||
): Promise<void> {
|
||||
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
|
||||
|
||||
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remote queries and variant analysis only
|
||||
if (finalSingleItem.t === 'remote') {
|
||||
await commands.executeCommand('codeQL.exportRemoteQueryResults', finalSingleItem.queryId);
|
||||
} else if (finalSingleItem.t === 'variant-analysis') {
|
||||
await commands.executeCommand('codeQL.exportVariantAnalysisResults', finalSingleItem.variantAnalysis.id);
|
||||
}
|
||||
}
|
||||
|
||||
addQuery(item: QueryHistoryInfo) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
import { window, commands, Uri, ExtensionContext, QuickPickItem, workspace, ViewColumn } from 'vscode';
|
||||
import { window, commands, Uri, ExtensionContext, workspace, ViewColumn } from 'vscode';
|
||||
import { Credentials } from '../authentication';
|
||||
import { UserCancellationException } from '../commandRunner';
|
||||
import { showInformationMessageWithAction } from '../helpers';
|
||||
@@ -9,36 +9,54 @@ import { logger } from '../logging';
|
||||
import { QueryHistoryManager } from '../query-history';
|
||||
import { createGist } from './gh-api/gh-api-client';
|
||||
import { RemoteQueriesManager } from './remote-queries-manager';
|
||||
import { generateMarkdown } from './remote-queries-markdown-generation';
|
||||
import {
|
||||
generateMarkdown,
|
||||
generateVariantAnalysisMarkdown,
|
||||
MarkdownFile,
|
||||
} from './remote-queries-markdown-generation';
|
||||
import { RemoteQuery } from './remote-query';
|
||||
import { AnalysisResults, sumAnalysesResults } from './shared/analysis-result';
|
||||
import { RemoteQueryHistoryItem } from './remote-query-history-item';
|
||||
import { pluralize } from '../pure/word';
|
||||
import { VariantAnalysisManager } from './variant-analysis-manager';
|
||||
import { assertNever } from '../pure/helpers-pure';
|
||||
import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepository,
|
||||
VariantAnalysisScannedRepositoryResult
|
||||
} from './shared/variant-analysis';
|
||||
|
||||
/**
|
||||
* Exports the results of the given or currently-selected remote query.
|
||||
* Exports the results of the currently-selected remote query or variant analysis.
|
||||
*/
|
||||
export async function exportSelectedRemoteQueryResults(queryHistoryManager: QueryHistoryManager): Promise<void> {
|
||||
const queryHistoryItem = queryHistoryManager.getCurrentQueryHistoryItem();
|
||||
if (!queryHistoryItem || queryHistoryItem.t === 'local') {
|
||||
throw new Error('No variant analysis results currently open. To open results, click an item in the query history view.');
|
||||
}
|
||||
|
||||
if (queryHistoryItem.t === 'remote') {
|
||||
return commands.executeCommand('codeQL.exportRemoteQueryResults', queryHistoryItem.queryId);
|
||||
} else if (queryHistoryItem.t === 'variant-analysis') {
|
||||
return commands.executeCommand('codeQL.exportVariantAnalysisResults', queryHistoryItem.variantAnalysis.id);
|
||||
} else {
|
||||
assertNever(queryHistoryItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the results of the given remote query.
|
||||
* The user is prompted to select the export format.
|
||||
*/
|
||||
export async function exportRemoteQueryResults(
|
||||
queryHistoryManager: QueryHistoryManager,
|
||||
remoteQueriesManager: RemoteQueriesManager,
|
||||
ctx: ExtensionContext,
|
||||
queryId?: string,
|
||||
queryId: string,
|
||||
): Promise<void> {
|
||||
let queryHistoryItem: RemoteQueryHistoryItem;
|
||||
if (queryId) {
|
||||
const query = queryHistoryManager.getRemoteQueryById(queryId);
|
||||
if (!query) {
|
||||
void logger.log(`Could not find query with id ${queryId}`);
|
||||
throw new Error('There was an error when trying to retrieve variant analysis information');
|
||||
}
|
||||
queryHistoryItem = query;
|
||||
} else {
|
||||
const query = queryHistoryManager.getCurrentQueryHistoryItem();
|
||||
if (!query || query.t !== 'remote') {
|
||||
throw new Error('No variant analysis results currently open. To open results, click an item in the query history view.');
|
||||
}
|
||||
queryHistoryItem = query;
|
||||
const queryHistoryItem = queryHistoryManager.getRemoteQueryById(queryId);
|
||||
if (!queryHistoryItem) {
|
||||
void logger.log(`Could not find query with id ${queryId}`);
|
||||
throw new Error('There was an error when trying to retrieve variant analysis information');
|
||||
}
|
||||
|
||||
if (!queryHistoryItem.completed) {
|
||||
@@ -49,32 +67,107 @@ export async function exportRemoteQueryResults(
|
||||
const query = queryHistoryItem.remoteQuery;
|
||||
const analysesResults = remoteQueriesManager.getAnalysesResults(queryHistoryItem.queryId);
|
||||
|
||||
const exportFormat = await determineExportFormat();
|
||||
if (!exportFormat) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exportDirectory = await queryHistoryManager.getQueryHistoryItemDirectory(queryHistoryItem);
|
||||
|
||||
await exportRemoteQueryAnalysisResults(ctx, exportDirectory, query, analysesResults, exportFormat);
|
||||
}
|
||||
|
||||
export async function exportRemoteQueryAnalysisResults(
|
||||
ctx: ExtensionContext,
|
||||
exportDirectory: string,
|
||||
query: RemoteQuery,
|
||||
analysesResults: AnalysisResults[],
|
||||
exportFormat: 'gist' | 'local',
|
||||
) {
|
||||
const description = buildGistDescription(query, analysesResults);
|
||||
const markdownFiles = generateMarkdown(query, analysesResults, exportFormat);
|
||||
|
||||
await exportResults(ctx, exportDirectory, description, markdownFiles, exportFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the results of the given or currently-selected remote query.
|
||||
* The user is prompted to select the export format.
|
||||
*/
|
||||
export async function exportVariantAnalysisResults(
|
||||
ctx: ExtensionContext,
|
||||
variantAnalysisManager: VariantAnalysisManager,
|
||||
variantAnalysisId: number,
|
||||
): Promise<void> {
|
||||
const variantAnalysis = await variantAnalysisManager.getVariantAnalysis(variantAnalysisId);
|
||||
if (!variantAnalysis) {
|
||||
void logger.log(`Could not find variant analysis with id ${variantAnalysisId}`);
|
||||
throw new Error('There was an error when trying to retrieve variant analysis information');
|
||||
}
|
||||
|
||||
void logger.log(`Exporting variant analysis results for variant analysis with id ${variantAnalysis.id}`);
|
||||
|
||||
const exportFormat = await determineExportFormat();
|
||||
if (!exportFormat) {
|
||||
return;
|
||||
}
|
||||
|
||||
async function* getAnalysesResults(): AsyncGenerator<[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]> {
|
||||
if (!variantAnalysis?.scannedRepos) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const repo of variantAnalysis.scannedRepos) {
|
||||
if (repo.resultCount == 0) {
|
||||
yield [repo, {
|
||||
variantAnalysisId: variantAnalysis.id,
|
||||
repositoryId: repo.repository.id,
|
||||
}];
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = await variantAnalysisManager.loadResults(variantAnalysis.id, repo.repository.fullName, {
|
||||
skipCacheStore: true,
|
||||
});
|
||||
|
||||
yield [repo, result];
|
||||
}
|
||||
}
|
||||
|
||||
const exportDirectory = variantAnalysisManager.getVariantAnalysisStorageLocation(variantAnalysis.id);
|
||||
|
||||
await exportVariantAnalysisAnalysisResults(ctx, exportDirectory, variantAnalysis, getAnalysesResults(), exportFormat);
|
||||
}
|
||||
|
||||
export async function exportVariantAnalysisAnalysisResults(
|
||||
ctx: ExtensionContext,
|
||||
exportDirectory: string,
|
||||
variantAnalysis: VariantAnalysis,
|
||||
analysesResults: AsyncIterable<[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]>,
|
||||
exportFormat: 'gist' | 'local',
|
||||
) {
|
||||
const description = buildVariantAnalysisGistDescription(variantAnalysis);
|
||||
const markdownFiles = await generateVariantAnalysisMarkdown(variantAnalysis, analysesResults, 'gist');
|
||||
|
||||
await exportResults(ctx, exportDirectory, description, markdownFiles, exportFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the format in which to export the results, from the given export options.
|
||||
*/
|
||||
async function determineExportFormat(): Promise<'gist' | 'local' | undefined> {
|
||||
const gistOption = {
|
||||
label: '$(ports-open-browser-icon) Create Gist (GitHub)',
|
||||
};
|
||||
const localMarkdownOption = {
|
||||
label: '$(markdown) Save as markdown',
|
||||
};
|
||||
const exportFormat = await determineExportFormat(gistOption, localMarkdownOption);
|
||||
|
||||
if (exportFormat === gistOption) {
|
||||
await exportResultsToGist(ctx, query, analysesResults);
|
||||
} else if (exportFormat === localMarkdownOption) {
|
||||
const queryDirectoryPath = await queryHistoryManager.getQueryHistoryItemDirectory(
|
||||
queryHistoryItem
|
||||
);
|
||||
await exportResultsToLocalMarkdown(queryDirectoryPath, query, analysesResults);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the format in which to export the results, from the given export options.
|
||||
*/
|
||||
async function determineExportFormat(
|
||||
...options: { label: string }[]
|
||||
): Promise<QuickPickItem> {
|
||||
const exportFormat = await window.showQuickPick(
|
||||
options,
|
||||
[
|
||||
gistOption,
|
||||
localMarkdownOption,
|
||||
],
|
||||
{
|
||||
placeHolder: 'Select export format',
|
||||
canPickMany: false,
|
||||
@@ -84,20 +177,38 @@ async function determineExportFormat(
|
||||
if (!exportFormat || !exportFormat.label) {
|
||||
throw new UserCancellationException('No export format selected', true);
|
||||
}
|
||||
return exportFormat;
|
||||
|
||||
if (exportFormat === gistOption) {
|
||||
return 'gist';
|
||||
}
|
||||
if (exportFormat === localMarkdownOption) {
|
||||
return 'local';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the results of a remote query to markdown and uploads the files as a secret gist.
|
||||
*/
|
||||
export async function exportResultsToGist(
|
||||
export async function exportResults(
|
||||
ctx: ExtensionContext,
|
||||
query: RemoteQuery,
|
||||
analysesResults: AnalysisResults[]
|
||||
): Promise<void> {
|
||||
exportDirectory: string,
|
||||
description: string,
|
||||
markdownFiles: MarkdownFile[],
|
||||
exportFormat: 'gist' | 'local',
|
||||
) {
|
||||
if (exportFormat === 'gist') {
|
||||
await exportToGist(ctx, description, markdownFiles);
|
||||
} else if (exportFormat === 'local') {
|
||||
await exportToLocalMarkdown(exportDirectory, markdownFiles);
|
||||
}
|
||||
}
|
||||
|
||||
export async function exportToGist(
|
||||
ctx: ExtensionContext,
|
||||
description: string,
|
||||
markdownFiles: MarkdownFile[]
|
||||
) {
|
||||
const credentials = await Credentials.initialize(ctx);
|
||||
const description = buildGistDescription(query, analysesResults);
|
||||
const markdownFiles = generateMarkdown(query, analysesResults, 'gist');
|
||||
|
||||
// Convert markdownFiles to the appropriate format for uploading to gist
|
||||
const gistFiles = markdownFiles.reduce((acc, cur) => {
|
||||
acc[`${cur.fileName}.md`] = { content: cur.content.join('\n') };
|
||||
@@ -128,16 +239,25 @@ const buildGistDescription = (query: RemoteQuery, analysesResults: AnalysisResul
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the results of a remote query to markdown and saves the files locally
|
||||
* in the query directory (where query results and metadata are also saved).
|
||||
* Builds Gist description
|
||||
* Ex: Empty Block (Go) x results (y repositories)
|
||||
*/
|
||||
async function exportResultsToLocalMarkdown(
|
||||
queryDirectoryPath: string,
|
||||
query: RemoteQuery,
|
||||
analysesResults: AnalysisResults[]
|
||||
const buildVariantAnalysisGistDescription = (variantAnalysis: VariantAnalysis) => {
|
||||
const resultCount = variantAnalysis.scannedRepos?.reduce((acc, item) => acc + (item.resultCount ?? 0), 0) ?? 0;
|
||||
const resultLabel = pluralize(resultCount, 'result', 'results');
|
||||
|
||||
const repositoryLabel = variantAnalysis.scannedRepos?.length ? `(${pluralize(variantAnalysis.scannedRepos.length, 'repository', 'repositories')})` : '';
|
||||
return `${variantAnalysis.query.name} (${variantAnalysis.query.language}) ${resultLabel} ${repositoryLabel}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves the results of an exported query to local markdown files.
|
||||
*/
|
||||
async function exportToLocalMarkdown(
|
||||
exportDirectory: string,
|
||||
markdownFiles: MarkdownFile[],
|
||||
) {
|
||||
const markdownFiles = generateMarkdown(query, analysesResults, 'local');
|
||||
const exportedResultsPath = path.join(queryDirectoryPath, 'exported-results');
|
||||
const exportedResultsPath = path.join(exportDirectory, 'exported-results');
|
||||
await fs.ensureDir(exportedResultsPath);
|
||||
for (const markdownFile of markdownFiles) {
|
||||
const filePath = path.join(exportedResultsPath, `${markdownFile.fileName}.md`);
|
||||
|
||||
@@ -5,6 +5,11 @@ import { parseHighlightedLine, shouldHighlightLine } from '../pure/sarif-utils';
|
||||
import { convertNonPrintableChars } from '../text-utils';
|
||||
import { RemoteQuery } from './remote-query';
|
||||
import { AnalysisAlert, AnalysisRawResults, AnalysisResults, CodeSnippet, FileLink, getAnalysisResultCount, HighlightedRegion } from './shared/analysis-result';
|
||||
import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepository,
|
||||
VariantAnalysisScannedRepositoryResult
|
||||
} from './shared/variant-analysis';
|
||||
|
||||
export type MarkdownLinkType = 'local' | 'gist';
|
||||
|
||||
@@ -57,6 +62,51 @@ export function generateMarkdown(
|
||||
return [summaryFile, ...resultsFiles];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates markdown files with variant analysis results.
|
||||
*/
|
||||
export async function generateVariantAnalysisMarkdown(
|
||||
variantAnalysis: VariantAnalysis,
|
||||
results: AsyncIterable<[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]>,
|
||||
linkType: MarkdownLinkType
|
||||
): Promise<MarkdownFile[]> {
|
||||
const resultsFiles: MarkdownFile[] = [];
|
||||
// Generate summary file with links to individual files
|
||||
const summaryFile: MarkdownFile = generateVariantAnalysisMarkdownSummary(variantAnalysis);
|
||||
for await (const [scannedRepo, result] of results) {
|
||||
if (scannedRepo.resultCount === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Append nwo and results count to the summary table
|
||||
const fullName = scannedRepo.repository.fullName;
|
||||
const fileName = createFileName(fullName);
|
||||
const link = createRelativeLink(fileName, linkType);
|
||||
summaryFile.content.push(`| ${fullName} | [${scannedRepo.resultCount} result(s)](${link}) |`);
|
||||
|
||||
// Generate individual markdown file for each repository
|
||||
const resultsFileContent = [
|
||||
`### ${scannedRepo.repository.fullName}`,
|
||||
''
|
||||
];
|
||||
if (result.interpretedResults) {
|
||||
for (const interpretedResult of result.interpretedResults) {
|
||||
const individualResult = generateMarkdownForInterpretedResult(interpretedResult, variantAnalysis.query.language);
|
||||
resultsFileContent.push(...individualResult);
|
||||
}
|
||||
}
|
||||
if (result.rawResults) {
|
||||
const rawResultTable = generateMarkdownForRawResults(result.rawResults);
|
||||
resultsFileContent.push(...rawResultTable);
|
||||
}
|
||||
resultsFiles.push({
|
||||
fileName: fileName,
|
||||
content: resultsFileContent,
|
||||
});
|
||||
}
|
||||
return [summaryFile, ...resultsFiles];
|
||||
}
|
||||
|
||||
export function generateMarkdownSummary(query: RemoteQuery): MarkdownFile {
|
||||
const lines: string[] = [];
|
||||
// Title
|
||||
@@ -95,6 +145,44 @@ export function generateMarkdownSummary(query: RemoteQuery): MarkdownFile {
|
||||
};
|
||||
}
|
||||
|
||||
export function generateVariantAnalysisMarkdownSummary(variantAnalysis: VariantAnalysis): MarkdownFile {
|
||||
const lines: string[] = [];
|
||||
// Title
|
||||
lines.push(
|
||||
`### Results for "${variantAnalysis.query.name}"`,
|
||||
''
|
||||
);
|
||||
|
||||
// Expandable section containing query text
|
||||
const queryCodeBlock = [
|
||||
'```ql',
|
||||
...variantAnalysis.query.text.split('\n'),
|
||||
'```',
|
||||
];
|
||||
lines.push(
|
||||
...buildExpandableMarkdownSection('Query', queryCodeBlock)
|
||||
);
|
||||
|
||||
// Padding between sections
|
||||
lines.push(
|
||||
'<br />',
|
||||
'',
|
||||
);
|
||||
|
||||
// Summary table
|
||||
lines.push(
|
||||
'### Summary',
|
||||
'',
|
||||
'| Repository | Results |',
|
||||
'| --- | --- |',
|
||||
);
|
||||
// nwo and result count will be appended to this table
|
||||
return {
|
||||
fileName: '_summary',
|
||||
content: lines
|
||||
};
|
||||
}
|
||||
|
||||
function generateMarkdownForInterpretedResult(interpretedResult: AnalysisAlert, language: string): string[] {
|
||||
const lines: string[] = [];
|
||||
lines.push(createMarkdownRemoteFileRef(
|
||||
@@ -296,11 +384,11 @@ export function createMarkdownRemoteFileRef(
|
||||
|
||||
/**
|
||||
* Builds an expandable markdown section of the form:
|
||||
* <details>
|
||||
* <details>
|
||||
* <summary>title</summary>
|
||||
*
|
||||
*
|
||||
* contents
|
||||
*
|
||||
*
|
||||
* </details>
|
||||
*/
|
||||
function buildExpandableMarkdownSection(title: string, contents: string[]): string[] {
|
||||
|
||||
@@ -146,7 +146,7 @@ export class RemoteQueriesView extends AbstractWebview<ToRemoteQueriesMessage, F
|
||||
await this.downloadAllAnalysesResults(msg);
|
||||
break;
|
||||
case 'remoteQueryExportResults':
|
||||
await commands.executeCommand('codeQL.exportVariantAnalysisResults', msg.queryId);
|
||||
await commands.executeCommand('codeQL.exportRemoteQueryResults', msg.queryId);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
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 { LoadResultsOptions, VariantAnalysisResultsManager } from './variant-analysis-results-manager';
|
||||
import { getControllerRepo, getQueryName, prepareRemoteQueryRun } from './run-remote-query';
|
||||
import {
|
||||
processUpdatedVariantAnalysis,
|
||||
@@ -209,13 +209,13 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
|
||||
return this.variantAnalyses.size;
|
||||
}
|
||||
|
||||
public async loadResults(variantAnalysisId: number, repositoryFullName: string): Promise<void> {
|
||||
public async loadResults(variantAnalysisId: number, repositoryFullName: string, options?: LoadResultsOptions): Promise<VariantAnalysisScannedRepositoryResult> {
|
||||
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);
|
||||
return this.variantAnalysisResultsManager.loadResults(variantAnalysisId, this.getVariantAnalysisStorageLocation(variantAnalysisId), repositoryFullName, options);
|
||||
}
|
||||
|
||||
private async variantAnalysisRecordExists(variantAnalysisId: number): Promise<boolean> {
|
||||
|
||||
@@ -28,6 +28,12 @@ export type ResultDownloadedEvent = {
|
||||
repoTask: VariantAnalysisRepositoryTask;
|
||||
}
|
||||
|
||||
export type LoadResultsOptions = {
|
||||
// If true, when results are loaded from storage, they will not be stored in the cache. This reduces memory usage if
|
||||
// results are only needed temporarily (e.g. for exporting results to a different format).
|
||||
skipCacheStore?: boolean;
|
||||
}
|
||||
|
||||
export class VariantAnalysisResultsManager extends DisposableObject {
|
||||
private static readonly REPO_TASK_FILENAME = 'repo_task.json';
|
||||
private static readonly RESULTS_DIRECTORY = 'results';
|
||||
@@ -86,11 +92,19 @@ export class VariantAnalysisResultsManager extends DisposableObject {
|
||||
public async loadResults(
|
||||
variantAnalysisId: number,
|
||||
variantAnalysisStoragePath: string,
|
||||
repositoryFullName: string
|
||||
repositoryFullName: string,
|
||||
options?: LoadResultsOptions,
|
||||
): Promise<VariantAnalysisScannedRepositoryResult> {
|
||||
const result = this.cachedResults.get(createCacheKey(variantAnalysisId, repositoryFullName));
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return result ?? await this.loadResultsIntoMemory(variantAnalysisId, variantAnalysisStoragePath, repositoryFullName);
|
||||
if (options?.skipCacheStore) {
|
||||
return this.loadResultsFromStorage(variantAnalysisId, variantAnalysisStoragePath, repositoryFullName);
|
||||
}
|
||||
|
||||
return this.loadResultsIntoMemory(variantAnalysisId, variantAnalysisStoragePath, repositoryFullName);
|
||||
}
|
||||
|
||||
private async loadResultsIntoMemory(
|
||||
|
||||
@@ -495,6 +495,96 @@ describe('query-history', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCopyRepoList', () => {
|
||||
it('should not call a command for a local query', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(localQueryHistory);
|
||||
|
||||
const item = localQueryHistory[4];
|
||||
await queryHistoryManager.handleCopyRepoList(item, [item]);
|
||||
|
||||
expect(executeCommandSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('should copy repo list for a single remote query', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
const item = remoteQueryHistory[1];
|
||||
await queryHistoryManager.handleCopyRepoList(item, [item]);
|
||||
expect(executeCommandSpy).to.have.been.calledWith('codeQL.copyRepoList', item.queryId);
|
||||
});
|
||||
|
||||
it('should not copy repo list for multiple remote queries', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
const item1 = remoteQueryHistory[1];
|
||||
const item2 = remoteQueryHistory[3];
|
||||
await queryHistoryManager.handleCopyRepoList(item1, [item1, item2]);
|
||||
expect(executeCommandSpy).not.to.have.been.called;
|
||||
});
|
||||
|
||||
it('should copy repo list for a single variant analysis', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
const item = variantAnalysisHistory[1];
|
||||
await queryHistoryManager.handleCopyRepoList(item, [item]);
|
||||
expect(executeCommandSpy).to.have.been.calledWith('codeQL.copyVariantAnalysisRepoList', item.variantAnalysis.id);
|
||||
});
|
||||
|
||||
it('should not copy repo list for multiple variant analyses', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
const item1 = variantAnalysisHistory[1];
|
||||
const item2 = variantAnalysisHistory[3];
|
||||
await queryHistoryManager.handleCopyRepoList(item1, [item1, item2]);
|
||||
expect(executeCommandSpy).not.to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleExportResults', () => {
|
||||
it('should not call a command for a local query', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(localQueryHistory);
|
||||
|
||||
const item = localQueryHistory[4];
|
||||
await queryHistoryManager.handleExportResults(item, [item]);
|
||||
|
||||
expect(executeCommandSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('should export results for a single remote query', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
const item = remoteQueryHistory[1];
|
||||
await queryHistoryManager.handleExportResults(item, [item]);
|
||||
expect(executeCommandSpy).to.have.been.calledWith('codeQL.exportRemoteQueryResults', item.queryId);
|
||||
});
|
||||
|
||||
it('should not export results for multiple remote queries', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
const item1 = remoteQueryHistory[1];
|
||||
const item2 = remoteQueryHistory[3];
|
||||
await queryHistoryManager.handleExportResults(item1, [item1, item2]);
|
||||
expect(executeCommandSpy).not.to.have.been.called;
|
||||
});
|
||||
|
||||
it('should export results for a single variant analysis', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
const item = variantAnalysisHistory[1];
|
||||
await queryHistoryManager.handleExportResults(item, [item]);
|
||||
expect(executeCommandSpy).to.have.been.calledWith('codeQL.exportVariantAnalysisResults', item.variantAnalysis.id);
|
||||
});
|
||||
|
||||
it('should not export results for multiple variant analyses', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
const item1 = variantAnalysisHistory[1];
|
||||
const item2 = variantAnalysisHistory[3];
|
||||
await queryHistoryManager.handleExportResults(item1, [item1, item2]);
|
||||
expect(executeCommandSpy).not.to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('determineSelection', () => {
|
||||
const singleItem = 'a';
|
||||
const multipleItems = ['b', 'c', 'd'];
|
||||
|
||||
@@ -8,12 +8,12 @@ import { createMockExtensionContext } from '../index';
|
||||
import { Credentials } from '../../../authentication';
|
||||
import { MarkdownFile } from '../../../remote-queries/remote-queries-markdown-generation';
|
||||
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
|
||||
import { exportResultsToGist } from '../../../remote-queries/export-results';
|
||||
import { exportRemoteQueryAnalysisResults } from '../../../remote-queries/export-results';
|
||||
|
||||
const proxyquire = pq.noPreserveCache();
|
||||
|
||||
describe('export results', async function() {
|
||||
describe('exportResultsToGist', async function() {
|
||||
describe('exportRemoteQueryAnalysisResults', async function() {
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
let mockCredentials: Credentials;
|
||||
let mockResponse: sinon.SinonStub<any, Promise<{ status: number }>>;
|
||||
@@ -47,7 +47,7 @@ describe('export results', async function() {
|
||||
const query = JSON.parse(await fs.readFile(path.join(__dirname, '../data/remote-queries/query-with-results/query.json'), 'utf8'));
|
||||
const analysesResults = JSON.parse(await fs.readFile(path.join(__dirname, '../data/remote-queries/query-with-results/analyses-results.json'), 'utf8'));
|
||||
|
||||
await exportResultsToGist(ctx, query, analysesResults);
|
||||
await exportRemoteQueryAnalysisResults(ctx, '', query, analysesResults, 'gist');
|
||||
|
||||
expect(mockCreateGist.calledOnce).to.be.true;
|
||||
expect(mockCreateGist.firstCall.args[1]).to.equal('Shell command built from environment values (javascript) 3 results (10 repositories)');
|
||||
|
||||
Reference in New Issue
Block a user