This is a large commit and includes all the changes to add query
history items immediately. This also includes some smaller related 
changes that were hit while cleaning this area up.

The major part of this change is a refactoring of what we store in
the query history list. Previously, the `CompletedQuery` was stored.
Previously, objects of this type include all information about a query that was run
including:

- Its source file and text range (if a quick eval)
- Its database
- Its label
- The query results itself
- Metrics about the query run
- Metadata about the query itself

Now, the item stored is called a `FullQueryInfo`, which has two
properties:

- InitialQueryInfo: all the data about the query that we know _before_
  the query completes, eg- its source file and text range, database, and
  label
- CompletedQueryInfo: all the data about the query that we can only
  learn _after_ the query completes. This is an optional property.

There is also a `failureReason` property, which is an optional string
describing why the query failed.


There is also a `FullCompletedQueryInfo` type, which only exists to 
help with stronger typing. It is a `FullQueryInfo` with a non-optional
`CompletedQueryInfo`.

Most of the changes are around changing how the query history accesses
its history list.

There are some other smaller changes included here:

- New icon for completed query (previously, completed queries had no
  icons).
- New spinning icon for in progress queries.
- Better error handling in the logger to handle log messages when the
  extension is shutting down. This mostly helps clean up the output
  during tests.
- Add more disposables to subscriptions to be disposed of when the
  extension shuts down.
This commit is contained in:
Andrew Eisenberg 2022-01-19 17:43:23 -08:00
Родитель 9c2bd2a57b
Коммит a6f42e3eb3
15 изменённых файлов: 1050 добавлений и 712 удалений

Просмотреть файл

@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5 12.1952C15.5 12.9126 14.9137 13.4996 14.1957 13.4996H1.80435C1.08696 13.4996 0.5 12.9126 0.5 12.1952L0.5 9.80435C0.5 9.08696 1.08696 8.5 1.80435 8.5H14.1956C14.9137 8.5 15.5 9.08696 15.5 9.80435L15.5 12.1952Z" stroke="#959DA5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.45654 11.5H13.5435" stroke="#959DA5" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.5 9.5C13.224 9.5 13 9.725 13 10C13 10.275 13.224 10.5 13.5 10.5C13.776 10.5 14 10.275 14 10C14 9.725 13.776 9.5 13.5 9.5" fill="#959DA5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5 9.5C11.224 9.5 11 9.725 11 10C11 10.275 11.224 10.5 11.5 10.5C11.776 10.5 12 10.275 12 10C12 9.725 11.776 9.5 11.5 9.5" fill="#959DA5"/>
<path d="M15.5 9.81464L13.8728 2.76261C13.6922 2.06804 12.9572 1.5 12.2391 1.5H3.76087C3.04348 1.5 2.30848 2.06804 2.12783 2.76261L0.5 9.8" stroke="#959DA5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

Просмотреть файл

