Add exporting of variant analysis results
This adds the export of variant analysis results. This is unfortunately a larger change than I would have liked because there are many differences in the types and I think further unification of the code might make it less clear and would actually make this code harder to read when the remote queries code is removed. In general, the idea for the export of a variant analysis follows the same process as the export of remote queries, with the difference being that variant analysis results are loaded on-the-fly from dis, rather than only loading from memory. This means it should use less memory, but it also means that the export is slower.
This commit is contained in:
@@ -97,7 +97,11 @@ import { RemoteQueryResult } from './remote-queries/remote-query-result';
|
|||||||
import { URLSearchParams } from 'url';
|
import { URLSearchParams } from 'url';
|
||||||
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
|
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
|
||||||
import { HistoryItemLabelProvider } from './history-item-label-provider';
|
import { HistoryItemLabelProvider } from './history-item-label-provider';
|
||||||
import { exportRemoteQueryResults, exportSelectedRemoteQueryResults } from './remote-queries/export-results';
|
import {
|
||||||
|
exportRemoteQueryResults,
|
||||||
|
exportSelectedRemoteQueryResults,
|
||||||
|
exportVariantAnalysisResults
|
||||||
|
} from './remote-queries/export-results';
|
||||||
import { RemoteQuery } from './remote-queries/remote-query';
|
import { RemoteQuery } from './remote-queries/remote-query';
|
||||||
import { EvalLogViewer } from './eval-log-viewer';
|
import { EvalLogViewer } from './eval-log-viewer';
|
||||||
import { SummaryLanguageSupport } from './log-insights/summary-language-support';
|
import { SummaryLanguageSupport } from './log-insights/summary-language-support';
|
||||||
@@ -986,6 +990,12 @@ async function activateWithInstalledDistribution(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ctx.subscriptions.push(
|
||||||
|
commandRunner('codeQL.exportVariantAnalysisResults', async (variantAnalysisId: number) => {
|
||||||
|
await exportVariantAnalysisResults(ctx, variantAnalysisManager, variantAnalysisId);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
ctx.subscriptions.push(
|
ctx.subscriptions.push(
|
||||||
commandRunner('codeQL.loadVariantAnalysisRepoResults', async (variantAnalysisId: number, repositoryFullName: string) => {
|
commandRunner('codeQL.loadVariantAnalysisRepoResults', async (variantAnalysisId: number, repositoryFullName: string) => {
|
||||||
await variantAnalysisManager.loadResults(variantAnalysisId, repositoryFullName);
|
await variantAnalysisManager.loadResults(variantAnalysisId, repositoryFullName);
|
||||||
|
|||||||
@@ -1273,9 +1273,11 @@ export class QueryHistoryManager extends DisposableObject {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remote queries only
|
// Remote queries and variant analysis only
|
||||||
if (finalSingleItem.t === 'remote') {
|
if (finalSingleItem.t === 'remote') {
|
||||||
await commands.executeCommand('codeQL.exportRemoteQueryResults', finalSingleItem.queryId);
|
await commands.executeCommand('codeQL.exportRemoteQueryResults', finalSingleItem.queryId);
|
||||||
|
} else if (finalSingleItem.t === 'variant-analysis') {
|
||||||
|
await commands.executeCommand('codeQL.exportVariantAnalysisResults', finalSingleItem.variantAnalysis.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs-extra';
|
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 { Credentials } from '../authentication';
|
||||||
import { UserCancellationException } from '../commandRunner';
|
import { UserCancellationException } from '../commandRunner';
|
||||||
import { showInformationMessageWithAction } from '../helpers';
|
import { showInformationMessageWithAction } from '../helpers';
|
||||||
@@ -9,13 +9,24 @@ import { logger } from '../logging';
|
|||||||
import { QueryHistoryManager } from '../query-history';
|
import { QueryHistoryManager } from '../query-history';
|
||||||
import { createGist } from './gh-api/gh-api-client';
|
import { createGist } from './gh-api/gh-api-client';
|
||||||
import { RemoteQueriesManager } from './remote-queries-manager';
|
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 { RemoteQuery } from './remote-query';
|
||||||
import { AnalysisResults, sumAnalysesResults } from './shared/analysis-result';
|
import { AnalysisResults, sumAnalysesResults } from './shared/analysis-result';
|
||||||
import { pluralize } from '../pure/word';
|
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 currently-selected remote query.
|
* Exports the results of the currently-selected remote query or variant analysis.
|
||||||
*/
|
*/
|
||||||
export async function exportSelectedRemoteQueryResults(queryHistoryManager: QueryHistoryManager): Promise<void> {
|
export async function exportSelectedRemoteQueryResults(queryHistoryManager: QueryHistoryManager): Promise<void> {
|
||||||
const queryHistoryItem = queryHistoryManager.getCurrentQueryHistoryItem();
|
const queryHistoryItem = queryHistoryManager.getCurrentQueryHistoryItem();
|
||||||
@@ -23,19 +34,17 @@ export async function exportSelectedRemoteQueryResults(queryHistoryManager: Quer
|
|||||||
throw new Error('No variant analysis results currently open. To open results, click an item in the query history view.');
|
throw new Error('No variant analysis results currently open. To open results, click an item in the query history view.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!queryHistoryItem.completed) {
|
|
||||||
throw new Error('Variant analysis results are not yet available.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queryHistoryItem.t === 'remote') {
|
if (queryHistoryItem.t === 'remote') {
|
||||||
return commands.executeCommand('codeQL.exportRemoteQueryResults', queryHistoryItem.queryId);
|
return commands.executeCommand('codeQL.exportRemoteQueryResults', queryHistoryItem.queryId);
|
||||||
|
} else if (queryHistoryItem.t === 'variant-analysis') {
|
||||||
|
return commands.executeCommand('codeQL.exportVariantAnalysisResults', queryHistoryItem.variantAnalysis.id);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No variant analysis results currently open. To open results, click an item in the query history view.');
|
assertNever(queryHistoryItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exports the results of the given or currently-selected remote query.
|
* Exports the results of the given remote query.
|
||||||
* The user is prompted to select the export format.
|
* The user is prompted to select the export format.
|
||||||
*/
|
*/
|
||||||
export async function exportRemoteQueryResults(
|
export async function exportRemoteQueryResults(
|
||||||
@@ -58,32 +67,111 @@ export async function exportRemoteQueryResults(
|
|||||||
const query = queryHistoryItem.remoteQuery;
|
const query = queryHistoryItem.remoteQuery;
|
||||||
const analysesResults = remoteQueriesManager.getAnalysesResults(queryHistoryItem.queryId);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: VariantAnalysisScannedRepositoryResult;
|
||||||
|
|
||||||
|
if (!variantAnalysisManager.areResultsLoaded(variantAnalysis.id, repo.repository.fullName)) {
|
||||||
|
result = await variantAnalysisManager.loadResultsFromStorage(variantAnalysis.id, repo.repository.fullName);
|
||||||
|
} else {
|
||||||
|
result = await variantAnalysisManager.loadResults(variantAnalysis.id, repo.repository.fullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = {
|
const gistOption = {
|
||||||
label: '$(ports-open-browser-icon) Create Gist (GitHub)',
|
label: '$(ports-open-browser-icon) Create Gist (GitHub)',
|
||||||
};
|
};
|
||||||
const localMarkdownOption = {
|
const localMarkdownOption = {
|
||||||
label: '$(markdown) Save as markdown',
|
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(
|
const exportFormat = await window.showQuickPick(
|
||||||
options,
|
[
|
||||||
|
gistOption,
|
||||||
|
localMarkdownOption,
|
||||||
|
],
|
||||||
{
|
{
|
||||||
placeHolder: 'Select export format',
|
placeHolder: 'Select export format',
|
||||||
canPickMany: false,
|
canPickMany: false,
|
||||||
@@ -93,20 +181,38 @@ async function determineExportFormat(
|
|||||||
if (!exportFormat || !exportFormat.label) {
|
if (!exportFormat || !exportFormat.label) {
|
||||||
throw new UserCancellationException('No export format selected', true);
|
throw new UserCancellationException('No export format selected', true);
|
||||||
}
|
}
|
||||||
return exportFormat;
|
|
||||||
|
if (exportFormat === gistOption) {
|
||||||
|
return 'gist';
|
||||||
|
}
|
||||||
|
if (exportFormat === localMarkdownOption) {
|
||||||
|
return 'local';
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function exportResults(
|
||||||
* Converts the results of a remote query to markdown and uploads the files as a secret gist.
|
|
||||||
*/
|
|
||||||
export async function exportResultsToGist(
|
|
||||||
ctx: ExtensionContext,
|
ctx: ExtensionContext,
|
||||||
query: RemoteQuery,
|
exportDirectory: string,
|
||||||
analysesResults: AnalysisResults[]
|
description: string,
|
||||||
): Promise<void> {
|
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 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
|
// Convert markdownFiles to the appropriate format for uploading to gist
|
||||||
const gistFiles = markdownFiles.reduce((acc, cur) => {
|
const gistFiles = markdownFiles.reduce((acc, cur) => {
|
||||||
acc[`${cur.fileName}.md`] = { content: cur.content.join('\n') };
|
acc[`${cur.fileName}.md`] = { content: cur.content.join('\n') };
|
||||||
@@ -137,16 +243,25 @@ const buildGistDescription = (query: RemoteQuery, analysesResults: AnalysisResul
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the results of a remote query to markdown and saves the files locally
|
* Builds Gist description
|
||||||
* in the query directory (where query results and metadata are also saved).
|
* Ex: Empty Block (Go) x results (y repositories)
|
||||||
*/
|
*/
|
||||||
async function exportResultsToLocalMarkdown(
|
const buildVariantAnalysisGistDescription = (variantAnalysis: VariantAnalysis) => {
|
||||||
queryDirectoryPath: string,
|
const resultCount = variantAnalysis.scannedRepos?.reduce((acc, item) => acc + (item.resultCount ?? 0), 0) ?? 0;
|
||||||
query: RemoteQuery,
|
const resultLabel = pluralize(resultCount, 'result', 'results');
|
||||||
analysesResults: AnalysisResults[]
|
|
||||||
|
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(exportDirectory, 'exported-results');
|
||||||
const exportedResultsPath = path.join(queryDirectoryPath, 'exported-results');
|
|
||||||
await fs.ensureDir(exportedResultsPath);
|
await fs.ensureDir(exportedResultsPath);
|
||||||
for (const markdownFile of markdownFiles) {
|
for (const markdownFile of markdownFiles) {
|
||||||
const filePath = path.join(exportedResultsPath, `${markdownFile.fileName}.md`);
|
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 { convertNonPrintableChars } from '../text-utils';
|
||||||
import { RemoteQuery } from './remote-query';
|
import { RemoteQuery } from './remote-query';
|
||||||
import { AnalysisAlert, AnalysisRawResults, AnalysisResults, CodeSnippet, FileLink, getAnalysisResultCount, HighlightedRegion } from './shared/analysis-result';
|
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';
|
export type MarkdownLinkType = 'local' | 'gist';
|
||||||
|
|
||||||
@@ -57,6 +62,51 @@ export function generateMarkdown(
|
|||||||
return [summaryFile, ...resultsFiles];
|
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 {
|
export function generateMarkdownSummary(query: RemoteQuery): MarkdownFile {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
// Title
|
// 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[] {
|
function generateMarkdownForInterpretedResult(interpretedResult: AnalysisAlert, language: string): string[] {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
lines.push(createMarkdownRemoteFileRef(
|
lines.push(createMarkdownRemoteFileRef(
|
||||||
@@ -296,11 +384,11 @@ export function createMarkdownRemoteFileRef(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds an expandable markdown section of the form:
|
* Builds an expandable markdown section of the form:
|
||||||
* <details>
|
* <details>
|
||||||
* <summary>title</summary>
|
* <summary>title</summary>
|
||||||
*
|
*
|
||||||
* contents
|
* contents
|
||||||
*
|
*
|
||||||
* </details>
|
* </details>
|
||||||
*/
|
*/
|
||||||
function buildExpandableMarkdownSection(title: string, contents: string[]): string[] {
|
function buildExpandableMarkdownSection(title: string, contents: string[]): string[] {
|
||||||
|
|||||||
@@ -142,13 +142,34 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
|
|||||||
return this.variantAnalyses.size;
|
return this.variantAnalyses.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async loadResults(variantAnalysisId: number, repositoryFullName: string): Promise<void> {
|
public async loadResults(variantAnalysisId: number, repositoryFullName: string): Promise<VariantAnalysisScannedRepositoryResult> {
|
||||||
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
|
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
|
||||||
if (!variantAnalysis) {
|
if (!variantAnalysis) {
|
||||||
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public areResultsLoaded(variantAnalysisId: number, repositoryFullName: string): boolean {
|
||||||
|
if (!this.variantAnalyses.has(variantAnalysisId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.variantAnalysisResultsManager.areResultsLoaded(variantAnalysisId, repositoryFullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method should only be used to temporarily get the results for a variant analysis. In general, loadResults should
|
||||||
|
* be preferred.
|
||||||
|
*/
|
||||||
|
public async loadResultsFromStorage(variantAnalysisId: number, repositoryFullName: string): Promise<VariantAnalysisScannedRepositoryResult> {
|
||||||
|
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
|
||||||
|
if (!variantAnalysis) {
|
||||||
|
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.variantAnalysisResultsManager.loadResultsFromStorage(variantAnalysisId, this.getVariantAnalysisStorageLocation(variantAnalysisId), repositoryFullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async variantAnalysisRecordExists(variantAnalysisId: number): Promise<boolean> {
|
private async variantAnalysisRecordExists(variantAnalysisId: number): Promise<boolean> {
|
||||||
|
|||||||
@@ -93,6 +93,13 @@ export class VariantAnalysisResultsManager extends DisposableObject {
|
|||||||
return result ?? await this.loadResultsIntoMemory(variantAnalysisId, variantAnalysisStoragePath, repositoryFullName);
|
return result ?? await this.loadResultsIntoMemory(variantAnalysisId, variantAnalysisStoragePath, repositoryFullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public areResultsLoaded(
|
||||||
|
variantAnalysisId: number,
|
||||||
|
repositoryFullName: string
|
||||||
|
): boolean {
|
||||||
|
return this.cachedResults.has(createCacheKey(variantAnalysisId, repositoryFullName));
|
||||||
|
}
|
||||||
|
|
||||||
private async loadResultsIntoMemory(
|
private async loadResultsIntoMemory(
|
||||||
variantAnalysisId: number,
|
variantAnalysisId: number,
|
||||||
variantAnalysisStoragePath: string,
|
variantAnalysisStoragePath: string,
|
||||||
@@ -104,7 +111,7 @@ export class VariantAnalysisResultsManager extends DisposableObject {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadResultsFromStorage(
|
public async loadResultsFromStorage(
|
||||||
variantAnalysisId: number,
|
variantAnalysisId: number,
|
||||||
variantAnalysisStoragePath: string,
|
variantAnalysisStoragePath: string,
|
||||||
repositoryFullName: string,
|
repositoryFullName: string,
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import { createMockExtensionContext } from '../index';
|
|||||||
import { Credentials } from '../../../authentication';
|
import { Credentials } from '../../../authentication';
|
||||||
import { MarkdownFile } from '../../../remote-queries/remote-queries-markdown-generation';
|
import { MarkdownFile } from '../../../remote-queries/remote-queries-markdown-generation';
|
||||||
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
|
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();
|
const proxyquire = pq.noPreserveCache();
|
||||||
|
|
||||||
describe('export results', async function() {
|
describe('export results', async function() {
|
||||||
describe('exportResultsToGist', async function() {
|
describe('exportRemoteQueryAnalysisResults', async function() {
|
||||||
let sandbox: sinon.SinonSandbox;
|
let sandbox: sinon.SinonSandbox;
|
||||||
let mockCredentials: Credentials;
|
let mockCredentials: Credentials;
|
||||||
let mockResponse: sinon.SinonStub<any, Promise<{ status: number }>>;
|
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 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'));
|
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.calledOnce).to.be.true;
|
||||||
expect(mockCreateGist.firstCall.args[1]).to.equal('Shell command built from environment values (javascript) 3 results (10 repositories)');
|
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