Files
vscode-codeql/extensions/ql-vscode/src/remote-queries/export-results.ts
Elena Tanasoiu cc907d2f31 Add test for exportResultsToGist method
While we're here we're also adding a test for the `exportResultsToGist`
method, as there were no tests for the `export-results.ts` file.

We initially attempted to add the test to the pure-tests folder, but the
`export-results.ts` file imports some components from `vscode`, which
meant we needed to set up the test in an environment where VSCode
dependencies are available.

We chose to add the test to `vscode-tests/no-workspace` for convenience,
as there are already other unit tests there.

We've also had to import our own query and analysis result to be able
to work with data closer to reality for exported results.

Since we've introduced functionality to build a gist title, let's check
that the `exportResultsToGist` method will forward the correct title to
the GitHub Actions API.

Co-authored-by: Shati Patel <shati-patel@github.com>
2022-07-18 19:52:51 +01:00

141 lines
5.5 KiB
TypeScript

import * as path from 'path';
import * as fs from 'fs-extra';
import { window, commands, Uri, ExtensionContext, QuickPickItem, workspace, ViewColumn } from 'vscode';
import { Credentials } from '../authentication';
import { UserCancellationException } from '../commandRunner';
import { showInformationMessageWithAction } from '../helpers';
import { logger } from '../logging';
import { QueryHistoryManager } from '../query-history';
import { createGist } from './gh-actions-api-client';
import { RemoteQueriesManager } from './remote-queries-manager';
import { generateMarkdown } from './remote-queries-markdown-generation';
import { RemoteQuery } from './remote-query';
import { AnalysisResults, sumAnalysesResults } from './shared/analysis-result';
/**
* Exports the results of the currently-selected remote query.
* The user is prompted to select the export format.
*/
export async function exportRemoteQueryResults(
queryHistoryManager: QueryHistoryManager,
remoteQueriesManager: RemoteQueriesManager,
ctx: ExtensionContext,
): Promise<void> {
const queryHistoryItem = queryHistoryManager.getCurrentQueryHistoryItem();
if (!queryHistoryItem || queryHistoryItem.t !== 'remote') {
throw new Error('No variant analysis results currently open. To open results, click an item in the query history view.');
} else if (!queryHistoryItem.completed) {
throw new Error('Variant analysis results are not yet available.');
}
const queryId = queryHistoryItem.queryId;
void logger.log(`Exporting variant analysis results for query: ${queryId}`);
const query = queryHistoryItem.remoteQuery;
const analysesResults = remoteQueriesManager.getAnalysesResults(queryId);
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,
{
placeHolder: 'Select export format',
canPickMany: false,
ignoreFocusOut: true,
}
);
if (!exportFormat || !exportFormat.label) {
throw new UserCancellationException('No export format selected', true);
}
return exportFormat;
}
/**
* Converts the results of a remote query to markdown and uploads the files as a secret gist.
*/
export async function exportResultsToGist(
ctx: ExtensionContext,
query: RemoteQuery,
analysesResults: AnalysisResults[]
): Promise<void> {
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') };
return acc;
}, {} as { [key: string]: { content: string } });
const gistUrl = await createGist(credentials, description, gistFiles);
if (gistUrl) {
const shouldOpenGist = await showInformationMessageWithAction(
'Variant analysis results exported to gist.',
'Open gist'
);
if (shouldOpenGist) {
await commands.executeCommand('vscode.open', Uri.parse(gistUrl));
}
}
}
/**
* Builds Gist description
* Ex: Empty Block (Go) x results (y repositories)
*/
const buildGistDescription = (query: RemoteQuery, analysesResults: AnalysisResults[]) => {
const resultCount = sumAnalysesResults(analysesResults);
const repositoryLabel = `${query.numRepositoriesQueried} ${query.numRepositoriesQueried === 1 ? 'repository' : 'repositories'}`;
const repositoryCount = query.numRepositoriesQueried ? repositoryLabel : '';
return `${query.queryName} (${query.language}) ${resultCount} results (${repositoryCount})`;
};
/**
* 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).
*/
async function exportResultsToLocalMarkdown(
queryDirectoryPath: string,
query: RemoteQuery,
analysesResults: AnalysisResults[]
) {
const markdownFiles = generateMarkdown(query, analysesResults, 'local');
const exportedResultsPath = path.join(queryDirectoryPath, 'exported-results');
await fs.ensureDir(exportedResultsPath);
for (const markdownFile of markdownFiles) {
const filePath = path.join(exportedResultsPath, `${markdownFile.fileName}.md`);
await fs.writeFile(filePath, markdownFile.content.join('\n'), 'utf8');
}
const shouldOpenExportedResults = await showInformationMessageWithAction(
`Variant analysis results exported to \"${exportedResultsPath}\".`,
'Open exported results'
);
if (shouldOpenExportedResults) {
const summaryFilePath = path.join(exportedResultsPath, '_summary.md');
const summaryFile = await workspace.openTextDocument(summaryFilePath);
await window.showTextDocument(summaryFile, ViewColumn.One);
await commands.executeCommand('revealFileInOS', Uri.file(summaryFilePath));
}
}