Merge branch 'main' into aeisenberg/remote-query-save

This commit is contained in:
Andrew Eisenberg
2022-02-22 10:13:16 -08:00
20 changed files with 208 additions and 103 deletions

View File

@@ -3,6 +3,8 @@
## [UNRELEASED] ## [UNRELEASED]
- Fix a bug where database upgrades could not be resolved if some of the target pack's dependencies are outside of the workspace. [#1138](https://github.com/github/vscode-codeql/pull/1138) - Fix a bug where database upgrades could not be resolved if some of the target pack's dependencies are outside of the workspace. [#1138](https://github.com/github/vscode-codeql/pull/1138)
- Open the query server logs for query errors (instead of the extension log). This will make it easier to track down query errors. [#1158](https://github.com/github/vscode-codeql/pull/1158)
- Fix a bug where queries took a long time to run if there are no folders in the workspace. [#1157](https://github.com/github/vscode-codeql/pull/1157)
## 1.5.11 - 10 February 2022 ## 1.5.11 - 10 February 2022

View File

@@ -514,8 +514,7 @@ export class CodeQLCliServer implements Disposable {
async resolveLibraryPath(workspaces: string[], queryPath: string): Promise<QuerySetup> { async resolveLibraryPath(workspaces: string[], queryPath: string): Promise<QuerySetup> {
const subcommandArgs = [ const subcommandArgs = [
'--query', queryPath, '--query', queryPath,
'--additional-packs', ...this.getAdditionalPacksArg(workspaces)
workspaces.join(path.delimiter)
]; ];
return await this.runJsonCodeQlCliCommand<QuerySetup>(['resolve', 'library-path'], subcommandArgs, 'Resolving library paths'); return await this.runJsonCodeQlCliCommand<QuerySetup>(['resolve', 'library-path'], subcommandArgs, 'Resolving library paths');
} }
@@ -528,8 +527,7 @@ export class CodeQLCliServer implements Disposable {
const subcommandArgs = [ const subcommandArgs = [
'--format', 'bylanguage', '--format', 'bylanguage',
queryUri.fsPath, queryUri.fsPath,
'--additional-packs', ...this.getAdditionalPacksArg(workspaces)
workspaces.join(path.delimiter)
]; ];
return JSON.parse(await this.runCodeQlCliCommand(['resolve', 'queries'], subcommandArgs, 'Resolving query by language')); return JSON.parse(await this.runCodeQlCliCommand(['resolve', 'queries'], subcommandArgs, 'Resolving query by language'));
} }
@@ -562,6 +560,17 @@ export class CodeQLCliServer implements Disposable {
); );
} }
/**
* Issues an internal clear-cache command to the cli server. This
* command is used to clear the qlpack cache of the server.
*
* This cache is generally cleared every 1s. This method is used
* to force an early clearing of the cache.
*/
public async clearCache(): Promise<void> {
await this.runCodeQlCliCommand(['clear-cache'], [], 'Clearing qlpack cache');
}
/** /**
* Runs QL tests. * Runs QL tests.
* @param testPaths Full paths of the tests to run. * @param testPaths Full paths of the tests to run.
@@ -573,7 +582,7 @@ export class CodeQLCliServer implements Disposable {
): AsyncGenerator<TestCompleted, void, unknown> { ): AsyncGenerator<TestCompleted, void, unknown> {
const subcommandArgs = this.cliConfig.additionalTestArguments.concat([ const subcommandArgs = this.cliConfig.additionalTestArguments.concat([
'--additional-packs', workspaces.join(path.delimiter), ...this.getAdditionalPacksArg(workspaces),
'--threads', '--threads',
this.cliConfig.numberTestThreads.toString(), this.cliConfig.numberTestThreads.toString(),
...testPaths ...testPaths
@@ -595,8 +604,12 @@ export class CodeQLCliServer implements Disposable {
/** Resolves the ML models that should be available when evaluating a query. */ /** Resolves the ML models that should be available when evaluating a query. */
async resolveMlModels(additionalPacks: string[]): Promise<MlModelsInfo> { async resolveMlModels(additionalPacks: string[]): Promise<MlModelsInfo> {
return await this.runJsonCodeQlCliCommand<MlModelsInfo>(['resolve', 'ml-models'], ['--additional-packs', return await this.runJsonCodeQlCliCommand<MlModelsInfo>(
additionalPacks.join(path.delimiter)], 'Resolving ML models', false); ['resolve', 'ml-models'],
this.getAdditionalPacksArg(additionalPacks),
'Resolving ML models',
false
);
} }
/** /**
@@ -761,7 +774,7 @@ export class CodeQLCliServer implements Disposable {
* @returns A list of database upgrade script directories * @returns A list of database upgrade script directories
*/ */
async resolveUpgrades(dbScheme: string, searchPath: string[], allowDowngradesIfPossible: boolean, targetDbScheme?: string): Promise<UpgradesInfo> { async resolveUpgrades(dbScheme: string, searchPath: string[], allowDowngradesIfPossible: boolean, targetDbScheme?: string): Promise<UpgradesInfo> {
const args = ['--additional-packs', searchPath.join(path.delimiter), '--dbscheme', dbScheme]; const args = [...this.getAdditionalPacksArg(searchPath), '--dbscheme', dbScheme];
if (targetDbScheme) { if (targetDbScheme) {
args.push('--target-dbscheme', targetDbScheme); args.push('--target-dbscheme', targetDbScheme);
if (allowDowngradesIfPossible && await this.cliConstraints.supportsDowngrades()) { if (allowDowngradesIfPossible && await this.cliConstraints.supportsDowngrades()) {
@@ -783,7 +796,7 @@ export class CodeQLCliServer implements Disposable {
* @returns A dictionary mapping qlpack name to the directory it comes from * @returns A dictionary mapping qlpack name to the directory it comes from
*/ */
resolveQlpacks(additionalPacks: string[], searchPath?: string[]): Promise<QlpacksInfo> { resolveQlpacks(additionalPacks: string[], searchPath?: string[]): Promise<QlpacksInfo> {
const args = ['--additional-packs', additionalPacks.join(path.delimiter)]; const args = this.getAdditionalPacksArg(additionalPacks);
if (searchPath?.length) { if (searchPath?.length) {
args.push('--search-path', path.join(...searchPath)); args.push('--search-path', path.join(...searchPath));
} }
@@ -829,7 +842,7 @@ export class CodeQLCliServer implements Disposable {
* @returns A list of query files found. * @returns A list of query files found.
*/ */
async resolveQueriesInSuite(suite: string, additionalPacks: string[], searchPath?: string[]): Promise<string[]> { async resolveQueriesInSuite(suite: string, additionalPacks: string[], searchPath?: string[]): Promise<string[]> {
const args = ['--additional-packs', additionalPacks.join(path.delimiter)]; const args = this.getAdditionalPacksArg(additionalPacks);
if (searchPath !== undefined) { if (searchPath !== undefined) {
args.push('--search-path', path.join(...searchPath)); args.push('--search-path', path.join(...searchPath));
} }
@@ -862,8 +875,7 @@ export class CodeQLCliServer implements Disposable {
'-o', '-o',
outputPath, outputPath,
dir, dir,
'--additional-packs', ...this.getAdditionalPacksArg(workspaceFolders)
workspaceFolders.join(path.delimiter)
]; ];
if (!precompile && await this.cliConstraints.supportsNoPrecompile()) { if (!precompile && await this.cliConstraints.supportsNoPrecompile()) {
args.push('--no-precompile'); args.push('--no-precompile');
@@ -918,6 +930,12 @@ export class CodeQLCliServer implements Disposable {
throw new Error('No distribution found'); throw new Error('No distribution found');
} }
} }
private getAdditionalPacksArg(paths: string[]): string[] {
return paths.length
? ['--additional-packs', paths.join(path.delimiter)]
: [];
}
} }
/** /**

View File

@@ -160,7 +160,8 @@ export function commandRunner(
export function commandRunnerWithProgress<R>( export function commandRunnerWithProgress<R>(
commandId: string, commandId: string,
task: ProgressTask<R>, task: ProgressTask<R>,
progressOptions: Partial<ProgressOptions> progressOptions: Partial<ProgressOptions>,
outputLogger = logger
): Disposable { ): Disposable {
return commands.registerCommand(commandId, async (...args: any[]) => { return commands.registerCommand(commandId, async (...args: any[]) => {
const startTime = Date.now(); const startTime = Date.now();
@@ -177,9 +178,9 @@ export function commandRunnerWithProgress<R>(
if (e instanceof UserCancellationException) { if (e instanceof UserCancellationException) {
// User has cancelled this action manually // User has cancelled this action manually
if (e.silent) { if (e.silent) {
void logger.log(errorMessage); void outputLogger.log(errorMessage);
} else { } else {
void showAndLogWarningMessage(errorMessage); void showAndLogWarningMessage(errorMessage, { outputLogger });
} }
} else { } else {
// Include the full stack in the error log only. // Include the full stack in the error log only.
@@ -187,6 +188,7 @@ export function commandRunnerWithProgress<R>(
? `${errorMessage}\n${e.stack}` ? `${errorMessage}\n${e.stack}`
: errorMessage; : errorMessage;
void showAndLogErrorMessage(errorMessage, { void showAndLogErrorMessage(errorMessage, {
outputLogger,
fullMessage fullMessage
}); });
} }

View File

@@ -61,7 +61,8 @@ import {
showAndLogErrorMessage, showAndLogErrorMessage,
showAndLogWarningMessage, showAndLogWarningMessage,
showAndLogInformationMessage, showAndLogInformationMessage,
showInformationMessageWithAction showInformationMessageWithAction,
tmpDir
} from './helpers'; } from './helpers';
import { assertNever } from './pure/helpers-pure'; import { assertNever } from './pure/helpers-pure';
import { spawnIdeServer } from './ide-server'; import { spawnIdeServer } from './ide-server';
@@ -538,6 +539,7 @@ async function activateWithInstalledDistribution(
// Note we must update the query history view after showing results as the // Note we must update the query history view after showing results as the
// display and sorting might depend on the number of results // display and sorting might depend on the number of results
} catch (e) { } catch (e) {
e.message = `Error running query: ${e.message}`;
item.failureReason = e.message; item.failureReason = e.message;
throw e; throw e;
} finally { } finally {
@@ -638,7 +640,10 @@ async function activateWithInstalledDistribution(
{ {
title: 'Running query', title: 'Running query',
cancellable: true cancellable: true
} },
// Open the query server logger on error since that's usually where the interesting errors appear.
queryServerLogger
) )
); );
interface DatabaseQuickPickItem extends QuickPickItem { interface DatabaseQuickPickItem extends QuickPickItem {
@@ -770,7 +775,11 @@ async function activateWithInstalledDistribution(
{ {
title: 'Running queries', title: 'Running queries',
cancellable: true cancellable: true
}) },
// Open the query server logger on error since that's usually where the interesting errors appear.
queryServerLogger
)
); );
ctx.subscriptions.push( ctx.subscriptions.push(
commandRunnerWithProgress( commandRunnerWithProgress(
@@ -783,7 +792,10 @@ async function activateWithInstalledDistribution(
{ {
title: 'Running query', title: 'Running query',
cancellable: true cancellable: true
}) },
// Open the query server logger on error since that's usually where the interesting errors appear.
queryServerLogger
)
); );
ctx.subscriptions.push( ctx.subscriptions.push(
@@ -798,7 +810,11 @@ async function activateWithInstalledDistribution(
{ {
title: 'Running query', title: 'Running query',
cancellable: true cancellable: true
}) },
// Open the query server logger on error since that's usually where the interesting errors appear.
queryServerLogger
)
); );
ctx.subscriptions.push( ctx.subscriptions.push(
@@ -809,7 +825,10 @@ async function activateWithInstalledDistribution(
displayQuickQuery(ctx, cliServer, databaseUI, progress, token), displayQuickQuery(ctx, cliServer, databaseUI, progress, token),
{ {
title: 'Run Quick Query' title: 'Run Quick Query'
} },
// Open the query server logger on error since that's usually where the interesting errors appear.
queryServerLogger
) )
); );
@@ -1003,18 +1022,23 @@ async function activateWithInstalledDistribution(
// Jump-to-definition and find-references // Jump-to-definition and find-references
void logger.log('Registering jump-to-definition handlers.'); void logger.log('Registering jump-to-definition handlers.');
// Store contextual queries in a temporary folder so that they are removed
// when the application closes. There is no need for the user to interact with them.
const contextualQueryStorageDir = path.join(tmpDir.name, 'contextual-query-storage');
await fs.ensureDir(contextualQueryStorageDir);
languages.registerDefinitionProvider( languages.registerDefinitionProvider(
{ scheme: archiveFilesystemProvider.zipArchiveScheme }, { scheme: archiveFilesystemProvider.zipArchiveScheme },
new TemplateQueryDefinitionProvider(cliServer, qs, dbm, queryStorageDir) new TemplateQueryDefinitionProvider(cliServer, qs, dbm, contextualQueryStorageDir)
); );
languages.registerReferenceProvider( languages.registerReferenceProvider(
{ scheme: archiveFilesystemProvider.zipArchiveScheme }, { scheme: archiveFilesystemProvider.zipArchiveScheme },
new TemplateQueryReferenceProvider(cliServer, qs, dbm, queryStorageDir) new TemplateQueryReferenceProvider(cliServer, qs, dbm, contextualQueryStorageDir)
); );
const astViewer = new AstViewer(); const astViewer = new AstViewer();
const templateProvider = new TemplatePrintAstProvider(cliServer, qs, dbm, queryStorageDir); const templateProvider = new TemplatePrintAstProvider(cliServer, qs, dbm, contextualQueryStorageDir);
ctx.subscriptions.push(astViewer); ctx.subscriptions.push(astViewer);
ctx.subscriptions.push(commandRunnerWithProgress('codeQL.viewAst', async ( ctx.subscriptions.push(commandRunnerWithProgress('codeQL.viewAst', async (

View File

@@ -25,7 +25,7 @@ export class AnalysesResultsManager {
analysisSummary: AnalysisSummary, analysisSummary: AnalysisSummary,
publishResults: (analysesResults: AnalysisResults[]) => Promise<void> publishResults: (analysesResults: AnalysisResults[]) => Promise<void>
): Promise<void> { ): Promise<void> {
if (this.analysesResults.some(x => x.nwo === analysisSummary.nwo)) { if (this.isAnalysisDownloaded(analysisSummary)) {
// We already have the results for this analysis, don't download again. // We already have the results for this analysis, don't download again.
return; return;
} }
@@ -38,10 +38,13 @@ export class AnalysesResultsManager {
} }
public async downloadAnalysesResults( public async downloadAnalysesResults(
analysesToDownload: AnalysisSummary[], allAnalysesToDownload: AnalysisSummary[],
token: CancellationToken | undefined, token: CancellationToken | undefined,
publishResults: (analysesResults: AnalysisResults[]) => Promise<void> publishResults: (analysesResults: AnalysisResults[]) => Promise<void>
): Promise<void> { ): Promise<void> {
// Filter out analyses that we have already downloaded.
const analysesToDownload = allAnalysesToDownload.filter(x => !this.isAnalysisDownloaded(x));
const credentials = await Credentials.initialize(this.ctx); const credentials = await Credentials.initialize(this.ctx);
void this.logger.log('Downloading and processing analyses results'); void this.logger.log('Downloading and processing analyses results');
@@ -134,4 +137,8 @@ export class AnalysesResultsManager {
return queryResults; return queryResults;
} }
private isAnalysisDownloaded(analysis: AnalysisSummary): boolean {
return this.analysesResults.some(x => x.nwo === analysis.nwo);
}
} }

View File

@@ -7,9 +7,9 @@ import { logger } from '../logging';
import { RemoteQueryWorkflowResult } from './remote-query-workflow-result'; import { RemoteQueryWorkflowResult } from './remote-query-workflow-result';
import { DownloadLink } from './download-link'; import { DownloadLink } from './download-link';
import { RemoteQuery } from './remote-query'; import { RemoteQuery } from './remote-query';
import { RemoteQueryResultIndex, RemoteQueryResultIndexItem } from './remote-query-result-index'; import { RemoteQueryFailureIndexItem, RemoteQueryResultIndex, RemoteQuerySuccessIndexItem } from './remote-query-result-index';
interface ApiResultIndexItem { interface ApiSuccessIndexItem {
nwo: string; nwo: string;
id: string; id: string;
results_count: number; results_count: number;
@@ -17,6 +17,17 @@ interface ApiResultIndexItem {
sarif_file_size?: number; sarif_file_size?: number;
} }
interface ApiFailureIndexItem {
nwo: string;
id: string;
error: string;
}
interface ApiResultIndex {
successes: ApiSuccessIndexItem[];
failures: ApiFailureIndexItem[];
}
export async function getRemoteQueryIndex( export async function getRemoteQueryIndex(
credentials: Credentials, credentials: Credentials,
remoteQuery: RemoteQuery remoteQuery: RemoteQuery
@@ -31,9 +42,9 @@ export async function getRemoteQueryIndex(
const artifactList = await listWorkflowRunArtifacts(credentials, owner, repoName, workflowRunId); const artifactList = await listWorkflowRunArtifacts(credentials, owner, repoName, workflowRunId);
const resultIndexArtifactId = getArtifactIDfromName('result-index', workflowUri, artifactList); const resultIndexArtifactId = getArtifactIDfromName('result-index', workflowUri, artifactList);
const resultIndexItems = await getResultIndexItems(credentials, owner, repoName, resultIndexArtifactId); const resultIndex = await getResultIndex(credentials, owner, repoName, resultIndexArtifactId);
const items = resultIndexItems.map(item => { const successes = resultIndex?.successes.map(item => {
const artifactId = getArtifactIDfromName(item.id, workflowUri, artifactList); const artifactId = getArtifactIDfromName(item.id, workflowUri, artifactList);
return { return {
@@ -42,13 +53,22 @@ export async function getRemoteQueryIndex(
nwo: item.nwo, nwo: item.nwo,
resultCount: item.results_count, resultCount: item.results_count,
bqrsFileSize: item.bqrs_file_size, bqrsFileSize: item.bqrs_file_size,
sarifFileSize: item.sarif_file_size, sarifFileSize: item.sarif_file_size
} as RemoteQueryResultIndexItem; } as RemoteQuerySuccessIndexItem;
});
const failures = resultIndex?.failures.map(item => {
return {
id: item.id.toString(),
nwo: item.nwo,
error: item.error
} as RemoteQueryFailureIndexItem;
}); });
return { return {
artifactsUrlPath, artifactsUrlPath,
items successes: successes || [],
failures: failures || []
}; };
} }
@@ -81,17 +101,17 @@ export async function downloadArtifactFromLink(
* @param workflowRunId The ID of the workflow run to get the result index for. * @param workflowRunId The ID of the workflow run to get the result index for.
* @returns An object containing the result index. * @returns An object containing the result index.
*/ */
async function getResultIndexItems( async function getResultIndex(
credentials: Credentials, credentials: Credentials,
owner: string, owner: string,
repo: string, repo: string,
artifactId: number artifactId: number
): Promise<ApiResultIndexItem[]> { ): Promise<ApiResultIndex | undefined> {
const artifactPath = await downloadArtifact(credentials, owner, repo, artifactId); const artifactPath = await downloadArtifact(credentials, owner, repo, artifactId);
const indexFilePath = path.join(artifactPath, 'index.json'); const indexFilePath = path.join(artifactPath, 'index.json');
if (!(await fs.pathExists(indexFilePath))) { if (!(await fs.pathExists(indexFilePath))) {
void showAndLogWarningMessage('Could not find an `index.json` file in the result artifact.'); void showAndLogWarningMessage('Could not find an `index.json` file in the result artifact.');
return []; return undefined;
} }
const resultIndex = await fs.readFile(path.join(artifactPath, 'index.json'), 'utf8'); const resultIndex = await fs.readFile(path.join(artifactPath, 'index.json'), 'utf8');

View File

@@ -82,7 +82,8 @@ export class RemoteQueriesInterfaceManager {
totalResultCount: totalResultCount, totalResultCount: totalResultCount,
executionTimestamp: this.formatDate(query.executionStartTime), executionTimestamp: this.formatDate(query.executionStartTime),
executionDuration: executionDuration, executionDuration: executionDuration,
analysisSummaries: analysisSummaries analysisSummaries: analysisSummaries,
analysisFailures: queryResult.analysisFailures,
}; };
} }

View File

@@ -139,7 +139,8 @@ export class RemoteQueriesManager {
} }
private mapQueryResult(executionEndTime: Date, resultIndex: RemoteQueryResultIndex, queryId: string): RemoteQueryResult { private mapQueryResult(executionEndTime: Date, resultIndex: RemoteQueryResultIndex, queryId: string): RemoteQueryResult {
const analysisSummaries = resultIndex.items.map(item => ({
const analysisSummaries = resultIndex.successes.map(item => ({
nwo: item.nwo, nwo: item.nwo,
resultCount: item.resultCount, resultCount: item.resultCount,
fileSizeInBytes: item.sarifFileSize ? item.sarifFileSize : item.bqrsFileSize, fileSizeInBytes: item.sarifFileSize ? item.sarifFileSize : item.bqrsFileSize,
@@ -150,10 +151,15 @@ export class RemoteQueriesManager {
queryId, queryId,
} as DownloadLink } as DownloadLink
})); }));
const analysisFailures = resultIndex.failures.map(item => ({
nwo: item.nwo,
error: item.error
}));
return { return {
executionEndTime, executionEndTime,
analysisSummaries, analysisSummaries,
analysisFailures
}; };
} }

View File

@@ -1,9 +1,10 @@
export interface RemoteQueryResultIndex { export interface RemoteQueryResultIndex {
artifactsUrlPath: string; artifactsUrlPath: string;
items: RemoteQueryResultIndexItem[]; successes: RemoteQuerySuccessIndexItem[];
failures: RemoteQueryFailureIndexItem[];
} }
export interface RemoteQueryResultIndexItem { export interface RemoteQuerySuccessIndexItem {
id: string; id: string;
artifactId: number; artifactId: number;
nwo: string; nwo: string;
@@ -11,3 +12,10 @@ export interface RemoteQueryResultIndexItem {
bqrsFileSize: number; bqrsFileSize: number;
sarifFileSize?: number; sarifFileSize?: number;
} }
export interface RemoteQueryFailureIndexItem {
id: string;
artifactId: number;
nwo: string;
error: string;
}

View File

@@ -1,8 +1,10 @@
import { DownloadLink } from './download-link'; import { DownloadLink } from './download-link';
import { AnalysisFailure } from './shared/analysis-failure';
export interface RemoteQueryResult { export interface RemoteQueryResult {
executionEndTime: Date; executionEndTime: Date;
analysisSummaries: AnalysisSummary[]; analysisSummaries: AnalysisSummary[];
analysisFailures: AnalysisFailure[];
} }
export interface AnalysisSummary { export interface AnalysisSummary {

View File

@@ -166,6 +166,9 @@ async function generateQueryPack(cliServer: cli.CodeQLCliServer, queryFile: stri
await ensureNameAndSuite(queryPackDir, packRelativePath); await ensureNameAndSuite(queryPackDir, packRelativePath);
// Clear the cliServer cache so that the previous qlpack text is purged from the CLI.
await cliServer.clearCache();
const bundlePath = await getPackedBundlePath(queryPackDir); const bundlePath = await getPackedBundlePath(queryPackDir);
void logger.log(`Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`); void logger.log(`Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`);
await cliServer.packInstall(queryPackDir); await cliServer.packInstall(queryPackDir);
@@ -379,16 +382,12 @@ export async function attemptRerun(
) { ) {
if (typeof error.message === 'string' && error.message.includes('Some repositories were invalid')) { if (typeof error.message === 'string' && error.message.includes('Some repositories were invalid')) {
const invalidRepos = error?.response?.data?.invalid_repos || []; const invalidRepos = error?.response?.data?.invalid_repos || [];
const reposWithoutDbUploads = error?.response?.data?.repos_without_db_uploads || [];
void logger.log('Unable to run query on some of the specified repositories'); void logger.log('Unable to run query on some of the specified repositories');
if (invalidRepos.length > 0) { if (invalidRepos.length > 0) {
void logger.log(`Invalid repos: ${invalidRepos.join(', ')}`); void logger.log(`Invalid repos: ${invalidRepos.join(', ')}`);
} }
if (reposWithoutDbUploads.length > 0) {
void logger.log(`Repos without DB uploads: ${reposWithoutDbUploads.join(', ')}`);
}
if (invalidRepos.length + reposWithoutDbUploads.length === repositories.length) { if (invalidRepos.length === repositories.length) {
// Every repo is invalid in some way // Every repo is invalid in some way
void showAndLogErrorMessage('Unable to run query on any of the specified repositories.'); void showAndLogErrorMessage('Unable to run query on any of the specified repositories.');
return; return;
@@ -397,7 +396,7 @@ export async function attemptRerun(
const popupMessage = 'Unable to run query on some of the specified repositories. [See logs for more details](command:codeQL.showLogs).'; const popupMessage = 'Unable to run query on some of the specified repositories. [See logs for more details](command:codeQL.showLogs).';
const rerunQuery = await showInformationMessageWithAction(popupMessage, 'Rerun on the valid repositories only'); const rerunQuery = await showInformationMessageWithAction(popupMessage, 'Rerun on the valid repositories only');
if (rerunQuery) { if (rerunQuery) {
const validRepositories = repositories.filter(r => !invalidRepos.includes(r) && !reposWithoutDbUploads.includes(r)); const validRepositories = repositories.filter(r => !invalidRepos.includes(r));
void logger.log(`Rerunning query on set of valid repositories: ${JSON.stringify(validRepositories)}`); void logger.log(`Rerunning query on set of valid repositories: ${JSON.stringify(validRepositories)}`);
return await runRemoteQueriesApiRequest(credentials, ref, language, validRepositories, owner, repo, queryPackBase64, dryRun); return await runRemoteQueriesApiRequest(credentials, ref, language, validRepositories, owner, repo, queryPackBase64, dryRun);
} }

View File

@@ -83,6 +83,16 @@ export const sampleRemoteQueryResult: RemoteQueryResult = {
queryId: 'query.ql-123-xyz' queryId: 'query.ql-123-xyz'
} }
} }
],
analysisFailures: [
{
nwo: 'big-corp/repo5',
error: 'Error message'
},
{
nwo: 'big-corp/repo6',
error: 'Error message'
},
] ]
}; };

View File

@@ -0,0 +1,4 @@
export interface AnalysisFailure {
nwo: string,
error: string
}

View File

@@ -1,4 +1,5 @@
import { DownloadLink } from '../download-link'; import { DownloadLink } from '../download-link';
import { AnalysisFailure } from './analysis-failure';
export interface RemoteQueryResult { export interface RemoteQueryResult {
queryTitle: string; queryTitle: string;
@@ -10,7 +11,8 @@ export interface RemoteQueryResult {
totalResultCount: number; totalResultCount: number;
executionTimestamp: string; executionTimestamp: string;
executionDuration: string; executionDuration: string;
analysisSummaries: AnalysisSummary[] analysisSummaries: AnalysisSummary[],
analysisFailures: AnalysisFailure[];
} }
export interface AnalysisSummary { export interface AnalysisSummary {

View File

@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import * as octicons from '../../view/octicons';
import styled from 'styled-components'; import styled from 'styled-components';
import { DownloadIcon } from '@primer/octicons-react';
const ButtonLink = styled.a` const ButtonLink = styled.a`
display: inline-block; display: inline-block;
@@ -16,7 +16,7 @@ const ButtonLink = styled.a`
const DownloadButton = ({ text, onClick }: { text: string, onClick: () => void }) => ( const DownloadButton = ({ text, onClick }: { text: string, onClick: () => void }) => (
<ButtonLink onClick={onClick}> <ButtonLink onClick={onClick}>
{octicons.download}{text} <DownloadIcon size={16} />{text}
</ButtonLink> </ButtonLink>
); );

View File

@@ -1,10 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import * as Rdom from 'react-dom'; import * as Rdom from 'react-dom';
import { ThemeProvider } from '@primer/react'; import { Flash, ThemeProvider } from '@primer/react';
import { ToRemoteQueriesMessage } from '../../pure/interface-types'; import { ToRemoteQueriesMessage } from '../../pure/interface-types';
import { AnalysisSummary, RemoteQueryResult } from '../shared/remote-query-result'; import { AnalysisSummary, RemoteQueryResult } from '../shared/remote-query-result';
import * as octicons from '../../view/octicons';
import { vscode } from '../../view/vscode-api'; import { vscode } from '../../view/vscode-api';
@@ -17,7 +16,7 @@ import DownloadButton from './DownloadButton';
import { AnalysisResults } from '../shared/analysis-result'; import { AnalysisResults } from '../shared/analysis-result';
import DownloadSpinner from './DownloadSpinner'; import DownloadSpinner from './DownloadSpinner';
import CollapsibleItem from './CollapsibleItem'; import CollapsibleItem from './CollapsibleItem';
import { FileSymlinkFileIcon } from '@primer/octicons-react'; import { AlertIcon, CodeSquareIcon, FileCodeIcon, FileSymlinkFileIcon, RepoIcon } from '@primer/octicons-react';
const numOfReposInContractedMode = 10; const numOfReposInContractedMode = 10;
@@ -31,7 +30,8 @@ const emptyQueryResult: RemoteQueryResult = {
totalResultCount: 0, totalResultCount: 0,
executionTimestamp: '', executionTimestamp: '',
executionDuration: '', executionDuration: '',
analysisSummaries: [] analysisSummaries: [],
analysisFailures: [],
}; };
const downloadAnalysisResults = (analysisSummary: AnalysisSummary) => { const downloadAnalysisResults = (analysisSummary: AnalysisSummary) => {
@@ -78,12 +78,14 @@ const QueryInfo = (queryResult: RemoteQueryResult) => (
{queryResult.totalResultCount} results from running against {queryResult.totalRepositoryCount} repositories {queryResult.totalResultCount} results from running against {queryResult.totalRepositoryCount} repositories
({queryResult.executionDuration}), {queryResult.executionTimestamp} ({queryResult.executionDuration}), {queryResult.executionTimestamp}
<VerticalSpace size={1} /> <VerticalSpace size={1} />
<span className="vscode-codeql__query-file">{octicons.file} <span className="vscode-codeql__query-file">
<FileCodeIcon size={16} />
<a className="vscode-codeql__query-file-link" href="#" onClick={() => openQueryFile(queryResult)}> <a className="vscode-codeql__query-file-link" href="#" onClick={() => openQueryFile(queryResult)}>
{queryResult.queryFileName} {queryResult.queryFileName}
</a> </a>
</span> </span>
<span>{octicons.codeSquare} <span>
<CodeSquareIcon size={16} />
<a className="vscode-codeql__query-file-link" href="#" onClick={() => openQueryTextVirtualFile(queryResult)}> <a className="vscode-codeql__query-file-link" href="#" onClick={() => openQueryTextVirtualFile(queryResult)}>
query query
</a> </a>
@@ -91,6 +93,31 @@ const QueryInfo = (queryResult: RemoteQueryResult) => (
</> </>
); );
const Failures = (queryResult: RemoteQueryResult) => {
if (queryResult.analysisFailures.length === 0) {
return <></>;
}
return (
<>
<VerticalSpace size={3} />
<Flash variant="danger">
{queryResult.analysisFailures.map((f, i) => (
<>
<p className="vscode-codeql__analysis-failure" key={i}>
<AlertIcon size={16} />
<b>{f.nwo}: </b>
{f.error}
</p>
{
i === queryResult.analysisFailures.length - 1 ? <></> : <VerticalSpace size={1} />
}
</>
))}
</Flash>
</>
);
};
const SummaryTitleWithResults = ({ const SummaryTitleWithResults = ({
queryResult, queryResult,
analysesResults analysesResults
@@ -155,7 +182,7 @@ const SummaryItem = ({
analysisResults: AnalysisResults | undefined analysisResults: AnalysisResults | undefined
}) => ( }) => (
<span> <span>
<span className="vscode-codeql__analysis-item">{octicons.repo}</span> <span className="vscode-codeql__analysis-item"><RepoIcon size={16} /></span>
<span className="vscode-codeql__analysis-item">{analysisSummary.nwo}</span> <span className="vscode-codeql__analysis-item">{analysisSummary.nwo}</span>
<span className="vscode-codeql__analysis-item"><Badge text={analysisSummary.resultCount.toString()} /></span> <span className="vscode-codeql__analysis-item"><Badge text={analysisSummary.resultCount.toString()} /></span>
<span className="vscode-codeql__analysis-item"> <span className="vscode-codeql__analysis-item">
@@ -293,9 +320,10 @@ export function RemoteQueries(): JSX.Element {
try { try {
return <div> return <div>
<ThemeProvider> <ThemeProvider colorMode="auto">
<ViewTitle>{queryResult.queryTitle}</ViewTitle> <ViewTitle>{queryResult.queryTitle}</ViewTitle>
<QueryInfo {...queryResult} /> <QueryInfo {...queryResult} />
<Failures {...queryResult} />
<Summary queryResult={queryResult} analysesResults={analysesResults} /> <Summary queryResult={queryResult} analysesResults={analysesResults} />
{showAnalysesResults && <AnalysesResults analysesResults={analysesResults} totalResults={queryResult.totalResultCount} />} {showAnalysesResults && <AnalysesResults analysesResults={analysesResults} totalResults={queryResult.totalResultCount} />}
</ThemeProvider> </ThemeProvider>

View File

@@ -1,15 +1,3 @@
.octicon {
fill: var(--vscode-editor-foreground);
height: 1.2em;
width: 1.2em;
vertical-align: middle;
display: inline-block;
}
.octicon-light {
opacity: 0.6;
}
.vscode-codeql__query-file { .vscode-codeql__query-file {
padding-right: 1em; padding-right: 1em;
} }
@@ -64,3 +52,10 @@
.vscode-codeql__analysis-result-file-link { .vscode-codeql__analysis-result-file-link {
vertical-align: middle; vertical-align: middle;
} }
.vscode-codeql__analysis-failure {
margin: 0;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace;
color: var(--vscode-editor-foreground);
}

View File

@@ -20,23 +20,3 @@ export const listUnordered = <svg className="octicon octicon-light" width="16" h
export const info = <svg className="octicon octicon-light" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" > export const info = <svg className="octicon octicon-light" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" >
<path fillRule="evenodd" clipRule="evenodd" d="M8.568 1.03a6.8 6.8 0 0 1 4.192 2.02 7.06 7.06 0 0 1 .46 9.39 6.85 6.85 0 0 1-8.58 1.74 7 7 0 0 1-3.12-3.5 7.12 7.12 0 0 1-.23-4.71 7 7 0 0 1 2.77-3.79 6.8 6.8 0 0 1 4.508-1.15zm.472 12.85a5.89 5.89 0 0 0 3.41-2.07 6.07 6.07 0 0 0-.4-8.06 5.82 5.82 0 0 0-7.43-.74 6.06 6.06 0 0 0 .5 10.29 5.81 5.81 0 0 0 3.92.58zM8.51 7h-1v4h1V7zm0-2h-1v1h1V5z" /> <path fillRule="evenodd" clipRule="evenodd" d="M8.568 1.03a6.8 6.8 0 0 1 4.192 2.02 7.06 7.06 0 0 1 .46 9.39 6.85 6.85 0 0 1-8.58 1.74 7 7 0 0 1-3.12-3.5 7.12 7.12 0 0 1-.23-4.71 7 7 0 0 1 2.77-3.79 6.8 6.8 0 0 1 4.508-1.15zm.472 12.85a5.89 5.89 0 0 0 3.41-2.07 6.07 6.07 0 0 0-.4-8.06 5.82 5.82 0 0 0-7.43-.74 6.06 6.06 0 0 0 .5 10.29 5.81 5.81 0 0 0 3.92.58zM8.51 7h-1v4h1V7zm0-2h-1v1h1V5z" />
</svg>; </svg>;
/**
* The icons below come from https://primer.style/octicons/
*/
export const file = <svg className="octicon octicon-light" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" d="M3.75 1.5a.25.25 0 00-.25.25v11.5c0 .138.112.25.25.25h8.5a.25.25 0 00.25-.25V6H9.75A1.75 1.75 0 018 4.25V1.5H3.75zm5.75.56v2.19c0 .138.112.25.25.25h2.19L9.5 2.06zM2 1.75C2 .784 2.784 0 3.75 0h5.086c.464 0 .909.184 1.237.513l3.414 3.414c.329.328.513.773.513 1.237v8.086A1.75 1.75 0 0112.25 15h-8.5A1.75 1.75 0 012 13.25V1.75z"></path>
</svg>;
export const codeSquare = <svg className="octicon octicon-light" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" d="M1.75 1.5a.25.25 0 00-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25V1.75a.25.25 0 00-.25-.25H1.75zM0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0114.25 16H1.75A1.75 1.75 0 010 14.25V1.75zm9.22 3.72a.75.75 0 000 1.06L10.69 8 9.22 9.47a.75.75 0 101.06 1.06l2-2a.75.75 0 000-1.06l-2-2a.75.75 0 00-1.06 0zM6.78 6.53a.75.75 0 00-1.06-1.06l-2 2a.75.75 0 000 1.06l2 2a.75.75 0 101.06-1.06L5.31 8l1.47-1.47z"></path>
</svg>;
export const repo = <svg className="octicon octicon-light" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path>
</svg>;
export const download = <svg className="octicon octicon-light" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" d="M7.47 10.78a.75.75 0 001.06 0l3.75-3.75a.75.75 0 00-1.06-1.06L8.75 8.44V1.75a.75.75 0 00-1.5 0v6.69L4.78 5.97a.75.75 0 00-1.06 1.06l3.75 3.75zM3.75 13a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5z"></path>
</svg>;

View File

@@ -96,12 +96,16 @@ describe('Remote queries', function() {
expect(fs.existsSync(path.join(queryPackDir, 'not-in-pack.ql'))).to.be.false; expect(fs.existsSync(path.join(queryPackDir, 'not-in-pack.ql'))).to.be.false;
// the compiled pack // the compiled pack
const compiledPackDir = path.join(queryPackDir, '.codeql/pack/github/remote-query-pack/0.0.0/'); const compiledPackDir = path.join(queryPackDir, '.codeql/pack/codeql-remote/query/0.0.0/');
printDirectoryContents(compiledPackDir); printDirectoryContents(compiledPackDir);
expect(fs.existsSync(path.join(compiledPackDir, 'in-pack.ql'))).to.be.true; expect(fs.existsSync(path.join(compiledPackDir, 'in-pack.ql'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'lib.qll'))).to.be.true; expect(fs.existsSync(path.join(compiledPackDir, 'lib.qll'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'qlpack.yml'))).to.be.true; expect(fs.existsSync(path.join(compiledPackDir, 'qlpack.yml'))).to.be.true;
// should have generated a correct qlpack file
const qlpackContents: any = yaml.safeLoad(fs.readFileSync(path.join(compiledPackDir, 'qlpack.yml'), 'utf8'));
expect(qlpackContents.name).to.equal('codeql-remote/query');
// depending on the cli version, we should have one of these files // depending on the cli version, we should have one of these files
expect( expect(
fs.existsSync(path.join(compiledPackDir, 'qlpack.lock.yml')) || fs.existsSync(path.join(compiledPackDir, 'qlpack.lock.yml')) ||
@@ -211,7 +215,7 @@ describe('Remote queries', function() {
expect(fs.existsSync(path.join(queryPackDir, 'not-in-pack.ql'))).to.be.false; expect(fs.existsSync(path.join(queryPackDir, 'not-in-pack.ql'))).to.be.false;
// the compiled pack // the compiled pack
const compiledPackDir = path.join(queryPackDir, '.codeql/pack/github/remote-query-pack/0.0.0/'); const compiledPackDir = path.join(queryPackDir, '.codeql/pack/codeql-remote/query/0.0.0/');
printDirectoryContents(compiledPackDir); printDirectoryContents(compiledPackDir);
expect(fs.existsSync(path.join(compiledPackDir, 'otherfolder/lib.qll'))).to.be.true; expect(fs.existsSync(path.join(compiledPackDir, 'otherfolder/lib.qll'))).to.be.true;
expect(fs.existsSync(path.join(compiledPackDir, 'subfolder/in-pack.ql'))).to.be.true; expect(fs.existsSync(path.join(compiledPackDir, 'subfolder/in-pack.ql'))).to.be.true;

View File

@@ -114,11 +114,10 @@ describe('run-remote-query', function() {
let mod: any; let mod: any;
const error = { const error = {
message: 'Unable to run query on the specified repositories. Some repositories were invalid or don\'t have database uploads enabled.', message: 'Unable to run query on the specified repositories. Some repositories were invalid.',
response: { response: {
data: { data: {
invalid_repos: ['abc/def', 'ghi/jkl'], invalid_repos: ['abc/def', 'ghi/jkl']
repos_without_db_uploads: ['mno/pqr', 'stu/vwx']
} }
} }
}; };
@@ -151,7 +150,7 @@ describe('run-remote-query', function() {
}); });
it('should return and log error if it can\'t run on any repos', async () => { it('should return and log error if it can\'t run on any repos', async () => {
const repositories = ['abc/def', 'ghi/jkl', 'mno/pqr', 'stu/vwx']; const repositories = ['abc/def', 'ghi/jkl'];
// make the function call // make the function call
await mod.attemptRerun(error, credentials, ref, language, repositories, query, owner, repo); await mod.attemptRerun(error, credentials, ref, language, repositories, query, owner, repo);
@@ -159,12 +158,11 @@ describe('run-remote-query', function() {
// check logging output // check logging output
expect(logSpy.firstCall.args[0]).to.contain('Unable to run query'); expect(logSpy.firstCall.args[0]).to.contain('Unable to run query');
expect(logSpy.secondCall.args[0]).to.contain('Invalid repos: abc/def, ghi/jkl'); expect(logSpy.secondCall.args[0]).to.contain('Invalid repos: abc/def, ghi/jkl');
expect(logSpy.thirdCall.args[0]).to.contain('Repos without DB uploads: mno/pqr, stu/vwx');
expect(showAndLogErrorMessageSpy.firstCall.args[0]).to.contain('Unable to run query on any'); expect(showAndLogErrorMessageSpy.firstCall.args[0]).to.contain('Unable to run query on any');
}); });
it('should list invalid repos and repos without DB uploads, and rerun on valid ones', async () => { it('should list invalid repos and rerun on valid ones', async () => {
const repositories = ['foo/bar', 'abc/def', 'ghi/jkl', 'mno/pqr', 'foo/baz']; const repositories = ['foo/bar', 'abc/def', 'ghi/jkl', 'foo/baz'];
// fake return values // fake return values
showInformationMessageWithActionSpy.resolves(true); showInformationMessageWithActionSpy.resolves(true);
@@ -175,7 +173,6 @@ describe('run-remote-query', function() {
// check logging output // check logging output
expect(logSpy.firstCall.args[0]).to.contain('Unable to run query'); expect(logSpy.firstCall.args[0]).to.contain('Unable to run query');
expect(logSpy.secondCall.args[0]).to.contain('Invalid repos: abc/def, ghi/jkl'); expect(logSpy.secondCall.args[0]).to.contain('Invalid repos: abc/def, ghi/jkl');
expect(logSpy.thirdCall.args[0]).to.contain('Repos without DB uploads: mno/pqr');
// check that the correct information message is displayed // check that the correct information message is displayed
expect(showInformationMessageWithActionSpy.firstCall.args[0]).to.contain('Unable to run query on some'); expect(showInformationMessageWithActionSpy.firstCall.args[0]).to.contain('Unable to run query on some');
@@ -198,8 +195,4 @@ describe('run-remote-query', function() {
}; };
} }
}); });
describe('runRemoteQuery', () => {
// TODO
});
}); });