@ -9,7 +9,6 @@ import {
import * as path from 'path';
import { tmpDir } from '../run-queries';
import { CompletedQuery } from '../query-results';
import {
FromCompareViewMessage,
ToCompareViewMessage,
@ -21,10 +20,11 @@ import { DatabaseManager } from '../databases';
import { getHtmlForWebview, jumpToLocation } from '../interface-utils';
import { transformBqrsResultSet, RawResultSet, BQRSInfo } from '../pure/bqrs-cli-types';
import resultsDiff from './resultsDiff';
import { FullCompletedQueryInfo } from '../query-results';
interface ComparePair {
from: CompletedQuery;
to: CompletedQuery;
from: FullCompletedQueryInfo;
to: FullCompletedQueryInfo;
}
export class CompareInterfaceManager extends DisposableObject {
@ -39,15 +39,15 @@ export class CompareInterfaceManager extends DisposableObject {
private cliServer: CodeQLCliServer,
private logger: Logger,
private showQueryResultsCallback: (
item: CompletedQuery
item: FullCompletedQueryInfo
) => Promise<void>
) {
super();
}
async showResults(
from: CompletedQuery,
to: CompletedQuery,
from: FullCompletedQueryInfo,
to: FullCompletedQueryInfo,
selectedResultSetName?: string
) {
this.comparePair = { from, to };
@ -80,17 +80,13 @@ export class CompareInterfaceManager extends DisposableObject {
// since we split the description into several rows
// only run interpolation if the label is user-defined
// otherwise we will wind up with duplicated rows
name: from.options.label
? from.interpolate(from.getLabel())
: from.queryName,
status: from.statusString,
name: from.getShortLabel(),
status: from.completedQuery.statusString,
time: from.time,
},
toQuery: {
name: to.options.label
? to.interpolate(to.getLabel())
: to.queryName,
status: to.statusString,
name: to.getShortLabel(),
status: to.completedQuery.statusString,
time: to.time,
},
},
@ -99,7 +95,7 @@ export class CompareInterfaceManager extends DisposableObject {
currentResultSetName: currentResultSetName,
rows,
message,
datebaseUri: to.database.databaseUri,
datebaseUri: to.initialInfo.databaseInfo.databaseUri,
});
}
}
@ -121,14 +117,14 @@ export class CompareInterfaceManager extends DisposableObject {
],
}
));
this.panel.onDidDispose(
this.push(this.panel.onDidDispose(
() => {
this.panel = undefined;
this.comparePair = undefined;
},
null,
ctx.subscriptions
);
));
const scriptPathOnDisk = Uri.file(
ctx.asAbsolutePath('out/compareView.js')
@ -143,11 +139,11 @@ export class CompareInterfaceManager extends DisposableObject {
scriptPathOnDisk,
[stylesheetPathOnDisk]
);
panel.webview.onDidReceiveMessage(
this.push(panel.webview.onDidReceiveMessage(
async (e) => this.handleMsgFromView(e),
undefined,
ctx.subscriptions
);
));
}
return this.panel;
}
@ -191,15 +187,15 @@ export class CompareInterfaceManager extends DisposableObject {
}
private async findCommonResultSetNames(
from: CompletedQuery,
to: CompletedQuery,
from: FullCompletedQueryInfo,
to: FullCompletedQueryInfo,
selectedResultSetName: string | undefined
): Promise<[string[], string, RawResultSet, RawResultSet]> {
const fromSchemas = await this.cliServer.bqrsInfo(
from.query.resultsPaths.resultsPath
from.completedQuery.query.resultsPaths.resultsPath
);
const toSchemas = await this.cliServer.bqrsInfo(
to.query.resultsPaths.resultsPath
to.completedQuery.query.resultsPaths.resultsPath
);
const fromSchemaNames = fromSchemas['result-sets'].map(
(schema) => schema.name
@ -215,12 +211,12 @@ export class CompareInterfaceManager extends DisposableObject {
const fromResultSet = await this.getResultSet(
fromSchemas,
currentResultSetName,
from.query.resultsPaths.resultsPath
from.completedQuery.query.resultsPaths.resultsPath
);
const toResultSet = await this.getResultSet(
toSchemas,
currentResultSetName,
to.query.resultsPaths.resultsPath
to.completedQuery.query.resultsPaths.resultsPath
);
return [
commonResultSetNames,

Просмотреть файл

@ -1,5 +1,3 @@
import * as vscode from 'vscode';
import { decodeSourceArchiveUri, encodeArchiveBasePath } from '../archive-filesystem-provider';
import { ColumnKindCode, EntityValue, getResultSetSchema, ResultSetSchema } from '../pure/bqrs-cli-types';
import { CodeQLCliServer } from '../cli';
@ -7,16 +5,17 @@ import { DatabaseManager, DatabaseItem } from '../databases';
import fileRangeFromURI from './fileRangeFromURI';
import * as messages from '../pure/messages';
import { QueryServerClient } from '../queryserver-client';
import { QueryWithResults, compileAndRunQueryAgainstDatabase } from '../run-queries';
import { QueryWithResults, compileAndRunQueryAgainstDatabase, createInitialQueryInfo } from '../run-queries';
import { ProgressCallback } from '../commandRunner';
import { KeyType } from './keyType';
import { qlpackOfDatabase, resolveQueries } from './queryResolver';
import { CancellationToken, LocationLink, Uri } from 'vscode';
export const SELECT_QUERY_NAME = '#select';
export const TEMPLATE_NAME = 'selectedSourceFile';
export interface FullLocationLink extends vscode.LocationLink {
originUri: vscode.Uri;
export interface FullLocationLink extends LocationLink {
originUri: Uri;
}
/**
@ -40,10 +39,10 @@ export async function getLocationsForUriString(
uriString: string,
keyType: KeyType,
progress: ProgressCallback,
token: vscode.CancellationToken,
token: CancellationToken,
filter: (src: string, dest: string) => boolean
): Promise<FullLocationLink[]> {
const uri = decodeSourceArchiveUri(vscode.Uri.parse(uriString, true));
const uri = decodeSourceArchiveUri(Uri.parse(uriString, true));
const sourceArchiveUri = encodeArchiveBasePath(uri.sourceArchiveZipPath);
const db = dbm.findDatabaseItemBySourceArchive(sourceArchiveUri);
@ -56,12 +55,20 @@ export async function getLocationsForUriString(
const links: FullLocationLink[] = [];
for (const query of await resolveQueries(cli, qlpack, keyType)) {
const initialInfo = await createInitialQueryInfo(
Uri.file(query),
{
name: db.name,
databaseUri: db.databaseUri.toString(),
},
false
);
const results = await compileAndRunQueryAgainstDatabase(
cli,
qs,
db,
false,
vscode.Uri.file(query),
initialInfo,
progress,
token,
templates

Просмотреть файл

@ -18,7 +18,7 @@ import { CachedOperation } from '../helpers';
import { ProgressCallback, withProgress } from '../commandRunner';
import * as messages from '../pure/messages';
import { QueryServerClient } from '../queryserver-client';
import { compileAndRunQueryAgainstDatabase, QueryWithResults } from '../run-queries';
import { compileAndRunQueryAgainstDatabase, createInitialQueryInfo, QueryWithResults } from '../run-queries';
import AstBuilder from './astBuilder';
import {
KeyType,
@ -123,15 +123,20 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
}
}
type QueryWithDb = {
query: QueryWithResults,
dbUri: Uri
};
export class TemplatePrintAstProvider {
private cache: CachedOperation<QueryWithResults>;
private cache: CachedOperation<QueryWithDb>;
constructor(
private cli: CodeQLCliServer,
private qs: QueryServerClient,
private dbm: DatabaseManager,
) {
this.cache = new CachedOperation<QueryWithResults>(this.getAst.bind(this));
this.cache = new CachedOperation<QueryWithDb>(this.getAst.bind(this));
}
async provideAst(
@ -142,13 +147,13 @@ export class TemplatePrintAstProvider {
if (!document) {
throw new Error('Cannot view the AST. Please select a valid source file inside a CodeQL database.');
}
const queryResults = this.shouldCache()
const { query, dbUri } = this.shouldCache()
? await this.cache.get(document.uri.toString(), progress, token)
: await this.getAst(document.uri.toString(), progress, token);
return new AstBuilder(
queryResults, this.cli,
this.dbm.findDatabaseItem(Uri.parse(queryResults.database.databaseUri!, true))!,
query, this.cli,
this.dbm.findDatabaseItem(dbUri)!,
document.fileName
);
}
@ -161,7 +166,7 @@ export class TemplatePrintAstProvider {
uriString: string,
progress: ProgressCallback,
token: CancellationToken
): Promise<QueryWithResults> {
): Promise<QueryWithDb> {
const uri = Uri.parse(uriString, true);
if (uri.scheme !== zipArchiveScheme) {
throw new Error('Cannot view the AST. Please select a valid source file inside a CodeQL database.');
@ -195,15 +200,26 @@ export class TemplatePrintAstProvider {
}
};
return await compileAndRunQueryAgainstDatabase(
this.cli,
this.qs,
db,
false,
const initialInfo = await createInitialQueryInfo(
Uri.file(query),
progress,
token,
templates
{
name: db.name,
databaseUri: db.databaseUri.toString(),
},
false
);
return {
query: await compileAndRunQueryAgainstDatabase(
this.cli,
this.qs,
db,
initialInfo,
progress,
token,
templates
),
dbUri: db.databaseUri
};
}
}

Просмотреть файл

@ -59,10 +59,10 @@ import { InterfaceManager } from './interface';
import { WebviewReveal } from './interface-utils';
import { ideServerLogger, logger, queryServerLogger } from './logging';
import { QueryHistoryManager } from './query-history';
import { CompletedQuery } from './query-results';
import { FullCompletedQueryInfo, FullQueryInfo } from './query-results';
import * as qsClient from './queryserver-client';
import { displayQuickQuery } from './quick-query';
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal } from './run-queries';
import { compileAndRunQueryAgainstDatabase, createInitialQueryInfo, tmpDirDisposal } from './run-queries';
import { QLTestAdapterFactory } from './test-adapter';
import { TestUIService } from './test-ui';
import { CompareInterfaceManager } from './compare/compare-interface';
@ -432,7 +432,7 @@ async function activateWithInstalledDistribution(
void logger.log('Initializing query history manager.');
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
ctx.subscriptions.push(queryHistoryConfigurationListener);
const showResults = async (item: CompletedQuery) =>
const showResults = async (item: FullCompletedQueryInfo) =>
showResultsForCompletedQuery(item, WebviewReveal.Forced);
const qhm = new QueryHistoryManager(
@ -440,7 +440,7 @@ async function activateWithInstalledDistribution(
ctx.extensionPath,
queryHistoryConfigurationListener,
showResults,
async (from: CompletedQuery, to: CompletedQuery) =>
async (from: FullCompletedQueryInfo, to: FullCompletedQueryInfo) =>
showResultsForComparison(from, to),
);
ctx.subscriptions.push(qhm);
@ -462,8 +462,8 @@ async function activateWithInstalledDistribution(
archiveFilesystemProvider.activate(ctx);
async function showResultsForComparison(
from: CompletedQuery,
to: CompletedQuery
from: FullCompletedQueryInfo,
to: FullCompletedQueryInfo
): Promise<void> {
try {
await cmpm.showResults(from, to);
@ -473,7 +473,7 @@ async function activateWithInstalledDistribution(
}
async function showResultsForCompletedQuery(
query: CompletedQuery,
query: FullCompletedQueryInfo,
forceReveal: WebviewReveal
): Promise<void> {
await intm.showResults(query, forceReveal, false);
@ -493,22 +493,35 @@ async function activateWithInstalledDistribution(
if (databaseItem === undefined) {
throw new Error('Can\'t run query without a selected database');
}
const info = await compileAndRunQueryAgainstDatabase(
cliServer,
qs,
databaseItem,
quickEval,
selectedQuery,
progress,
token,
undefined,
range
);
const item = qhm.buildCompletedQuery(info);
await showResultsForCompletedQuery(item, WebviewReveal.NotForced);
// Note we must update the query history view after showing results as the
// display and sorting might depend on the number of results
await qhm.addCompletedQuery(item);
const databaseInfo = {
name: databaseItem.name,
databaseUri: databaseItem.databaseUri.toString(),
};
const initialInfo = await createInitialQueryInfo(selectedQuery, databaseInfo, quickEval, range);
const item = new FullQueryInfo(initialInfo, queryHistoryConfigurationListener);
qhm.addCompletedQuery(item);
await qhm.refreshTreeView(item);
try {
const info = await compileAndRunQueryAgainstDatabase(
cliServer,
qs,
databaseItem,
initialInfo,
progress,
token,
);
item.completeThisQuery(info);
await showResultsForCompletedQuery(item as FullCompletedQueryInfo, WebviewReveal.NotForced);
// Note we must update the query history view after showing results as the
// display and sorting might depend on the number of results
} catch (e) {
item.failureReason = e.message;
throw e;
} finally {
await qhm.refreshTreeView(item);
}
}
}
@ -1027,7 +1040,7 @@ const checkForUpdatesCommand = 'codeQL.checkForUpdatesToCLI';
/**
* This text provider lets us open readonly files in the editor.
*
*
* TODO: Consolidate this with the 'codeql' text provider in query-history.ts.
*/
function registerRemoteQueryTextProvider() {

Просмотреть файл

@ -32,8 +32,8 @@ import {
import { Logger } from './logging';
import * as messages from './pure/messages';
import { commandRunner } from './commandRunner';
import { CompletedQuery, interpretResults } from './query-results';
import { QueryInfo, tmpDir } from './run-queries';
import { CompletedQueryInfo, interpretResults } from './query-results';
import { QueryEvaluatonInfo, tmpDir } from './run-queries';
import { parseSarifLocation, parseSarifPlainTextMessage } from './pure/sarif-utils';
import {
WebviewReveal,
@ -47,6 +47,7 @@ import {
import { getDefaultResultSetName, ParsedResultSets } from './pure/interface-types';
import { RawResultSet, transformBqrsResultSet, ResultSetSchema } from './pure/bqrs-cli-types';
import { PAGE_SIZE } from './config';
import { FullCompletedQueryInfo } from './query-results';
/**
* interface.ts
@ -96,7 +97,7 @@ function numInterpretedPages(interpretation: Interpretation | undefined): number
}
export class InterfaceManager extends DisposableObject {
private _displayedQuery?: CompletedQuery;
private _displayedQuery?: FullCompletedQueryInfo;
private _interpretation?: Interpretation;
private _panel: vscode.WebviewPanel | undefined;
private _panelLoaded = false;
@ -176,14 +177,14 @@ export class InterfaceManager extends DisposableObject {
}
));
this._panel.onDidDispose(
this.push(this._panel.onDidDispose(
() => {
this._panel = undefined;
this._displayedQuery = undefined;
},
null,
ctx.subscriptions
);
));
const scriptPathOnDisk = vscode.Uri.file(
ctx.asAbsolutePath('out/resultsView.js')
);
@ -195,11 +196,11 @@ export class InterfaceManager extends DisposableObject {
scriptPathOnDisk,
[stylesheetPathOnDisk]
);
panel.webview.onDidReceiveMessage(
this.push(panel.webview.onDidReceiveMessage(
async (e) => this.handleMsgFromView(e),
undefined,
ctx.subscriptions
);
));
}
return this._panel;
}
@ -238,7 +239,7 @@ export class InterfaceManager extends DisposableObject {
}
// Notify the webview that it should expect new results.
await this.postMessage({ t: 'resultsUpdating' });
await this._displayedQuery.updateInterpretedSortState(sortState);
await this._displayedQuery.completedQuery.updateInterpretedSortState(sortState);
await this.showResults(this._displayedQuery, WebviewReveal.NotForced, true);
}
@ -254,7 +255,7 @@ export class InterfaceManager extends DisposableObject {
}
// Notify the webview that it should expect new results.
await this.postMessage({ t: 'resultsUpdating' });
await this._displayedQuery.updateSortState(
await this._displayedQuery.completedQuery.updateSortState(
this.cliServer,
resultSetName,
sortState
@ -314,7 +315,7 @@ export class InterfaceManager extends DisposableObject {
// sortedResultsInfo doesn't have an entry for the current
// result set. Use this to determine whether or not we use
// the sorted bqrs file.
this._displayedQuery?.sortedResultsInfo.has(msg.selectedTable) || false
this._displayedQuery?.completedQuery.sortedResultsInfo.has(msg.selectedTable) || false
);
}
break;
@ -347,7 +348,7 @@ export class InterfaceManager extends DisposableObject {
/**
* Show query results in webview panel.
* @param results Evaluation info for the executed query.
* @param fullQuery Evaluation info for the executed query.
* @param shouldKeepOldResultsWhileRendering Should keep old results while rendering.
* @param forceReveal Force the webview panel to be visible and
* Appropriate when the user has just performed an explicit
@ -355,27 +356,27 @@ export class InterfaceManager extends DisposableObject {
* history entry.
*/
public async showResults(
results: CompletedQuery,
fullQuery: FullCompletedQueryInfo,
forceReveal: WebviewReveal,
shouldKeepOldResultsWhileRendering = false
): Promise<void> {
if (results.result.resultType !== messages.QueryResultType.SUCCESS) {
if (fullQuery.completedQuery.result.resultType !== messages.QueryResultType.SUCCESS) {
return;
}
this._interpretation = undefined;
const interpretationPage = await this.interpretResultsInfo(
results.query,
results.interpretedResultsSortState
fullQuery.completedQuery.query,
fullQuery.completedQuery.interpretedResultsSortState
);
const sortedResultsMap: SortedResultsMap = {};
results.sortedResultsInfo.forEach(
fullQuery.completedQuery.sortedResultsInfo.forEach(
(v, k) =>
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v))
);
this._displayedQuery = results;
this._displayedQuery = fullQuery;
const panel = this.getPanel();
await this.waitForPanelLoaded();
@ -388,7 +389,7 @@ export class InterfaceManager extends DisposableObject {
// more asynchronous message to not so abruptly interrupt
// user's workflow by immediately revealing the panel.
const showButton = 'View Results';
const queryName = results.queryName;
const queryName = fullQuery.getShortLabel();
const resultPromise = vscode.window.showInformationMessage(
`Finished running query ${queryName.length > 0 ? ` "${queryName}"` : ''
}.`,
@ -407,7 +408,7 @@ export class InterfaceManager extends DisposableObject {
// Note that the resultSetSchemas will return offsets for the default (unsorted) page,
// which may not be correct. However, in this case, it doesn't matter since we only
// need the first offset, which will be the same no matter which sorting we use.
const resultSetSchemas = await this.getResultSetSchemas(results);
const resultSetSchemas = await this.getResultSetSchemas(fullQuery.completedQuery);
const resultSetNames = resultSetSchemas.map(schema => schema.name);
const selectedTable = getDefaultResultSetName(resultSetNames);
@ -417,7 +418,7 @@ export class InterfaceManager extends DisposableObject {
// Use sorted results path if it exists. This may happen if we are
// reloading the results view after it has been sorted in the past.
const resultsPath = results.getResultsPath(selectedTable);
const resultsPath = fullQuery.completedQuery.getResultsPath(selectedTable);
const pageSize = PAGE_SIZE.getValue<number>();
const chunk = await this.cliServer.bqrsDecode(
resultsPath,
@ -432,7 +433,7 @@ export class InterfaceManager extends DisposableObject {
}
);
const resultSet = transformBqrsResultSet(schema, chunk);
results.setResultCount(interpretationPage?.numTotalResults || resultSet.schema.rows);
fullQuery.completedQuery.setResultCount(interpretationPage?.numTotalResults || resultSet.schema.rows);
const parsedResultSets: ParsedResultSets = {
pageNumber: 0,
pageSize,
@ -446,17 +447,17 @@ export class InterfaceManager extends DisposableObject {
await this.postMessage({
t: 'setState',
interpretation: interpretationPage,
origResultsPaths: results.query.resultsPaths,
origResultsPaths: fullQuery.completedQuery.query.resultsPaths,
resultsPath: this.convertPathToWebviewUri(
results.query.resultsPaths.resultsPath
fullQuery.completedQuery.query.resultsPaths.resultsPath
),
parsedResultSets,
sortedResultsMap,
database: results.database,
database: fullQuery.initialInfo.databaseInfo,
shouldKeepOldResultsWhileRendering,
metadata: results.query.metadata,
queryName: results.toString(),
queryPath: results.query.program.queryPath
metadata: fullQuery.completedQuery.query.metadata,
queryName: fullQuery.label,
queryPath: fullQuery.completedQuery.query.program.queryPath
});
}
@ -476,25 +477,25 @@ export class InterfaceManager extends DisposableObject {
throw new Error('Trying to show interpreted results but results were undefined');
}
const resultSetSchemas = await this.getResultSetSchemas(this._displayedQuery);
const resultSetSchemas = await this.getResultSetSchemas(this._displayedQuery.completedQuery);
const resultSetNames = resultSetSchemas.map(schema => schema.name);
await this.postMessage({
t: 'showInterpretedPage',
interpretation: this.getPageOfInterpretedResults(pageNumber),
database: this._displayedQuery.database,
metadata: this._displayedQuery.query.metadata,
database: this._displayedQuery.initialInfo.databaseInfo,
metadata: this._displayedQuery.completedQuery.query.metadata,
pageNumber,
resultSetNames,
pageSize: PAGE_SIZE.getValue(),
numPages: numInterpretedPages(this._interpretation),
queryName: this._displayedQuery.toString(),
queryPath: this._displayedQuery.query.program.queryPath
queryName: this._displayedQuery.label,
queryPath: this._displayedQuery.completedQuery.query.program.queryPath
});
}
private async getResultSetSchemas(results: CompletedQuery, selectedTable = ''): Promise<ResultSetSchema[]> {
const resultsPath = results.getResultsPath(selectedTable);
private async getResultSetSchemas(completedQuery: CompletedQueryInfo, selectedTable = ''): Promise<ResultSetSchema[]> {
const resultsPath = completedQuery.getResultsPath(selectedTable);
const schemas = await this.cliServer.bqrsInfo(
resultsPath,
PAGE_SIZE.getValue()
@ -521,17 +522,17 @@ export class InterfaceManager extends DisposableObject {
}
const sortedResultsMap: SortedResultsMap = {};
results.sortedResultsInfo.forEach(
results.completedQuery.sortedResultsInfo.forEach(
(v, k) =>
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v))
);
const resultSetSchemas = await this.getResultSetSchemas(results, sorted ? selectedTable : '');
const resultSetSchemas = await this.getResultSetSchemas(results.completedQuery, sorted ? selectedTable : '');
// If there is a specific sorted table selected, a different bqrs file is loaded that doesn't have all the result set names.
// Make sure that we load all result set names here.
// See https://github.com/github/vscode-codeql/issues/1005
const allResultSetSchemas = sorted ? await this.getResultSetSchemas(results, '') : resultSetSchemas;
const allResultSetSchemas = sorted ? await this.getResultSetSchemas(results.completedQuery, '') : resultSetSchemas;
const resultSetNames = allResultSetSchemas.map(schema => schema.name);
const schema = resultSetSchemas.find(
@ -542,7 +543,7 @@ export class InterfaceManager extends DisposableObject {
const pageSize = PAGE_SIZE.getValue<number>();
const chunk = await this.cliServer.bqrsDecode(
results.getResultsPath(selectedTable, sorted),
results.completedQuery.getResultsPath(selectedTable, sorted),
schema.name,
{
offset: schema.pagination?.offsets[pageNumber],
@ -564,17 +565,17 @@ export class InterfaceManager extends DisposableObject {
await this.postMessage({
t: 'setState',
interpretation: this._interpretation,
origResultsPaths: results.query.resultsPaths,
origResultsPaths: results.completedQuery.query.resultsPaths,
resultsPath: this.convertPathToWebviewUri(
results.query.resultsPaths.resultsPath
results.completedQuery.query.resultsPaths.resultsPath
),
parsedResultSets,
sortedResultsMap,
database: results.database,
database: results.initialInfo.databaseInfo,
shouldKeepOldResultsWhileRendering: false,
metadata: results.query.metadata,
queryName: results.toString(),
queryPath: results.query.program.queryPath
metadata: results.completedQuery.query.metadata,
queryName: results.label,
queryPath: results.completedQuery.query.program.queryPath
});
}
@ -643,7 +644,7 @@ export class InterfaceManager extends DisposableObject {
}
private async interpretResultsInfo(
query: QueryInfo,
query: QueryEvaluatonInfo,
sortState: InterpretedResultsSortState | undefined
): Promise<Interpretation | undefined> {
if (

Просмотреть файл

@ -74,31 +74,39 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
* continuing.
*/
async log(message: string, options = {} as LogOptions): Promise<void> {
if (options.trailingNewline === undefined) {
options.trailingNewline = true;
}
if (options.trailingNewline) {
this.outputChannel.appendLine(message);
} else {
this.outputChannel.append(message);
}
if (this.additionalLogLocationPath && options.additionalLogLocation) {
const logPath = path.join(this.additionalLogLocationPath, options.additionalLogLocation);
let additional = this.additionalLocations.get(logPath);
if (!additional) {
const msg = `| Log being saved to ${logPath} |`;
const separator = new Array(msg.length).fill('-').join('');
this.outputChannel.appendLine(separator);
this.outputChannel.appendLine(msg);
this.outputChannel.appendLine(separator);
additional = new AdditionalLogLocation(logPath, !this.isCustomLogDirectory);
this.additionalLocations.set(logPath, additional);
this.track(additional);
try {
if (options.trailingNewline === undefined) {
options.trailingNewline = true;
}
if (options.trailingNewline) {
this.outputChannel.appendLine(message);
} else {
this.outputChannel.append(message);
}
await additional.log(message, options);
if (this.additionalLogLocationPath && options.additionalLogLocation) {
const logPath = path.join(this.additionalLogLocationPath, options.additionalLogLocation);
let additional = this.additionalLocations.get(logPath);
if (!additional) {
const msg = `| Log being saved to ${logPath} |`;
const separator = new Array(msg.length).fill('-').join('');
this.outputChannel.appendLine(separator);
this.outputChannel.appendLine(msg);
this.outputChannel.appendLine(separator);
additional = new AdditionalLogLocation(logPath, !this.isCustomLogDirectory);
this.additionalLocations.set(logPath, additional);
this.track(additional);
}
await additional.log(message, options);
}
} catch (e) {
if (e instanceof Error && e.message === 'Channel has been closed') {
// Output channel is closed logging to console instead
console.log('Output channel is closed logging to console instead:', message);
} else {
throw e;
}
}
}

Просмотреть файл

@ -1,9 +1,20 @@
import * as path from 'path';
import * as vscode from 'vscode';
import { window as Window, env } from 'vscode';
import { CompletedQuery } from './query-results';
import {
commands,
env,
Event,
EventEmitter,
ProviderResult,
Range,
ThemeIcon,
TreeItem,
TreeView,
Uri,
ViewColumn,
window,
workspace,
} from 'vscode';
import { QueryHistoryConfig } from './config';
import { QueryWithResults } from './run-queries';
import {
showAndLogErrorMessage,
showAndLogInformationMessage,
@ -16,6 +27,7 @@ import { QueryServerClient } from './queryserver-client';
import { DisposableObject } from './pure/disposable-object';
import { commandRunner } from './commandRunner';
import { assertNever } from './pure/helpers-pure';
import { FullCompletedQueryInfo, FullQueryInfo, QueryStatus } from './query-results';
/**
* query-history.ts
@ -26,12 +38,6 @@ import { assertNever } from './pure/helpers-pure';
* `TreeDataProvider` subclass below.
*/
export type QueryHistoryItemOptions = {
label?: string; // user-settable label
queryText?: string; // text of the selected file
isQuickQuery?: boolean;
};
export const SHOW_QUERY_TEXT_MSG = `\
////////////////////////////////////////////////////////////////////////////////////
// This is the text of the entire query file when it was executed for this query //
@ -59,6 +65,11 @@ const SHOW_QUERY_TEXT_QUICK_EVAL_MSG = `\
*/
const FAILED_QUERY_HISTORY_ITEM_ICON = 'media/red-x.svg';
/**
* Path to icon to display next to a successful local run.
*/
const LOCAL_SUCCESS_QUERY_HISTORY_ITEM_ICON = 'media/drive.svg';
enum SortOrder {
NameAsc = 'NameAsc',
NameDesc = 'NameDesc',
@ -74,19 +85,21 @@ enum SortOrder {
export class HistoryTreeDataProvider extends DisposableObject {
private _sortOrder = SortOrder.DateAsc;
private _onDidChangeTreeData = super.push(new vscode.EventEmitter<CompletedQuery | undefined>());
private _onDidChangeTreeData = super.push(new EventEmitter<FullQueryInfo | undefined>());
readonly onDidChangeTreeData: vscode.Event<CompletedQuery | undefined> = this
readonly onDidChangeTreeData: Event<FullQueryInfo | undefined> = this
._onDidChangeTreeData.event;
private history: CompletedQuery[] = [];
private history: FullQueryInfo[] = [];
private failedIconPath: string;
private localSuccessIconPath: string;
/**
* When not undefined, must be reference-equal to an item in `this.databases`.
*/
private current: CompletedQuery | undefined;
private current: FullQueryInfo | undefined;
constructor(extensionPath: string) {
super();
@ -94,10 +107,14 @@ export class HistoryTreeDataProvider extends DisposableObject {
extensionPath,
FAILED_QUERY_HISTORY_ITEM_ICON
);
this.localSuccessIconPath = path.join(
extensionPath,
LOCAL_SUCCESS_QUERY_HISTORY_ITEM_ICON
);
}
async getTreeItem(element: CompletedQuery): Promise<vscode.TreeItem> {
const treeItem = new vscode.TreeItem(element.toString());
async getTreeItem(element: FullQueryInfo): Promise<TreeItem> {
const treeItem = new TreeItem(element.label);
treeItem.command = {
title: 'Query History Item',
@ -108,61 +125,77 @@ export class HistoryTreeDataProvider extends DisposableObject {
// Mark this query history item according to whether it has a
// SARIF file so that we can make context menu items conditionally
// available.
const hasResults = await element.query.hasInterpretedResults();
const hasResults = await element.completedQuery?.query.hasInterpretedResults();
treeItem.contextValue = hasResults
? 'interpretedResultsItem'
: 'rawResultsItem';
if (!element.didRunSuccessfully) {
treeItem.iconPath = this.failedIconPath;
switch (element.status) {
case QueryStatus.InProgress:
// TODO this is not a good icon.
treeItem.iconPath = new ThemeIcon('sync~spin');
break;
case QueryStatus.Completed:
treeItem.iconPath = this.localSuccessIconPath;
break;
case QueryStatus.Failed:
treeItem.iconPath = this.failedIconPath;
break;
default:
assertNever(element.status);
}
return treeItem;
}
getChildren(
element?: CompletedQuery
): vscode.ProviderResult<CompletedQuery[]> {
return element ? [] : this.history.sort((q1, q2) => {
element?: FullQueryInfo
): ProviderResult<FullQueryInfo[]> {
return element ? [] : this.history.sort((h1, h2) => {
const q1 = h1.completedQuery;
const q2 = h2.completedQuery;
switch (this.sortOrder) {
case SortOrder.NameAsc:
return q1.toString().localeCompare(q2.toString(), env.language);
return h1.label.localeCompare(h2.label, env.language);
case SortOrder.NameDesc:
return q2.toString().localeCompare(q1.toString(), env.language);
return h2.label.localeCompare(h1.label, env.language);
case SortOrder.DateAsc:
return q1.date.getTime() - q2.date.getTime();
return h1.initialInfo.start.getTime() - h2.initialInfo.start.getTime();
case SortOrder.DateDesc:
return q2.date.getTime() - q1.date.getTime();
return h2.initialInfo.start.getTime() - h1.initialInfo.start.getTime();
case SortOrder.CountAsc:
return q1.resultCount - q2.resultCount;
return (!q1 || !q2) ? 0 : q1.resultCount - q2.resultCount;
case SortOrder.CountDesc:
return q2.resultCount - q1.resultCount;
return (!q1 || !q2) ? 0 : q2.resultCount - q1.resultCount;
default:
assertNever(this.sortOrder);
}
});
}
getParent(_element: CompletedQuery): vscode.ProviderResult<CompletedQuery> {
getParent(_element: FullQueryInfo): ProviderResult<FullQueryInfo> {
return null;
}
getCurrent(): CompletedQuery | undefined {
getCurrent(): FullQueryInfo | undefined {
return this.current;
}
pushQuery(item: CompletedQuery): void {
pushQuery(item: FullQueryInfo): void {
this.current = item;
this.history.push(item);
this.refresh();
}
setCurrentItem(item: CompletedQuery) {
setCurrentItem(item?: FullQueryInfo) {
this.current = item;
}
remove(item: CompletedQuery) {
if (this.current === item) this.current = undefined;
remove(item: FullQueryInfo) {
if (this.current === item) {
this.current = undefined;
}
const index = this.history.findIndex((i) => i === item);
if (index >= 0) {
this.history.splice(index, 1);
@ -175,16 +208,12 @@ export class HistoryTreeDataProvider extends DisposableObject {
}
}
get allHistory(): CompletedQuery[] {
get allHistory(): FullQueryInfo[] {
return this.history;
}
refresh(completedQuery?: CompletedQuery) {
this._onDidChangeTreeData.fire(completedQuery);
}
find(queryId: number): CompletedQuery | undefined {
return this.allHistory.find((query) => query.query.queryID === queryId);
refresh(item?: FullQueryInfo) {
this._onDidChangeTreeData.fire(item);
}
public get sortOrder() {
@ -204,33 +233,32 @@ export class HistoryTreeDataProvider extends DisposableObject {
const DOUBLE_CLICK_TIME = 500;
const NO_QUERY_SELECTED = 'No query selected. Select a query history item you have already run and try again.';
export class QueryHistoryManager extends DisposableObject {
treeDataProvider: HistoryTreeDataProvider;
treeView: vscode.TreeView<CompletedQuery>;
lastItemClick: { time: Date; item: CompletedQuery } | undefined;
compareWithItem: CompletedQuery | undefined;
treeView: TreeView<FullQueryInfo>;
lastItemClick: { time: Date; item: FullQueryInfo } | undefined;
compareWithItem: FullQueryInfo | undefined;
constructor(
private qs: QueryServerClient,
extensionPath: string,
private queryHistoryConfigListener: QueryHistoryConfig,
private selectedCallback: (item: CompletedQuery) => Promise<void>,
queryHistoryConfigListener: QueryHistoryConfig,
private selectedCallback: (item: FullCompletedQueryInfo) => Promise<void>,
private doCompareCallback: (
from: CompletedQuery,
to: CompletedQuery
from: FullCompletedQueryInfo,
to: FullCompletedQueryInfo
) => Promise<void>
) {
super();
const treeDataProvider = (this.treeDataProvider = new HistoryTreeDataProvider(
this.treeDataProvider = this.push(new HistoryTreeDataProvider(
extensionPath
));
this.treeView = Window.createTreeView('codeQLQueryHistory', {
treeDataProvider,
this.treeView = this.push(window.createTreeView('codeQLQueryHistory', {
treeDataProvider: this.treeDataProvider,
canSelectMany: true,
});
this.push(this.treeView);
this.push(treeDataProvider);
}));
// Lazily update the tree view selection due to limitations of TreeView API (see
// `updateTreeViewSelectionIfVisible` doc for details)
@ -331,20 +359,22 @@ export class QueryHistoryManager extends DisposableObject {
this.push(
commandRunner(
'codeQLQueryHistory.itemClicked',
async (item: CompletedQuery) => {
async (item: FullQueryInfo) => {
return this.handleItemClicked(item, [item]);
}
)
);
queryHistoryConfigListener.onDidChangeConfiguration(() => {
this.treeDataProvider.refresh();
});
this.push(
queryHistoryConfigListener.onDidChangeConfiguration(() => {
this.treeDataProvider.refresh();
})
);
// displays query text in a read-only document
vscode.workspace.registerTextDocumentContentProvider('codeql', {
this.push(workspace.registerTextDocumentContentProvider('codeql', {
provideTextDocumentContent(
uri: vscode.Uri
): vscode.ProviderResult<string> {
uri: Uri
): ProviderResult<string> {
const params = new URLSearchParams(uri.query);
return (
@ -353,19 +383,19 @@ export class QueryHistoryManager extends DisposableObject {
: SHOW_QUERY_TEXT_MSG) + params.get('queryText')
);
},
});
}));
}
async invokeCallbackOn(queryHistoryItem: CompletedQuery) {
if (this.selectedCallback !== undefined) {
async invokeCallbackOn(queryHistoryItem: FullQueryInfo) {
if (this.selectedCallback && queryHistoryItem.isCompleted()) {
const sc = this.selectedCallback;
await sc(queryHistoryItem);
await sc(queryHistoryItem as FullCompletedQueryInfo);
}
}
async handleOpenQuery(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
): Promise<void> {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
if (!this.assertSingleQuery(finalMultiSelect)) {
@ -376,19 +406,23 @@ export class QueryHistoryManager extends DisposableObject {
throw new Error(NO_QUERY_SELECTED);
}
const textDocument = await vscode.workspace.openTextDocument(
vscode.Uri.file(finalSingleItem.query.program.queryPath)
if (!finalSingleItem.completedQuery) {
throw new Error('Select a completed query.');
}
const textDocument = await workspace.openTextDocument(
Uri.file(finalSingleItem.completedQuery.query.program.queryPath)
);
const editor = await vscode.window.showTextDocument(
const editor = await window.showTextDocument(
textDocument,
vscode.ViewColumn.One
ViewColumn.One
);
const queryText = finalSingleItem.options.queryText;
if (queryText !== undefined && finalSingleItem.options.isQuickQuery) {
const queryText = finalSingleItem.initialInfo.queryText;
if (queryText !== undefined && finalSingleItem.initialInfo.isQuickQuery) {
await editor.edit((edit) =>
edit.replace(
textDocument.validateRange(
new vscode.Range(0, 0, textDocument.lineCount, 0)
new Range(0, 0, textDocument.lineCount, 0)
),
queryText
)
@ -397,14 +431,14 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleRemoveHistoryItem(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
(finalMultiSelect || [finalSingleItem]).forEach((item) => {
this.treeDataProvider.remove(item);
item.dispose();
item.completedQuery?.dispose();
});
const current = this.treeDataProvider.getCurrent();
if (current !== undefined) {
@ -438,22 +472,22 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleSetLabel(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
): Promise<void> {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
const response = await vscode.window.showInputBox({
const response = await window.showInputBox({
prompt: 'Label:',
placeHolder: '(use default)',
value: singleItem.getLabel(),
value: singleItem.label,
});
// undefined response means the user cancelled the dialog; don't change anything
if (response !== undefined) {
// Interpret empty string response as 'go back to using default'
singleItem.options.label = response === '' ? undefined : response;
singleItem.initialInfo.userSpecifiedLabel = response === '' ? undefined : response;
if (this.treeDataProvider.sortOrder === SortOrder.NameAsc ||
this.treeDataProvider.sortOrder === SortOrder.NameDesc) {
this.treeDataProvider.refresh();
@ -464,19 +498,19 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleCompareWith(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
) {
try {
if (!singleItem.didRunSuccessfully) {
if (!singleItem.completedQuery?.didRunSuccessfully) {
throw new Error('Please select a successful query.');
}
const from = this.compareWithItem || singleItem;
const to = await this.findOtherQueryToCompare(from, multiSelect);
if (from && to) {
await this.doCompareCallback(from, to);
if (from.isCompleted() && to?.isCompleted()) {
await this.doCompareCallback(from as FullCompletedQueryInfo, to as FullCompletedQueryInfo);
}
} catch (e) {
void showAndLogErrorMessage(e.message);
@ -484,8 +518,8 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleItemClicked(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
if (!this.assertSingleQuery(finalMultiSelect)) {
@ -516,23 +550,27 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleShowQueryLog(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
if (singleItem.logFileLocation) {
await this.tryOpenExternalFile(singleItem.logFileLocation);
if (!singleItem.completedQuery) {
return;
}
if (singleItem.completedQuery.logFileLocation) {
await this.tryOpenExternalFile(singleItem.completedQuery.logFileLocation);
} else {
void showAndLogWarningMessage('No log file available');
}
}
async handleShowQueryText(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
@ -542,35 +580,38 @@ export class QueryHistoryManager extends DisposableObject {
throw new Error(NO_QUERY_SELECTED);
}
const queryName = singleItem.queryName.endsWith('.ql')
? singleItem.queryName
: singleItem.queryName + '.ql';
const params = new URLSearchParams({
isQuickEval: String(!!singleItem.query.quickEvalPosition),
queryText: encodeURIComponent(await this.getQueryText(singleItem)),
});
const uri = vscode.Uri.parse(
`codeql:${singleItem.query.queryID}-${queryName}?${params.toString()}`, true
);
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc, { preview: false });
}
async handleViewSarifAlerts(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
) {
if (!this.assertSingleQuery(multiSelect)) {
if (!singleItem.completedQuery) {
return;
}
const hasInterpretedResults = await singleItem.query.canHaveInterpretedResults();
const rawQueryName = singleItem.getQueryName();
const queryName = rawQueryName.endsWith('.ql') ? rawQueryName : rawQueryName + '.ql';
const params = new URLSearchParams({
isQuickEval: String(!!singleItem.completedQuery.query.quickEvalPosition),
queryText: encodeURIComponent(await this.getQueryText(singleItem)),
});
const uri = Uri.parse(
`codeql:${singleItem.completedQuery.query.queryID}-${queryName}?${params.toString()}`, true
);
const doc = await workspace.openTextDocument(uri);
await window.showTextDocument(doc, { preview: false });
}
async handleViewSarifAlerts(
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
) {
if (!this.assertSingleQuery(multiSelect) || !singleItem.completedQuery) {
return;
}
const query = singleItem.completedQuery.query;
const hasInterpretedResults = await query.canHaveInterpretedResults();
if (hasInterpretedResults) {
await this.tryOpenExternalFile(
singleItem.query.resultsPaths.interpretedResultsPath
query.resultsPaths.interpretedResultsPath
);
} else {
const label = singleItem.getLabel();
const label = singleItem.label;
void showAndLogInformationMessage(
`Query ${label} has no interpreted results.`
);
@ -578,81 +619,87 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleViewCsvResults(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
if (await singleItem.query.hasCsv()) {
void this.tryOpenExternalFile(singleItem.query.csvPath);
if (!singleItem.completedQuery) {
return;
}
await singleItem.query.exportCsvResults(this.qs, singleItem.query.csvPath, () => {
const query = singleItem.completedQuery.query;
if (await query.hasCsv()) {
void this.tryOpenExternalFile(query.csvPath);
return;
}
await query.exportCsvResults(this.qs, query.csvPath, () => {
void this.tryOpenExternalFile(
singleItem.query.csvPath
query.csvPath
);
});
}
async handleViewCsvAlerts(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
) {
if (!this.assertSingleQuery(multiSelect)) {
if (!this.assertSingleQuery(multiSelect) || !singleItem.completedQuery) {
return;
}
await this.tryOpenExternalFile(
await singleItem.query.ensureCsvProduced(this.qs)
await singleItem.completedQuery.query.ensureCsvProduced(this.qs)
);
}
async handleViewDil(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[],
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[],
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
if (!singleItem.completedQuery) {
return;
}
await this.tryOpenExternalFile(
await singleItem.query.ensureDilPath(this.qs)
await singleItem.completedQuery.query.ensureDilPath(this.qs)
);
}
async getQueryText(queryHistoryItem: CompletedQuery): Promise<string> {
if (queryHistoryItem.options.queryText) {
return queryHistoryItem.options.queryText;
} else if (queryHistoryItem.query.quickEvalPosition) {
async getQueryText(queryHistoryItem: FullQueryInfo): Promise<string> {
if (queryHistoryItem.initialInfo.queryText) {
return queryHistoryItem.initialInfo.queryText;
}
if (!queryHistoryItem.completedQuery) {
return '<No label>';
}
const query = queryHistoryItem.completedQuery.query;
if (query.quickEvalPosition) {
// capture all selected lines
const startLine = queryHistoryItem.query.quickEvalPosition.line;
const endLine = queryHistoryItem.query.quickEvalPosition.endLine;
const textDocument = await vscode.workspace.openTextDocument(
queryHistoryItem.query.quickEvalPosition.fileName
const startLine = query.quickEvalPosition.line;
const endLine = query.quickEvalPosition.endLine;
const textDocument = await workspace.openTextDocument(
query.quickEvalPosition.fileName
);
return textDocument.getText(
new vscode.Range(startLine - 1, 0, endLine, 0)
new Range(startLine - 1, 0, endLine, 0)
);
} else {
return '';
}
}
buildCompletedQuery(info: QueryWithResults): CompletedQuery {
const item = new CompletedQuery(info, this.queryHistoryConfigListener);
return item;
}
addCompletedQuery(item: CompletedQuery) {
addCompletedQuery(item: FullQueryInfo) {
this.treeDataProvider.pushQuery(item);
this.updateTreeViewSelectionIfVisible();
}
find(queryId: number): CompletedQuery | undefined {
return this.treeDataProvider.find(queryId);
}
/**
* Update the tree view selection if the tree view is visible.
*
@ -674,9 +721,9 @@ export class QueryHistoryManager extends DisposableObject {
}
private async tryOpenExternalFile(fileLocation: string) {
const uri = vscode.Uri.file(fileLocation);
const uri = Uri.file(fileLocation);
try {
await vscode.window.showTextDocument(uri, { preview: false });
await window.showTextDocument(uri, { preview: false });
} catch (e) {
if (
e.message.includes(
@ -693,7 +740,7 @@ the file in the file explorer and dragging it into the workspace.`
);
if (res) {
try {
await vscode.commands.executeCommand('revealFileInOS', uri);
await commands.executeCommand('revealFileInOS', uri);
} catch (e) {
void showAndLogErrorMessage(e.message);
}
@ -707,20 +754,26 @@ the file in the file explorer and dragging it into the workspace.`
}
private async findOtherQueryToCompare(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
): Promise<CompletedQuery | undefined> {
const dbName = singleItem.database.name;
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
): Promise<FullQueryInfo | undefined> {
if (!singleItem.completedQuery) {
return undefined;
}
const dbName = singleItem.initialInfo.databaseInfo.name;
// if exactly 2 queries are selected, use those
if (multiSelect?.length === 2) {
// return the query that is not the first selected one
const otherQuery =
singleItem === multiSelect[0] ? multiSelect[1] : multiSelect[0];
if (!otherQuery.didRunSuccessfully) {
if (!otherQuery.completedQuery) {
throw new Error('Please select a completed query.');
}
if (!otherQuery.completedQuery.didRunSuccessfully) {
throw new Error('Please select a successful query.');
}
if (otherQuery.database.name !== dbName) {
if (otherQuery.initialInfo.databaseInfo.name !== dbName) {
throw new Error('Query databases must be the same.');
}
return otherQuery;
@ -735,23 +788,24 @@ the file in the file explorer and dragging it into the workspace.`
.filter(
(otherQuery) =>
otherQuery !== singleItem &&
otherQuery.didRunSuccessfully &&
otherQuery.database.name === dbName
otherQuery.completedQuery &&
otherQuery.completedQuery.didRunSuccessfully &&
otherQuery.initialInfo.databaseInfo.name === dbName
)
.map((otherQuery) => ({
label: otherQuery.toString(),
description: otherQuery.databaseName,
detail: otherQuery.statusString,
query: otherQuery,
.map((item) => ({
label: item.label,
description: item.initialInfo.databaseInfo.name,
detail: item.completedQuery!.statusString,
query: item,
}));
if (comparableQueryLabels.length < 1) {
throw new Error('No other queries available to compare with.');
}
const choice = await vscode.window.showQuickPick(comparableQueryLabels);
const choice = await window.showQuickPick(comparableQueryLabels);
return choice?.query;
}
private assertSingleQuery(multiSelect: CompletedQuery[] = [], message = 'Please select a single query.') {
private assertSingleQuery(multiSelect: FullQueryInfo[] = [], message = 'Please select a single query.') {
if (multiSelect.length > 1) {
void showAndLogErrorMessage(
message
@ -778,7 +832,7 @@ the file in the file explorer and dragging it into the workspace.`
*
* @param newSelection the new selection after the most recent selection change
*/
private updateCompareWith(newSelection: CompletedQuery[]) {
private updateCompareWith(newSelection: FullQueryInfo[]) {
if (newSelection.length === 1) {
this.compareWithItem = newSelection[0];
} else if (
@ -799,9 +853,9 @@ the file in the file explorer and dragging it into the workspace.`
* @param multiSelect a multi-select or undefined if no items are selected
*/
private determineSelection(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
): { finalSingleItem: CompletedQuery; finalMultiSelect: CompletedQuery[] } {
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
): { finalSingleItem: FullQueryInfo; finalMultiSelect: FullQueryInfo[] } {
if (singleItem === undefined && (multiSelect === undefined || multiSelect.length === 0 || multiSelect[0] === undefined)) {
const selection = this.treeView.selection;
if (selection) {
@ -817,7 +871,7 @@ the file in the file explorer and dragging it into the workspace.`
};
}
async refreshTreeView(completedQuery: CompletedQuery): Promise<void> {
this.treeDataProvider.refresh(completedQuery);
async refreshTreeView(item: FullQueryInfo): Promise<void> {
this.treeDataProvider.refresh(item);
}
}

Просмотреть файл

@ -1,23 +1,46 @@
import { env } from 'vscode';
import { QueryWithResults, tmpDir, QueryInfo } from './run-queries';
import { QueryWithResults, tmpDir, QueryEvaluatonInfo } from './run-queries';
import * as messages from './pure/messages';
import * as cli from './cli';
import * as sarif from 'sarif';
import * as fs from 'fs-extra';
import * as path from 'path';
import { RawResultsSortState, SortedResultSetInfo, DatabaseInfo, QueryMetadata, InterpretedResultsSortState, ResultsPaths } from './pure/interface-types';
import {
RawResultsSortState,
SortedResultSetInfo,
QueryMetadata,
InterpretedResultsSortState,
ResultsPaths
} from './pure/interface-types';
import { QueryHistoryConfig } from './config';
import { QueryHistoryItemOptions } from './query-history';
import { DatabaseInfo } from './pure/interface-types';
export class CompletedQuery implements QueryWithResults {
readonly date: Date;
readonly time: string;
readonly query: QueryInfo;
/**
* A description of the information about a query
* that is available before results are populated.
*/
export interface InitialQueryInfo {
userSpecifiedLabel?: string; // if missing, use a default label
readonly queryText?: string; // text of the selected file
readonly isQuickQuery: boolean;
readonly isQuickEval: boolean;
readonly quickEvalPosition?: messages.Position;
readonly queryPath: string;
readonly databaseInfo: DatabaseInfo
readonly start: Date;
}
export enum QueryStatus {
InProgress = 'InProgress',
Completed = 'Completed',
Failed = 'Failed',
}
export class CompletedQueryInfo implements QueryWithResults {
readonly query: QueryEvaluatonInfo;
readonly result: messages.EvaluationResult;
readonly database: DatabaseInfo;
readonly logFileLocation?: string;
options: QueryHistoryItemOptions;
resultCount: number;
dispose: () => void;
@ -37,17 +60,12 @@ export class CompletedQuery implements QueryWithResults {
constructor(
evaluation: QueryWithResults,
public config: QueryHistoryConfig,
) {
this.query = evaluation.query;
this.result = evaluation.result;
this.database = evaluation.database;
this.logFileLocation = evaluation.logFileLocation;
this.options = evaluation.options;
this.dispose = evaluation.dispose;
this.date = new Date();
this.time = this.date.toLocaleString(env.language);
this.sortedResultsInfo = new Map();
this.resultCount = 0;
}
@ -56,26 +74,16 @@ export class CompletedQuery implements QueryWithResults {
this.resultCount = value;
}
get databaseName(): string {
return this.database.name;
}
get queryName(): string {
return getQueryName(this.query);
}
get queryFileName(): string {
return getQueryFileName(this.query);
}
get statusString(): string {
switch (this.result.resultType) {
case messages.QueryResultType.CANCELLATION:
return `cancelled after ${this.result.evaluationTime / 1000} seconds`;
return `cancelled after ${Math.round(this.result.evaluationTime / 1000)} seconds`;
case messages.QueryResultType.OOM:
return 'out of memory';
case messages.QueryResultType.SUCCESS:
return `finished in ${this.result.evaluationTime / 1000} seconds`;
return `finished in ${Math.round(this.result.evaluationTime / 1000)} seconds`;
case messages.QueryResultType.TIMEOUT:
return `timed out after ${this.result.evaluationTime / 1000} seconds`;
return `timed out after ${Math.round(this.result.evaluationTime / 1000)} seconds`;
case messages.QueryResultType.OTHER_ERROR:
default:
return this.result.message ? `failed: ${this.result.message}` : 'failed';
@ -90,36 +98,10 @@ export class CompletedQuery implements QueryWithResults {
|| this.query.resultsPaths.resultsPath;
}
interpolate(template: string): string {
const { databaseName, queryName, time, resultCount, statusString, queryFileName } = this;
const replacements: { [k: string]: string } = {
t: time,
q: queryName,
d: databaseName,
r: resultCount.toString(),
s: statusString,
f: queryFileName,
'%': '%',
};
return template.replace(/%(.)/g, (match, key) => {
const replacement = replacements[key];
return replacement !== undefined ? replacement : match;
});
}
getLabel(): string {
return this.options?.label
|| this.config.format;
}
get didRunSuccessfully(): boolean {
return this.result.resultType === messages.QueryResultType.SUCCESS;
}
toString(): string {
return this.interpolate(this.getLabel());
}
async updateSortState(
server: cli.CodeQLCliServer,
resultSetName: string,
@ -151,36 +133,6 @@ export class CompletedQuery implements QueryWithResults {
}
/**
* Gets a human-readable name for an evaluated query.
* Uses metadata if it exists, and defaults to the query file name.
*/
export function getQueryName(query: QueryInfo) {
if (query.quickEvalPosition !== undefined) {
return 'Quick evaluation of ' + getQueryFileName(query);
} else if (query.metadata?.name) {
return query.metadata.name;
} else {
return getQueryFileName(query);
}
}
/**
* Gets the file name for an evaluated query.
* Defaults to the query file name and may contain position information for quick eval queries.
*/
export function getQueryFileName(query: QueryInfo) {
// Queries run through quick evaluation are not usually the entire query file.
// Label them differently and include the line numbers.
if (query.quickEvalPosition !== undefined) {
const { line, endLine, fileName } = query.quickEvalPosition;
const lineInfo = line === endLine ? `${line}` : `${line}-${endLine}`;
return `${path.basename(fileName)}:${lineInfo}`;
}
return path.basename(query.program.queryPath);
}
/**
* Call cli command to interpret results.
*/
@ -211,3 +163,121 @@ export function ensureMetadataIsComplete(metadata: QueryMetadata | undefined) {
}
return metadata;
}
/**
* Used in Interface and Compare-Interface for queries that we know have been complated.
*/
export type FullCompletedQueryInfo = FullQueryInfo & {
completedQuery: CompletedQueryInfo
};
export class FullQueryInfo {
public failureReason: string | undefined;
public completedQuery: CompletedQueryInfo | undefined;
constructor(
public readonly initialInfo: InitialQueryInfo,
private readonly config: QueryHistoryConfig,
) {
/**/
}
get time() {
return this.initialInfo.start.toLocaleString(env.language);
}
interpolate(template: string): string {
const { resultCount = 0, statusString = 'in progress' } = this.completedQuery || {};
const replacements: { [k: string]: string } = {
t: this.time,
q: this.getQueryName(),
d: this.initialInfo.databaseInfo.name,
r: resultCount.toString(),
s: statusString,
f: this.getQueryFileName(),
'%': '%',
};
return template.replace(/%(.)/g, (match, key) => {
const replacement = replacements[key];
return replacement !== undefined ? replacement : match;
});
}
/**
* Returns a label for this query that includes interpolated values.
*/
get label(): string {
return this.interpolate(this.initialInfo.userSpecifiedLabel ?? this.config.format ?? '');
}
/**
* Avoids getting the default label for the query.
* If there is a custom label for this query, interpolate and use that.
* Otherwise, use the name of the query.
*
* @returns the name of the query, unless there is a custom label for this query.
*/
getShortLabel(): string {
return this.initialInfo.userSpecifiedLabel
? this.interpolate(this.initialInfo.userSpecifiedLabel)
: this.getQueryName();
}
/**
* The query's file name, unless it is a quick eval.
* Queries run through quick evaluation are not usually the entire query file.
* Label them differently and include the line numbers.
*/
getQueryFileName() {
if (this.initialInfo.quickEvalPosition) {
const { line, endLine, fileName } = this.initialInfo.quickEvalPosition;
const lineInfo = line === endLine ? `${line}` : `${line}-${endLine}`;
return `${path.basename(fileName)}:${lineInfo}`;
}
return path.basename(this.initialInfo.queryPath);
}
/**
* Three cases:
*
* - If this is a completed query, use the query name from the query metadata.
* - If this is a quick eval, return the query name with a prefix
* - Otherwise, return the query file name.
*/
getQueryName() {
if (this.initialInfo.quickEvalPosition) {
return 'Quick evaluation of ' + this.getQueryFileName();
} else if (this.completedQuery?.query.metadata?.name) {
return this.completedQuery?.query.metadata?.name;
} else {
return this.getQueryFileName();
}
}
isCompleted(): boolean {
return !!this.completedQuery;
}
completeThisQuery(info: QueryWithResults) {
this.completedQuery = new CompletedQueryInfo(info);
}
/**
* If there is a failure reason, then this query has failed.
* If there is no completed query, then this query is still running.
* If there is a completed query, then check if didRunSuccessfully.
* If true, then this query has completed successfully, otherwise it has failed.
*/
get status(): QueryStatus {
if (this.failureReason) {
return QueryStatus.Failed;
} else if (!this.completedQuery) {
return QueryStatus.InProgress;
} else if (this.completedQuery.didRunSuccessfully) {
return QueryStatus.Completed;
} else {
return QueryStatus.Failed;
}
}
}

Просмотреть файл

@ -58,7 +58,7 @@ export class RemoteQueriesInterfaceManager {
/**
* Builds up a model tailored to the view based on the query and result domain entities.
* The data is cleaned up, sorted where necessary, and transformed to a format that
* the view model can use.
* the view model can use.
* @param query Information about the query that was run.
* @param queryResult The result of the query.
* @returns A fully created view model.
@ -127,10 +127,12 @@ export class RemoteQueriesInterfaceManager {
scriptPathOnDisk,
[baseStylesheetUriOnDisk, stylesheetPathOnDisk]
);
panel.webview.onDidReceiveMessage(
async (e) => this.handleMsgFromView(e),
undefined,
ctx.subscriptions
ctx.subscriptions.push(
panel.webview.onDidReceiveMessage(
async (e) => this.handleMsgFromView(e),
undefined,
ctx.subscriptions
)
);
}
return this.panel;

Просмотреть файл

@ -22,7 +22,7 @@ import { ProgressCallback, UserCancellationException } from './commandRunner';
import { DatabaseInfo, QueryMetadata, ResultsPaths } from './pure/interface-types';
import { logger } from './logging';
import * as messages from './pure/messages';
import { QueryHistoryItemOptions } from './query-history';
import { InitialQueryInfo } from './query-results';
import * as qsClient from './queryserver-client';
import { isQuickQueryPath } from './quick-query';
import { compileDatabaseUpgradeSequence, hasNondestructiveUpgradeCapabilities, upgradeDatabaseExplicit } from './upgrades';
@ -53,7 +53,7 @@ export const tmpDirDisposal = {
* temporary files associated with it, such as the compiled query
* output and results.
*/
export class QueryInfo {
export class QueryEvaluatonInfo {
private static nextQueryId = 0;
readonly compiledQueryPath: string;
@ -71,7 +71,7 @@ export class QueryInfo {
public readonly metadata?: QueryMetadata,
public readonly templates?: messages.TemplateDefinitions,
) {
this.queryID = QueryInfo.nextQueryId++;
this.queryID = QueryEvaluatonInfo.nextQueryId++;
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${this.queryID}.qlo`);
this.dilPath = path.join(tmpDir.name, `results${this.queryID}.dil`);
this.csvPath = path.join(tmpDir.name, `results${this.queryID}.csv`);
@ -269,10 +269,8 @@ export class QueryInfo {
export interface QueryWithResults {
readonly query: QueryInfo;
readonly query: QueryEvaluatonInfo;
readonly result: messages.EvaluationResult;
readonly database: DatabaseInfo;
readonly options: QueryHistoryItemOptions;
readonly logFileLocation?: string;
readonly dispose: () => void;
}
@ -356,7 +354,7 @@ async function getSelectedPosition(editor: TextEditor, range?: Range): Promise<m
async function checkDbschemeCompatibility(
cliServer: cli.CodeQLCliServer,
qs: qsClient.QueryServerClient,
query: QueryInfo,
query: QueryEvaluatonInfo,
progress: ProgressCallback,
token: CancellationToken,
): Promise<void> {
@ -398,7 +396,7 @@ async function checkDbschemeCompatibility(
}
}
function reportNoUpgradePath(query: QueryInfo) {
function reportNoUpgradePath(query: QueryEvaluatonInfo) {
throw new Error(`Query ${query.program.queryPath} expects database scheme ${query.queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace.\n\nPlease try using a newer version of the query libraries.`);
}
@ -408,7 +406,7 @@ function reportNoUpgradePath(query: QueryInfo) {
async function compileNonDestructiveUpgrade(
qs: qsClient.QueryServerClient,
upgradeTemp: tmp.DirectoryResult,
query: QueryInfo,
query: QueryEvaluatonInfo,
progress: ProgressCallback,
token: CancellationToken,
): Promise<string> {
@ -557,32 +555,19 @@ export async function compileAndRunQueryAgainstDatabase(
cliServer: cli.CodeQLCliServer,
qs: qsClient.QueryServerClient,
db: DatabaseItem,
quickEval: boolean,
selectedQueryUri: Uri | undefined,
initialInfo: InitialQueryInfo,
progress: ProgressCallback,
token: CancellationToken,
templates?: messages.TemplateDefinitions,
range?: Range
): Promise<QueryWithResults> {
if (!db.contents || !db.contents.dbSchemeUri) {
throw new Error(`Database ${db.databaseUri} does not have a CodeQL database scheme.`);
}
// Determine which query to run, based on the selection and the active editor.
const { queryPath, quickEvalPosition, quickEvalText } = await determineSelectedQuery(selectedQueryUri, quickEval, range);
const historyItemOptions: QueryHistoryItemOptions = {};
historyItemOptions.isQuickQuery === isQuickQueryPath(queryPath);
if (quickEval) {
historyItemOptions.queryText = quickEvalText;
} else {
historyItemOptions.queryText = await fs.readFile(queryPath, 'utf8');
}
// Get the workspace folder paths.
const diskWorkspaceFolders = getOnDiskWorkspaceFolders();
// Figure out the library path for the query.
const packConfig = await cliServer.resolveLibraryPath(diskWorkspaceFolders, queryPath);
const packConfig = await cliServer.resolveLibraryPath(diskWorkspaceFolders, initialInfo.queryPath);
if (!packConfig.dbscheme) {
throw new Error('Could not find a database scheme for this query. Please check that you have a valid qlpack.yml file for this query, which refers to a database scheme either in the `dbscheme` field or through one of its dependencies.');
@ -596,7 +581,7 @@ export async function compileAndRunQueryAgainstDatabase(
const dbSchemaName = path.basename(db.contents.dbSchemeUri.fsPath);
if (querySchemaName != dbSchemaName) {
void logger.log(`Query schema was ${querySchemaName}, but database schema was ${dbSchemaName}.`);
throw new Error(`The query ${path.basename(queryPath)} cannot be run against the selected database (${db.name}): their target languages are different. Please select a different database and try again.`);
throw new Error(`The query ${path.basename(initialInfo.queryPath)} cannot be run against the selected database (${db.name}): their target languages are different. Please select a different database and try again.`);
}
const qlProgram: messages.QlProgram = {
@ -608,7 +593,7 @@ export async function compileAndRunQueryAgainstDatabase(
// we use the database's DB scheme here instead of the DB scheme
// from the current document's project.
dbschemePath: db.contents.dbSchemeUri.fsPath,
queryPath: queryPath
queryPath: initialInfo.queryPath
};
// Read the query metadata if possible, to use in the UI.
@ -632,7 +617,7 @@ export async function compileAndRunQueryAgainstDatabase(
}
}
const query = new QueryInfo(qlProgram, db, packConfig.dbscheme, quickEvalPosition, metadata, templates);
const query = new QueryEvaluatonInfo(qlProgram, db, packConfig.dbscheme, initialInfo.quickEvalPosition, metadata, templates);
const upgradeDir = await tmp.dir({ dir: upgradesTmpDir.name, unsafeCleanup: true });
try {
@ -647,7 +632,7 @@ export async function compileAndRunQueryAgainstDatabase(
errors = await query.compile(qs, progress, token);
} catch (e) {
if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
return createSyntheticResult(query, db, historyItemOptions, 'Query cancelled', messages.QueryResultType.CANCELLATION);
return createSyntheticResult(query, 'Query cancelled', messages.QueryResultType.CANCELLATION);
} else {
throw e;
}
@ -663,11 +648,6 @@ export async function compileAndRunQueryAgainstDatabase(
return {
query,
result,
database: {
name: db.name,
databaseUri: db.databaseUri.toString(true)
},
options: historyItemOptions,
logFileLocation: result.logFileLocation,
dispose: () => {
qs.logger.removeAdditionalLogLocation(result.logFileLocation);
@ -688,16 +668,16 @@ export async function compileAndRunQueryAgainstDatabase(
formattedMessages.push(formatted);
void qs.logger.log(formatted);
}
if (quickEval && formattedMessages.length <= 2) {
if (initialInfo.isQuickEval && formattedMessages.length <= 2) {
// If there are more than 2 error messages, they will not be displayed well in a popup
// and will be trimmed by the function displaying the error popup. Accordingly, we only
// try to show the errors if there are 2 or less, otherwise we direct the user to the log.
void showAndLogErrorMessage('Quick evaluation compilation failed: ' + formattedMessages.join('\n'));
} else {
void showAndLogErrorMessage((quickEval ? 'Quick evaluation' : 'Query') + compilationFailedErrorTail);
void showAndLogErrorMessage((initialInfo.isQuickEval ? 'Quick evaluation' : 'Query') + compilationFailedErrorTail);
}
return createSyntheticResult(query, db, historyItemOptions, 'Query had compilation errors', messages.QueryResultType.OTHER_ERROR);
return createSyntheticResult(query, 'Query had compilation errors', messages.QueryResultType.OTHER_ERROR);
}
} finally {
try {
@ -708,14 +688,36 @@ export async function compileAndRunQueryAgainstDatabase(
}
}
export async function createInitialQueryInfo(
selectedQueryUri: Uri | undefined,
databaseInfo: DatabaseInfo,
isQuickEval: boolean, range?: Range
): Promise<InitialQueryInfo> {
// Determine which query to run, based on the selection and the active editor.
const { queryPath, quickEvalPosition, quickEvalText } = await determineSelectedQuery(selectedQueryUri, isQuickEval, range);
return {
queryPath,
isQuickEval,
isQuickQuery: isQuickQueryPath(queryPath),
databaseInfo,
start: new Date(),
... (isQuickEval ? {
queryText: quickEvalText,
quickEvalPosition: quickEvalPosition
} : {
queryText: await fs.readFile(queryPath, 'utf8')
})
};
}
const compilationFailedErrorTail = ' compilation failed. Please make sure there are no errors in the query, the database is up to date,' +
' and the query and database use the same target language. For more details on the error, go to View > Output,' +
' and choose CodeQL Query Server from the dropdown.';
function createSyntheticResult(
query: QueryInfo,
db: DatabaseItem,
historyItemOptions: QueryHistoryItemOptions,
query: QueryEvaluatonInfo,
message: string,
resultType: number
): QueryWithResults {
@ -729,11 +731,6 @@ function createSyntheticResult(
runId: -1,
message
},
database: {
name: db.name,
databaseUri: db.databaseUri.toString(true)
},
options: historyItemOptions,
dispose: () => { /**/ },
};
}

Просмотреть файл

@ -11,7 +11,7 @@ import { DatabaseItem, DatabaseManager } from '../../databases';
import { CodeQLExtensionInterface } from '../../extension';
import { dbLoc, storagePath } from './global.helper';
import { importArchiveDatabase } from '../../databaseFetcher';
import { compileAndRunQueryAgainstDatabase } from '../../run-queries';
import { compileAndRunQueryAgainstDatabase, createInitialQueryInfo } from '../../run-queries';
import { CodeQLCliServer } from '../../cli';
import { QueryServerClient } from '../../queryserver-client';
import { skipIfNoCodeQL } from '../ensureCli';
@ -96,15 +96,12 @@ describe('Queries', function() {
cli,
qs,
dbItem,
false,
Uri.file(queryPath),
await mockInitialQueryInfo(queryPath),
progress,
token
);
// just check that the query was successful
expect(result.database.name).to.eq('db');
expect(result.options.queryText).to.eq(fs.readFileSync(queryPath, 'utf8'));
expect(result.result.resultType).to.eq(QueryResultType.SUCCESS);
} catch (e) {
console.error('Test Failed');
@ -121,15 +118,13 @@ describe('Queries', function() {
cli,
qs,
dbItem,
false,
Uri.file(queryPath),
await mockInitialQueryInfo(queryPath),
progress,
token
);
// this message would indicate that the databases were not properly reregistered
expect(result.result.message).not.to.eq('No result from server');
expect(result.options.queryText).to.eq(fs.readFileSync(queryPath, 'utf8'));
expect(result.result.resultType).to.eq(QueryResultType.SUCCESS);
} catch (e) {
console.error('Test Failed');
@ -174,4 +169,15 @@ describe('Queries', function() {
// ignore
}
}
async function mockInitialQueryInfo(queryPath: string) {
return await createInitialQueryInfo(
Uri.file(queryPath),
{
name: dbItem.name,
databaseUri: dbItem.databaseUri.toString(),
},
false
);
}
});

Просмотреть файл

@ -6,8 +6,11 @@ import * as sinon from 'sinon';
import * as chaiAsPromised from 'chai-as-promised';
import { logger } from '../../logging';
import { QueryHistoryManager, HistoryTreeDataProvider } from '../../query-history';
import { CompletedQuery } from '../../query-results';
import { QueryInfo } from '../../run-queries';
import { QueryEvaluatonInfo, QueryWithResults } from '../../run-queries';
import { QueryHistoryConfigListener } from '../../config';
import * as messages from '../../pure/messages';
import { QueryServerClient } from '../../queryserver-client';
import { FullQueryInfo, InitialQueryInfo } from '../../query-results';
chai.use(chaiAsPromised);
const expect = chai.expect;
@ -15,10 +18,14 @@ const assert = chai.assert;
describe('query-history', () => {
let configListener: QueryHistoryConfigListener;
let showTextDocumentSpy: sinon.SinonStub;
let showInformationMessageSpy: sinon.SinonStub;
let executeCommandSpy: sinon.SinonStub;
let showQuickPickSpy: sinon.SinonStub;
let queryHistoryManager: QueryHistoryManager | undefined;
let selectedCallback: sinon.SinonStub;
let doCompareCallback: sinon.SinonStub;
let tryOpenExternalFile: Function;
let sandbox: sinon.SinonSandbox;
@ -38,9 +45,16 @@ describe('query-history', () => {
executeCommandSpy = sandbox.stub(vscode.commands, 'executeCommand');
sandbox.stub(logger, 'log');
tryOpenExternalFile = (QueryHistoryManager.prototype as any).tryOpenExternalFile;
configListener = new QueryHistoryConfigListener();
selectedCallback = sandbox.stub();
doCompareCallback = sandbox.stub();
});
afterEach(() => {
afterEach(async () => {
if (queryHistoryManager) {
queryHistoryManager.dispose();
queryHistoryManager = undefined;
}
sandbox.restore();
});
@ -85,24 +99,24 @@ describe('query-history', () => {
});
});
let allHistory: FullQueryInfo[];
beforeEach(() => {
allHistory = [
createMockFullQueryInfo('a', createMockQueryWithResults(true)),
createMockFullQueryInfo('b', createMockQueryWithResults(true)),
createMockFullQueryInfo('a', createMockQueryWithResults(false)),
createMockFullQueryInfo('a', createMockQueryWithResults(true)),
];
});
describe('findOtherQueryToCompare', () => {
let allHistory: { database: { name: string }; didRunSuccessfully: boolean }[];
beforeEach(() => {
allHistory = [
{ didRunSuccessfully: true, database: { name: 'a' } },
{ didRunSuccessfully: true, database: { name: 'b' } },
{ didRunSuccessfully: false, database: { name: 'a' } },
{ didRunSuccessfully: true, database: { name: 'a' } },
];
});
it('should find the second query to compare when one is selected', async () => {
const thisQuery = allHistory[3];
const queryHistory = createMockQueryHistory(allHistory);
queryHistoryManager = await createMockQueryHistory(allHistory);
showQuickPickSpy.returns({ query: allHistory[0] });
const otherQuery = await queryHistory.findOtherQueryToCompare(thisQuery, []);
const otherQuery = await (queryHistoryManager as any).findOtherQueryToCompare(thisQuery, []);
expect(otherQuery).to.eq(allHistory[0]);
// only called with first item, other items filtered out
@ -112,9 +126,9 @@ describe('query-history', () => {
it('should handle cancelling out of the quick select', async () => {
const thisQuery = allHistory[3];
const queryHistory = createMockQueryHistory(allHistory);
queryHistoryManager = await createMockQueryHistory(allHistory);
const otherQuery = await queryHistory.findOtherQueryToCompare(thisQuery, []);
const otherQuery = await (queryHistoryManager as any).findOtherQueryToCompare(thisQuery, []);
expect(otherQuery).to.be.undefined;
// only called with first item, other items filtered out
@ -124,20 +138,20 @@ describe('query-history', () => {
it('should compare against 2 queries', async () => {
const thisQuery = allHistory[3];
const queryHistory = createMockQueryHistory(allHistory);
queryHistoryManager = await createMockQueryHistory(allHistory);
const otherQuery = await queryHistory.findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0]]);
const otherQuery = await (queryHistoryManager as any).findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0]]);
expect(otherQuery).to.eq(allHistory[0]);
expect(showQuickPickSpy).not.to.have.been.called;
});
it('should throw an error when a query is not successful', async () => {
const thisQuery = allHistory[3];
const queryHistory = createMockQueryHistory(allHistory);
allHistory[0].didRunSuccessfully = false;
queryHistoryManager = await createMockQueryHistory(allHistory);
allHistory[0] = createMockFullQueryInfo('a', createMockQueryWithResults(false));
try {
await queryHistory.findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0]]);
await (queryHistoryManager as any).findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0]]);
assert(false, 'Should have thrown');
} catch (e) {
expect(e.message).to.eq('Please select a successful query.');
@ -145,12 +159,12 @@ describe('query-history', () => {
});
it('should throw an error when a databases are not the same', async () => {
const thisQuery = allHistory[3];
const queryHistory = createMockQueryHistory(allHistory);
allHistory[0].database.name = 'c';
queryHistoryManager = await createMockQueryHistory(allHistory);
try {
await queryHistory.findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0]]);
// allHistory[0] is database a
// allHistory[1] is database b
await (queryHistoryManager as any).findOtherQueryToCompare(allHistory[0], [allHistory[0], allHistory[1]]);
assert(false, 'Should have thrown');
} catch (e) {
expect(e.message).to.eq('Query databases must be the same.');
@ -159,10 +173,10 @@ describe('query-history', () => {
it('should throw an error when more than 2 queries selected', async () => {
const thisQuery = allHistory[3];
const queryHistory = createMockQueryHistory(allHistory);
queryHistoryManager = await createMockQueryHistory(allHistory);
try {
await queryHistory.findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0], allHistory[1]]);
await (queryHistoryManager as any).findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0], allHistory[1]]);
assert(false, 'Should have thrown');
} catch (e) {
expect(e.message).to.eq('Please select no more than 2 queries.');
@ -170,39 +184,116 @@ describe('query-history', () => {
});
});
describe('handleItemClicked', () => {
it('should call the selectedCallback when an item is clicked', async () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
await queryHistoryManager.handleItemClicked(allHistory[0], [allHistory[0]]);
expect(selectedCallback).to.have.been.calledOnceWith(allHistory[0]);
expect(queryHistoryManager.treeDataProvider.getCurrent()).to.eq(allHistory[0]);
});
it('should do nothing if there is a multi-selection', async () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
await queryHistoryManager.handleItemClicked(allHistory[0], [allHistory[0], allHistory[1]]);
expect(selectedCallback).not.to.have.been.called;
expect(queryHistoryManager.treeDataProvider.getCurrent()).to.be.undefined;
});
it('should throw if there is no selection', async () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
try {
await queryHistoryManager.handleItemClicked(undefined!, []);
expect(true).to.be.false;
} catch (e) {
expect(selectedCallback).not.to.have.been.called;
expect(e.message).to.contain('No query selected');
}
});
});
it('should remove an item and not select a new one', async function() {
queryHistoryManager = await createMockQueryHistory(allHistory);
// deleting the first item when a different item is selected
// will not change the selection
const toDelete = allHistory[1];
const selected = allHistory[3];
// avoid triggering the callback by setting the field directly
(queryHistoryManager.treeDataProvider as any).current = selected;
await queryHistoryManager.handleRemoveHistoryItem(toDelete, [toDelete]);
expect(toDelete.completedQuery!.dispose).to.have.been.calledOnce;
expect(queryHistoryManager.treeDataProvider.getCurrent()).to.eq(selected);
expect(allHistory).not.to.contain(toDelete);
// the current item should have been re-selected
expect(selectedCallback).to.have.been.calledOnceWith(selected);
});
it('should remove an item and select a new one', async () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
// deleting the selected item automatically selects next item
const toDelete = allHistory[1];
const newSelected = allHistory[2];
// avoid triggering the callback by setting the field directly
(queryHistoryManager.treeDataProvider as any).current = toDelete;
await queryHistoryManager.handleRemoveHistoryItem(toDelete, [toDelete]);
expect(toDelete.completedQuery!.dispose).to.have.been.calledOnce;
expect(queryHistoryManager.treeDataProvider.getCurrent()).to.eq(newSelected);
expect(allHistory).not.to.contain(toDelete);
// the current item should have been selected
expect(selectedCallback).to.have.been.calledOnceWith(newSelected);
});
describe('Compare callback', () => {
it('should call the compare callback', async () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
await queryHistoryManager.handleCompareWith(allHistory[0], [allHistory[0], allHistory[3]]);
expect(doCompareCallback).to.have.been.calledOnceWith(allHistory[0], allHistory[3]);
});
it('should avoid calling the compare callback when only one item is selected', async () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
await queryHistoryManager.handleCompareWith(allHistory[0], [allHistory[0]]);
expect(doCompareCallback).not.to.have.been.called;
});
});
describe('updateCompareWith', () => {
it('should update compareWithItem when there is a single item', () => {
const queryHistory = createMockQueryHistory([]);
queryHistory.updateCompareWith(['a']);
expect(queryHistory.compareWithItem).to.be.eq('a');
it('should update compareWithItem when there is a single item', async () => {
queryHistoryManager = await createMockQueryHistory([]);
(queryHistoryManager as any).updateCompareWith(['a']);
expect(queryHistoryManager.compareWithItem).to.be.eq('a');
});
it('should delete compareWithItem when there are 0 items', () => {
const queryHistory = createMockQueryHistory([]);
queryHistory.compareWithItem = 'a';
queryHistory.updateCompareWith([]);
expect(queryHistory.compareWithItem).to.be.undefined;
it('should delete compareWithItem when there are 0 items', async () => {
queryHistoryManager = await createMockQueryHistory([]);
queryHistoryManager.compareWithItem = allHistory[0];
(queryHistoryManager as any).updateCompareWith([]);
expect(queryHistoryManager.compareWithItem).to.be.undefined;
});
it('should delete compareWithItem when there are more than 2 items', () => {
const queryHistory = createMockQueryHistory([]);
queryHistory.compareWithItem = 'a';
queryHistory.updateCompareWith(['a', 'b', 'c']);
expect(queryHistory.compareWithItem).to.be.undefined;
it('should delete compareWithItem when there are more than 2 items', async () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
queryHistoryManager.compareWithItem = allHistory[0];
(queryHistoryManager as any).updateCompareWith([allHistory[0], allHistory[1], allHistory[2]]);
expect(queryHistoryManager.compareWithItem).to.be.undefined;
});
it('should delete compareWithItem when there are 2 items and disjoint from compareWithItem', () => {
const queryHistory = createMockQueryHistory([]);
queryHistory.compareWithItem = 'a';
queryHistory.updateCompareWith(['b', 'c']);
expect(queryHistory.compareWithItem).to.be.undefined;
it('should delete compareWithItem when there are 2 items and disjoint from compareWithItem', async () => {
queryHistoryManager = await createMockQueryHistory([]);
queryHistoryManager.compareWithItem = allHistory[0];
(queryHistoryManager as any).updateCompareWith([allHistory[1], allHistory[2]]);
expect(queryHistoryManager.compareWithItem).to.be.undefined;
});
it('should do nothing when compareWithItem exists and exactly 2 items', () => {
const queryHistory = createMockQueryHistory([]);
queryHistory.compareWithItem = 'a';
queryHistory.updateCompareWith(['a', 'b']);
expect(queryHistory.compareWithItem).to.be.eq('a');
it('should do nothing when compareWithItem exists and exactly 2 items', async () => {
queryHistoryManager = await createMockQueryHistory([]);
queryHistoryManager.compareWithItem = allHistory[0];
(queryHistoryManager as any).updateCompareWith([allHistory[0], allHistory[1]]);
expect(queryHistoryManager.compareWithItem).to.be.eq(allHistory[0]);
});
});
@ -212,70 +303,107 @@ describe('query-history', () => {
historyTreeDataProvider = new HistoryTreeDataProvider(vscode.Uri.file('/a/b/c').fsPath);
});
afterEach(() => {
historyTreeDataProvider.dispose();
});
it('should get a tree item with raw results', async () => {
const mockQuery = {
query: {
hasInterpretedResults: () => Promise.resolve(false)
} as QueryInfo,
didRunSuccessfully: true,
toString: () => 'mock label'
} as CompletedQuery;
const mockQuery = createMockFullQueryInfo('a', createMockQueryWithResults(true, /* raw results */ false));
const treeItem = await historyTreeDataProvider.getTreeItem(mockQuery);
expect(treeItem.command).to.deep.eq({
title: 'Query History Item',
command: 'codeQLQueryHistory.itemClicked',
arguments: [mockQuery],
});
expect(treeItem.label).to.eq('mock label');
expect(treeItem.label).to.contain('hucairz');
expect(treeItem.contextValue).to.eq('rawResultsItem');
expect(treeItem.iconPath).to.be.undefined;
expect(treeItem.iconPath).to.deep.eq({
id: 'check', color: undefined
});
});
it('should get a tree item with interpreted results', async () => {
const mockQuery = {
query: {
// as above, except for this line
hasInterpretedResults: () => Promise.resolve(true)
} as QueryInfo,
didRunSuccessfully: true,
toString: () => 'mock label'
} as CompletedQuery;
const mockQuery = createMockFullQueryInfo('a', createMockQueryWithResults(true, /* interpreted results */ true));
const treeItem = await historyTreeDataProvider.getTreeItem(mockQuery);
expect(treeItem.contextValue).to.eq('interpretedResultsItem');
expect(treeItem.iconPath).to.deep.eq({
id: 'check', color: undefined
});
});
it('should get a tree item that did not complete successfully', async () => {
const mockQuery = {
query: {
hasInterpretedResults: () => Promise.resolve(true)
} as QueryInfo,
// as above, except for this line
didRunSuccessfully: false,
toString: () => 'mock label'
} as CompletedQuery;
const mockQuery = createMockFullQueryInfo('a', createMockQueryWithResults(false), false);
const treeItem = await historyTreeDataProvider.getTreeItem(mockQuery);
expect(treeItem.iconPath).to.eq(vscode.Uri.file('/a/b/c/media/red-x.svg').fsPath);
});
it('should get a tree item that failed before creating any results', async () => {
const mockQuery = createMockFullQueryInfo('a', undefined, true);
const treeItem = await historyTreeDataProvider.getTreeItem(mockQuery);
expect(treeItem.iconPath).to.eq(vscode.Uri.file('/a/b/c/media/red-x.svg').fsPath);
});
it('should get a tree item that is in progress', async () => {
const mockQuery = createMockFullQueryInfo('a');
const treeItem = await historyTreeDataProvider.getTreeItem(mockQuery);
expect(treeItem.iconPath).to.deep.eq({
id: 'search-refresh', color: undefined
});
});
it('should get children', () => {
const mockQuery = {
databaseName: 'abc'
} as CompletedQuery;
const mockQuery = createMockFullQueryInfo();
historyTreeDataProvider.allHistory.push(mockQuery);
expect(historyTreeDataProvider.getChildren()).to.deep.eq([mockQuery]);
expect(historyTreeDataProvider.getChildren(mockQuery)).to.deep.eq([]);
});
});
});
function createMockQueryHistory(allHistory: Record<string, unknown>[]) {
return {
assertSingleQuery: (QueryHistoryManager.prototype as any).assertSingleQuery,
findOtherQueryToCompare: (QueryHistoryManager.prototype as any).findOtherQueryToCompare,
treeDataProvider: {
allHistory
},
updateCompareWith: (QueryHistoryManager.prototype as any).updateCompareWith,
compareWithItem: undefined as undefined | string,
};
}
function createMockFullQueryInfo(dbName = 'a', queryWitbResults?: QueryWithResults, isFail = false): FullQueryInfo {
const fqi = new FullQueryInfo(
{
databaseInfo: { name: dbName },
start: new Date(),
queryPath: 'hucairz'
} as InitialQueryInfo,
configListener
);
if (queryWitbResults) {
fqi.completeThisQuery(queryWitbResults);
}
if (isFail) {
fqi.failureReason = 'failure reason';
}
return fqi;
}
function createMockQueryWithResults(didRunSuccessfully = true, hasInterpretedResults = true): QueryWithResults {
return {
query: {
hasInterpretedResults: () => Promise.resolve(hasInterpretedResults)
} as QueryEvaluatonInfo,
result: {
resultType: didRunSuccessfully
? messages.QueryResultType.SUCCESS
: messages.QueryResultType.OTHER_ERROR
} as messages.EvaluationResult,
dispose: sandbox.spy(),
};
}
async function createMockQueryHistory(allHistory: FullQueryInfo[]) {
const qhm = new QueryHistoryManager(
{} as QueryServerClient,
'xxx',
configListener,
selectedCallback,
doCompareCallback
);
(qhm.treeDataProvider as any).history = allHistory;
await vscode.workspace.saveAll();
return qhm;
}
});

Просмотреть файл

@ -3,162 +3,186 @@ import * as path from 'path';
import * as fs from 'fs-extra';
import 'mocha';
import 'sinon-chai';
import * as Sinon from 'sinon';
import * as sinon from 'sinon';
import * as chaiAsPromised from 'chai-as-promised';
import { CompletedQuery, interpretResults } from '../../query-results';
import { QueryInfo, QueryWithResults, tmpDir } from '../../run-queries';
import { FullQueryInfo, InitialQueryInfo, interpretResults } from '../../query-results';
import { QueryEvaluatonInfo, QueryWithResults, tmpDir } from '../../run-queries';
import { QueryHistoryConfig } from '../../config';
import { EvaluationResult, QueryResultType } from '../../pure/messages';
import { SortDirection, SortedResultSetInfo } from '../../pure/interface-types';
import { CodeQLCliServer, SourceInfo } from '../../cli';
import { env } from 'process';
chai.use(chaiAsPromised);
const expect = chai.expect;
describe('CompletedQuery', () => {
let disposeSpy: Sinon.SinonSpy;
let onDidChangeQueryHistoryConfigurationSpy: Sinon.SinonSpy;
describe('query-results', () => {
let disposeSpy: sinon.SinonSpy;
let onDidChangeQueryHistoryConfigurationSpy: sinon.SinonSpy;
let sandbox: sinon.SinonSandbox;
beforeEach(() => {
disposeSpy = Sinon.spy();
onDidChangeQueryHistoryConfigurationSpy = Sinon.spy();
sandbox = sinon.createSandbox();
disposeSpy = sandbox.spy();
onDidChangeQueryHistoryConfigurationSpy = sandbox.spy();
});
it('should construct a CompletedQuery', () => {
const completedQuery = mockCompletedQuery();
expect(completedQuery.logFileLocation).to.eq('mno');
expect(completedQuery.databaseName).to.eq('def');
afterEach(() => {
sandbox.restore();
});
it('should get the query name', () => {
const completedQuery = mockCompletedQuery();
// from the query path
expect(completedQuery.queryName).to.eq('stu');
// from the metadata
(completedQuery.query as any).metadata = {
name: 'vwx'
};
expect(completedQuery.queryName).to.eq('vwx');
// from quick eval position
(completedQuery.query as any).quickEvalPosition = {
line: 1,
endLine: 2,
fileName: '/home/users/yz'
};
expect(completedQuery.queryName).to.eq('Quick evaluation of yz:1-2');
(completedQuery.query as any).quickEvalPosition.endLine = 1;
expect(completedQuery.queryName).to.eq('Quick evaluation of yz:1');
});
it('should get the query file name', () => {
const completedQuery = mockCompletedQuery();
// from the query path
expect(completedQuery.queryFileName).to.eq('stu');
// from quick eval position
(completedQuery.query as any).quickEvalPosition = {
line: 1,
endLine: 2,
fileName: '/home/users/yz'
};
expect(completedQuery.queryFileName).to.eq('yz:1-2');
(completedQuery.query as any).quickEvalPosition.endLine = 1;
expect(completedQuery.queryFileName).to.eq('yz:1');
});
it('should get the label', () => {
const completedQuery = mockCompletedQuery();
expect(completedQuery.getLabel()).to.eq('ghi');
completedQuery.options.label = '';
expect(completedQuery.getLabel()).to.eq('pqr');
});
it('should get the getResultsPath', () => {
const completedQuery = mockCompletedQuery();
// from results path
expect(completedQuery.getResultsPath('zxa', false)).to.eq('axa');
completedQuery.sortedResultsInfo.set('zxa', {
resultsPath: 'bxa'
} as SortedResultSetInfo);
// still from results path
expect(completedQuery.getResultsPath('zxa', false)).to.eq('axa');
// from sortedResultsInfo
expect(completedQuery.getResultsPath('zxa')).to.eq('bxa');
});
it('should get the statusString', () => {
const completedQuery = mockCompletedQuery();
expect(completedQuery.statusString).to.eq('failed');
completedQuery.result.message = 'Tremendously';
expect(completedQuery.statusString).to.eq('failed: Tremendously');
completedQuery.result.resultType = QueryResultType.OTHER_ERROR;
expect(completedQuery.statusString).to.eq('failed: Tremendously');
completedQuery.result.resultType = QueryResultType.CANCELLATION;
completedQuery.result.evaluationTime = 2000;
expect(completedQuery.statusString).to.eq('cancelled after 2 seconds');
completedQuery.result.resultType = QueryResultType.OOM;
expect(completedQuery.statusString).to.eq('out of memory');
completedQuery.result.resultType = QueryResultType.SUCCESS;
expect(completedQuery.statusString).to.eq('finished in 2 seconds');
completedQuery.result.resultType = QueryResultType.TIMEOUT;
expect(completedQuery.statusString).to.eq('timed out after 2 seconds');
});
it('should updateSortState', async () => {
const completedQuery = mockCompletedQuery();
const spy = Sinon.spy();
const mockServer = {
sortBqrs: spy
} as unknown as CodeQLCliServer;
const sortState = {
columnIndex: 1,
sortDirection: SortDirection.desc
};
await completedQuery.updateSortState(mockServer, 'result-name', sortState);
const expectedPath = path.join(tmpDir.name, 'sortedResults111-result-name.bqrs');
expect(spy).to.have.been.calledWith(
'axa',
expectedPath,
'result-name',
[sortState.columnIndex],
[sortState.sortDirection],
);
expect(completedQuery.sortedResultsInfo.get('result-name')).to.deep.equal({
resultsPath: expectedPath,
sortState
describe('FullQueryInfo', () => {
it('should interpolate', () => {
const fqi = createMockFullQueryInfo();
const date = new Date('2022-01-01T00:00:00.000Z');
const dateStr = date.toLocaleString(env.language);
(fqi.initialInfo as any).start = date;
expect(fqi.interpolate('xxx')).to.eq('xxx');
expect(fqi.interpolate('%t %q %d %s %%')).to.eq(`${dateStr} hucairz a in progress %`);
expect(fqi.interpolate('%t %q %d %s %%::%t %q %d %s %%')).to.eq(`${dateStr} hucairz a in progress %::${dateStr} hucairz a in progress %`);
});
// delete the sort stae
await completedQuery.updateSortState(mockServer, 'result-name');
expect(completedQuery.sortedResultsInfo.size).to.eq(0);
});
it('should get the query name', () => {
const fqi = createMockFullQueryInfo();
it('should interpolate', () => {
const completedQuery = mockCompletedQuery();
(completedQuery as any).time = '123';
expect(completedQuery.interpolate('xxx')).to.eq('xxx');
expect(completedQuery.interpolate('%t %q %d %s %%')).to.eq('123 stu def failed %');
expect(completedQuery.interpolate('%t %q %d %s %%::%t %q %d %s %%')).to.eq('123 stu def failed %::123 stu def failed %');
// from the query path
expect(fqi.getQueryName()).to.eq('hucairz');
fqi.completeThisQuery(createMockQueryWithResults());
// from the metadata
expect(fqi.getQueryName()).to.eq('vwx');
// from quick eval position
(fqi.initialInfo as any).quickEvalPosition = {
line: 1,
endLine: 2,
fileName: '/home/users/yz'
};
expect(fqi.getQueryName()).to.eq('Quick evaluation of yz:1-2');
(fqi.initialInfo as any).quickEvalPosition.endLine = 1;
expect(fqi.getQueryName()).to.eq('Quick evaluation of yz:1');
});
it('should get the query file name', () => {
const fqi = createMockFullQueryInfo();
// from the query path
expect(fqi.getQueryFileName()).to.eq('hucairz');
// from quick eval position
(fqi.initialInfo as any).quickEvalPosition = {
line: 1,
endLine: 2,
fileName: '/home/users/yz'
};
expect(fqi.getQueryFileName()).to.eq('yz:1-2');
(fqi.initialInfo as any).quickEvalPosition.endLine = 1;
expect(fqi.getQueryFileName()).to.eq('yz:1');
});
it('should get the label', () => {
const fqi = createMockFullQueryInfo('db-name');
// the %q from the config is now replaced by the file name of the query
expect(fqi.label).to.eq('from config hucairz');
// the %q from the config is now replaced by the name of the query
// in the metadata
fqi.completeThisQuery(createMockQueryWithResults());
expect(fqi.label).to.eq('from config vwx');
// replace the config with a user specified label
// must be interpolated
fqi.initialInfo.userSpecifiedLabel = 'user specified label %d';
expect(fqi.label).to.eq('user specified label db-name');
});
it('should get the getResultsPath', () => {
const fqi = createMockFullQueryInfo('a', createMockQueryWithResults());
const completedQuery = fqi.completedQuery!;
// from results path
expect(completedQuery.getResultsPath('zxa', false)).to.eq('/a/b/c');
completedQuery.sortedResultsInfo.set('zxa', {
resultsPath: 'bxa'
} as SortedResultSetInfo);
// still from results path
expect(completedQuery.getResultsPath('zxa', false)).to.eq('/a/b/c');
// from sortedResultsInfo
expect(completedQuery.getResultsPath('zxa')).to.eq('bxa');
});
it('should get the statusString', () => {
const fqi = createMockFullQueryInfo('a', createMockQueryWithResults(false));
const completedQuery = fqi.completedQuery!;
completedQuery.result.message = 'Tremendously';
expect(completedQuery.statusString).to.eq('failed: Tremendously');
completedQuery.result.resultType = QueryResultType.OTHER_ERROR;
expect(completedQuery.statusString).to.eq('failed: Tremendously');
completedQuery.result.resultType = QueryResultType.CANCELLATION;
completedQuery.result.evaluationTime = 2345;
expect(completedQuery.statusString).to.eq('cancelled after 2 seconds');
completedQuery.result.resultType = QueryResultType.OOM;
expect(completedQuery.statusString).to.eq('out of memory');
completedQuery.result.resultType = QueryResultType.SUCCESS;
expect(completedQuery.statusString).to.eq('finished in 2 seconds');
completedQuery.result.resultType = QueryResultType.TIMEOUT;
expect(completedQuery.statusString).to.eq('timed out after 2 seconds');
});
it('should updateSortState', async () => {
const fqi = createMockFullQueryInfo('a', createMockQueryWithResults());
const completedQuery = fqi.completedQuery!;
const spy = sandbox.spy();
const mockServer = {
sortBqrs: spy
} as unknown as CodeQLCliServer;
const sortState = {
columnIndex: 1,
sortDirection: SortDirection.desc
};
await completedQuery.updateSortState(mockServer, 'result-name', sortState);
const expectedPath = path.join(tmpDir.name, 'sortedResults6789-result-name.bqrs');
expect(spy).to.have.been.calledWith(
'/a/b/c',
expectedPath,
'result-name',
[sortState.columnIndex],
[sortState.sortDirection],
);
expect(completedQuery.sortedResultsInfo.get('result-name')).to.deep.equal({
resultsPath: expectedPath,
sortState
});
// delete the sort stae
await completedQuery.updateSortState(mockServer, 'result-name');
expect(completedQuery.sortedResultsInfo.size).to.eq(0);
});
// interpolate
// time
// label
// getShortLabel
// getQueryFileName
// getQueryName
// status
});
it('should interpretResults', async () => {
const spy = Sinon.mock();
const spy = sandbox.mock();
spy.returns('1234');
const mockServer = {
interpretBqrs: spy
@ -221,43 +245,52 @@ describe('CompletedQuery', () => {
expect(results3).to.deep.eq({ a: 6 });
});
function mockCompletedQuery() {
return new CompletedQuery(
mockQueryWithResults(),
mockQueryHistoryConfig()
);
}
function mockQueryWithResults(): QueryWithResults {
function createMockQueryWithResults(didRunSuccessfully = true, hasInterpretedResults = true): QueryWithResults {
return {
query: {
program: {
queryPath: 'stu'
hasInterpretedResults: () => Promise.resolve(hasInterpretedResults),
queryID: 6789,
metadata: {
name: 'vwx'
},
resultsPaths: {
resultsPath: 'axa'
},
queryID: 111
} as never as QueryInfo,
result: {} as never as EvaluationResult,
database: {
databaseUri: 'abc',
name: 'def'
},
options: {
label: 'ghi',
queryText: 'jkl',
isQuickQuery: false
},
logFileLocation: 'mno',
dispose: disposeSpy
resultsPath: '/a/b/c',
interpretedResultsPath: '/d/e/f'
}
} as QueryEvaluatonInfo,
result: {
evaluationTime: 12340,
resultType: didRunSuccessfully
? QueryResultType.SUCCESS
: QueryResultType.OTHER_ERROR
} as EvaluationResult,
dispose: disposeSpy,
};
}
function createMockFullQueryInfo(dbName = 'a', queryWitbResults?: QueryWithResults, isFail = false): FullQueryInfo {
const fqi = new FullQueryInfo(
{
databaseInfo: { name: dbName },
start: new Date(),
queryPath: 'path/to/hucairz'
} as InitialQueryInfo,
mockQueryHistoryConfig()
);
if (queryWitbResults) {
fqi.completeThisQuery(queryWitbResults);
}
if (isFail) {
fqi.failureReason = 'failure reason';
}
return fqi;
}
function mockQueryHistoryConfig(): QueryHistoryConfig {
return {
onDidChangeConfiguration: onDidChangeQueryHistoryConfigurationSpy,
format: 'pqr'
format: 'from config %q'
};
}
});

Просмотреть файл

@ -5,7 +5,7 @@ import 'sinon-chai';
import * as sinon from 'sinon';
import * as chaiAsPromised from 'chai-as-promised';
import { QueryInfo } from '../../run-queries';
import { QueryEvaluatonInfo } from '../../run-queries';
import { QlProgram, Severity, compileQuery } from '../../pure/messages';
import { DatabaseItem } from '../../databases';
@ -13,7 +13,7 @@ chai.use(chaiAsPromised);
const expect = chai.expect;
describe('run-queries', () => {
it('should create a QueryInfo', () => {
it('should create a QueryEvaluatonInfo', () => {
const info = createMockQueryInfo();
const queryID = info.queryID;
@ -85,7 +85,7 @@ describe('run-queries', () => {
});
function createMockQueryInfo() {
return new QueryInfo(
return new QueryEvaluatonInfo(
'my-program' as unknown as QlProgram,
{
contents: {