Merge pull request #1290 from github/aeisenberg/remote-history-label-editing

Allow remote query items to have their labels edited
This commit is contained in:
Andrew Eisenberg
2022-04-14 12:40:50 -07:00
committed by GitHub
14 changed files with 307 additions and 150 deletions

View File

@@ -224,7 +224,7 @@
},
"codeQL.queryHistory.format": {
"type": "string",
"default": "%q on %d - %s, %r result count [%t]",
"default": "%q on %d - %s, %r [%t]",
"markdownDescription": "Default string for how to label query history items.\n* %t is the time of the query\n* %q is the human-readable query name\n* %f is the query file name\n* %d is the database name\n* %r is the number of results\n* %s is a status string"
},
"codeQL.queryHistory.ttl": {

View File

@@ -22,6 +22,7 @@ import { transformBqrsResultSet, RawResultSet, BQRSInfo } from '../pure/bqrs-cli
import resultsDiff from './resultsDiff';
import { CompletedLocalQueryInfo } from '../query-results';
import { getErrorMessage } from '../pure/helpers-pure';
import { HistoryItemLabelProvider } from '../history-item-label-provider';
interface ComparePair {
from: CompletedLocalQueryInfo;
@@ -39,6 +40,7 @@ export class CompareInterfaceManager extends DisposableObject {
private databaseManager: DatabaseManager,
private cliServer: CodeQLCliServer,
private logger: Logger,
private labelProvider: HistoryItemLabelProvider,
private showQueryResultsCallback: (
item: CompletedLocalQueryInfo
) => Promise<void>
@@ -81,12 +83,12 @@ 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.getShortLabel(),
name: this.labelProvider.getShortLabel(from),
status: from.completedQuery.statusString,
time: from.startTime,
},
toQuery: {
name: to.getShortLabel(),
name: this.labelProvider.getShortLabel(to),
status: to.completedQuery.statusString,
time: to.startTime,
},

View File

@@ -96,6 +96,7 @@ import { RemoteQueryResult } from './remote-queries/remote-query-result';
import { URLSearchParams } from 'url';
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
import { HistoryItemLabelProvider } from './history-item-label-provider';
/**
* extension.ts
@@ -447,6 +448,7 @@ async function activateWithInstalledDistribution(
showResultsForCompletedQuery(item, WebviewReveal.Forced);
const queryStorageDir = path.join(ctx.globalStorageUri.fsPath, 'queries');
await fs.ensureDir(queryStorageDir);
const labelProvider = new HistoryItemLabelProvider(queryHistoryConfigurationListener);
void logger.log('Initializing query history.');
const qhm = new QueryHistoryManager(
@@ -455,6 +457,7 @@ async function activateWithInstalledDistribution(
queryStorageDir,
ctx,
queryHistoryConfigurationListener,
labelProvider,
async (from: CompletedLocalQueryInfo, to: CompletedLocalQueryInfo) =>
showResultsForComparison(from, to),
);
@@ -466,8 +469,9 @@ async function activateWithInstalledDistribution(
});
ctx.subscriptions.push(qhm);
void logger.log('Initializing results panel interface.');
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger, labelProvider);
ctx.subscriptions.push(intm);
void logger.log('Initializing compare panel interface.');
@@ -476,6 +480,7 @@ async function activateWithInstalledDistribution(
dbm,
cliServer,
queryServerLogger,
labelProvider,
showResults
);
ctx.subscriptions.push(cmpm);
@@ -525,7 +530,7 @@ async function activateWithInstalledDistribution(
token.onCancellationRequested(() => source.cancel());
const initialInfo = await createInitialQueryInfo(selectedQuery, databaseInfo, quickEval, range);
const item = new LocalQueryInfo(initialInfo, queryHistoryConfigurationListener, source);
const item = new LocalQueryInfo(initialInfo, source);
qhm.addQuery(item);
try {
const completedQueryInfo = await compileAndRunQueryAgainstDatabase(

View File

@@ -0,0 +1,82 @@
import { env } from 'vscode';
import * as path from 'path';
import { QueryHistoryConfig } from './config';
import { LocalQueryInfo, QueryHistoryInfo } from './query-results';
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
interface InterpolateReplacements {
t: string; // Start time
q: string; // Query name
d: string; // Database/Controller repo name
r: string; // Result count/Empty
s: string; // Status
f: string; // Query file name
'%': '%'; // Percent sign
}
export class HistoryItemLabelProvider {
constructor(private config: QueryHistoryConfig) {
/**/
}
getLabel(item: QueryHistoryInfo) {
const replacements = item.t === 'local'
? this.getLocalInterpolateReplacements(item)
: this.getRemoteInterpolateReplacements(item);
const rawLabel = item.userSpecifiedLabel ?? (this.config.format || '%q');
return this.interpolate(rawLabel, replacements);
}
/**
* If there is a user-specified label for this query, interpolate and use that.
* Otherwise, use the raw name of this query.
*
* @returns the name of the query, unless there is a custom label for this query.
*/
getShortLabel(item: QueryHistoryInfo): string {
return item.userSpecifiedLabel
? this.getLabel(item)
: item.t === 'local'
? item.getQueryName()
: item.remoteQuery.queryName;
}
private interpolate(rawLabel: string, replacements: InterpolateReplacements): string {
return rawLabel.replace(/%(.)/g, (match, key: keyof InterpolateReplacements) => {
const replacement = replacements[key];
return replacement !== undefined ? replacement : match;
});
}
private getLocalInterpolateReplacements(item: LocalQueryInfo): InterpolateReplacements {
const { resultCount = 0, statusString = 'in progress' } = item.completedQuery || {};
return {
t: item.startTime,
q: item.getQueryName(),
d: item.initialInfo.databaseInfo.name,
r: `${resultCount} results`,
s: statusString,
f: item.getQueryFileName(),
'%': '%',
};
}
private getRemoteInterpolateReplacements(item: RemoteQueryHistoryItem): InterpolateReplacements {
return {
t: new Date(item.remoteQuery.executionStartTime).toLocaleString(env.language),
q: item.remoteQuery.queryName,
// There is no database name for remote queries. Instead use the controller repository name.
d: `${item.remoteQuery.controllerRepository.owner}/${item.remoteQuery.controllerRepository.name}`,
// There is no synchronous way to get the results count.
r: '',
s: item.status,
f: path.basename(item.remoteQuery.queryFilePath),
'%': '%'
};
}
}

View File

@@ -49,6 +49,7 @@ import { getDefaultResultSetName, ParsedResultSets } from './pure/interface-type
import { RawResultSet, transformBqrsResultSet, ResultSetSchema } from './pure/bqrs-cli-types';
import { PAGE_SIZE } from './config';
import { CompletedLocalQueryInfo } from './query-results';
import { HistoryItemLabelProvider } from './history-item-label-provider';
/**
* interface.ts
@@ -136,7 +137,8 @@ export class InterfaceManager extends DisposableObject {
public ctx: vscode.ExtensionContext,
private databaseManager: DatabaseManager,
public cliServer: CodeQLCliServer,
public logger: Logger
public logger: Logger,
private labelProvider: HistoryItemLabelProvider
) {
super();
this.push(this._diagnosticCollection);
@@ -416,7 +418,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 = fullQuery.getShortLabel();
const queryName = this.labelProvider.getShortLabel(fullQuery);
const resultPromise = vscode.window.showInformationMessage(
`Finished running query ${queryName.length > 0 ? ` "${queryName}"` : ''
}.`,
@@ -483,7 +485,7 @@ export class InterfaceManager extends DisposableObject {
database: fullQuery.initialInfo.databaseInfo,
shouldKeepOldResultsWhileRendering,
metadata: fullQuery.completedQuery.query.metadata,
queryName: fullQuery.label,
queryName: this.labelProvider.getLabel(fullQuery),
queryPath: fullQuery.initialInfo.queryPath
});
}
@@ -516,7 +518,7 @@ export class InterfaceManager extends DisposableObject {
resultSetNames,
pageSize: interpretedPageSize(this._interpretation),
numPages: numInterpretedPages(this._interpretation),
queryName: this._displayedQuery.label,
queryName: this.labelProvider.getLabel(this._displayedQuery),
queryPath: this._displayedQuery.initialInfo.queryPath
});
}
@@ -601,7 +603,7 @@ export class InterfaceManager extends DisposableObject {
database: results.initialInfo.databaseInfo,
shouldKeepOldResultsWhileRendering: false,
metadata: results.completedQuery.query.metadata,
queryName: results.label,
queryName: this.labelProvider.getLabel(results),
queryPath: results.initialInfo.queryPath
});
}

View File

@@ -36,6 +36,7 @@ import { QueryStatus } from './query-status';
import { slurpQueryHistory, splatQueryHistory } from './query-serialization';
import * as fs from 'fs-extra';
import { CliVersionConstraint } from './cli';
import { HistoryItemLabelProvider } from './history-item-label-provider';
import { Credentials } from './authentication';
import { cancelRemoteQuery } from './remote-queries/gh-actions-api-client';
@@ -123,7 +124,10 @@ export class HistoryTreeDataProvider extends DisposableObject {
private current: QueryHistoryInfo | undefined;
constructor(extensionPath: string) {
constructor(
extensionPath: string,
private readonly labelProvider: HistoryItemLabelProvider,
) {
super();
this.failedIconPath = path.join(
extensionPath,
@@ -140,13 +144,13 @@ export class HistoryTreeDataProvider extends DisposableObject {
}
async getTreeItem(element: QueryHistoryInfo): Promise<TreeItem> {
const treeItem = new TreeItem(element.label);
const treeItem = new TreeItem(this.labelProvider.getLabel(element));
treeItem.command = {
title: 'Query History Item',
command: 'codeQLQueryHistory.itemClicked',
arguments: [element],
tooltip: element.failureReason || element.label
tooltip: element.failureReason || this.labelProvider.getLabel(element)
};
// Populate the icon and the context value. We use the context value to
@@ -185,8 +189,8 @@ export class HistoryTreeDataProvider extends DisposableObject {
): ProviderResult<QueryHistoryInfo[]> {
return element ? [] : this.history.sort((h1, h2) => {
const h1Label = h1.label.toLowerCase();
const h2Label = h2.label.toLowerCase();
const h1Label = this.labelProvider.getLabel(h1).toLowerCase();
const h2Label = this.labelProvider.getLabel(h2).toLowerCase();
const h1Date = h1.t === 'local'
? h1.initialInfo.start.getTime()
@@ -315,12 +319,13 @@ export class QueryHistoryManager extends DisposableObject {
._onWillOpenQueryItem.event;
constructor(
private qs: QueryServerClient,
private dbm: DatabaseManager,
private queryStorageDir: string,
private ctx: ExtensionContext,
private queryHistoryConfigListener: QueryHistoryConfig,
private doCompareCallback: (
private readonly qs: QueryServerClient,
private readonly dbm: DatabaseManager,
private readonly queryStorageDir: string,
private readonly ctx: ExtensionContext,
private readonly queryHistoryConfigListener: QueryHistoryConfig,
private readonly labelProvider: HistoryItemLabelProvider,
private readonly doCompareCallback: (
from: CompletedLocalQueryInfo,
to: CompletedLocalQueryInfo
) => Promise<void>
@@ -334,7 +339,8 @@ export class QueryHistoryManager extends DisposableObject {
this.queryMetadataStorageLocation = path.join((ctx.storageUri || ctx.globalStorageUri).fsPath, WORKSPACE_QUERY_HISTORY_FILE);
this.treeDataProvider = this.push(new HistoryTreeDataProvider(
ctx.extensionPath
ctx.extensionPath,
this.labelProvider
));
this.treeView = this.push(window.createTreeView('codeQLQueryHistory', {
treeDataProvider: this.treeDataProvider,
@@ -537,7 +543,7 @@ export class QueryHistoryManager extends DisposableObject {
async readQueryHistory(): Promise<void> {
void logger.log(`Reading cached query history from '${this.queryMetadataStorageLocation}'.`);
const history = await slurpQueryHistory(this.queryMetadataStorageLocation, this.queryHistoryConfigListener);
const history = await slurpQueryHistory(this.queryMetadataStorageLocation);
this.treeDataProvider.allHistory = history;
this.treeDataProvider.allHistory.forEach((item) => {
this._onDidAddQueryItem.fire(item);
@@ -605,7 +611,7 @@ export class QueryHistoryManager extends DisposableObject {
// Remote queries can be removed locally, but not remotely.
// The user must cancel the query on GitHub Actions explicitly.
this.treeDataProvider.remove(item);
void logger.log(`Deleted ${item.label}.`);
void logger.log(`Deleted ${this.labelProvider.getLabel(item)}.`);
if (item.status === QueryStatus.InProgress) {
void logger.log('The variant analysis is still running on GitHub Actions. To cancel there, you must go to the workflow run in your browser.');
}
@@ -652,20 +658,20 @@ export class QueryHistoryManager extends DisposableObject {
): Promise<void> {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
// TODO will support remote queries
if (!this.assertSingleQuery(finalMultiSelect) || finalSingleItem?.t !== 'local') {
if (!this.assertSingleQuery(finalMultiSelect)) {
return;
}
const response = await window.showInputBox({
prompt: 'Label:',
placeHolder: '(use default)',
value: finalSingleItem.label,
placeHolder: `(use default: ${this.queryHistoryConfigListener.format})`,
value: finalSingleItem.userSpecifiedLabel ?? '',
title: 'Set query label',
prompt: 'Set the query history item label. See the description of the codeQL.queryHistory.format setting for more information.',
});
// 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'
finalSingleItem.initialInfo.userSpecifiedLabel = response === '' ? undefined : response;
finalSingleItem.userSpecifiedLabel = response === '' ? undefined : response;
await this.refreshTreeView();
}
}
@@ -895,7 +901,7 @@ export class QueryHistoryManager extends DisposableObject {
query.resultsPaths.interpretedResultsPath
);
} else {
const label = finalSingleItem.label;
const label = this.labelProvider.getLabel(finalSingleItem);
void showAndLogInformationMessage(
`Query ${label} has no interpreted results.`
);
@@ -1084,7 +1090,7 @@ the file in the file explorer and dragging it into the workspace.`
otherQuery.initialInfo.databaseInfo.name === dbName
)
.map((item) => ({
label: item.label,
label: this.labelProvider.getLabel(item),
description: (item as CompletedLocalQueryInfo).initialInfo.databaseInfo.name,
detail: (item as CompletedLocalQueryInfo).completedQuery.statusString,
query: item as CompletedLocalQueryInfo,

View File

@@ -14,7 +14,6 @@ import {
SarifInterpretationData,
GraphInterpretationData
} from './pure/interface-types';
import { QueryHistoryConfig } from './config';
import { DatabaseInfo } from './pure/interface-types';
import { QueryStatus } from './query-status';
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
@@ -218,7 +217,6 @@ export class LocalQueryInfo {
public completedQuery: CompletedQueryInfo | undefined;
public evalLogLocation: string | undefined;
public evalLogSummaryLocation: string | undefined;
private config: QueryHistoryConfig | undefined;
/**
* Note that in the {@link slurpQueryHistory} method, we create a FullQueryInfo instance
@@ -226,11 +224,8 @@ export class LocalQueryInfo {
*/
constructor(
public readonly initialInfo: InitialQueryInfo,
config: QueryHistoryConfig,
private cancellationSource?: CancellationTokenSource // used to cancel in progress queries
) {
this.setConfig(config);
}
) { /**/ }
cancel() {
this.cancellationSource?.cancel();
@@ -243,43 +238,12 @@ export class LocalQueryInfo {
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.startTime,
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;
});
get userSpecifiedLabel() {
return this.initialInfo.userSpecifiedLabel;
}
/**
* 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();
set userSpecifiedLabel(label: string | undefined) {
this.initialInfo.userSpecifiedLabel = label;
}
/**
@@ -342,21 +306,4 @@ export class LocalQueryInfo {
return QueryStatus.Failed;
}
}
/**
* The `config` property must not be serialized since it contains a listerner
* for global configuration changes. Instead, It should be set when the query
* is deserialized.
*
* @param config the global query history config object
*/
setConfig(config: QueryHistoryConfig) {
// avoid serializing config property
Object.defineProperty(this, 'config', {
enumerable: false,
writable: false,
configurable: true,
value: config
});
}
}

View File

@@ -1,13 +1,12 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { QueryHistoryConfig } from './config';
import { showAndLogErrorMessage } from './helpers';
import { asyncFilter, getErrorMessage, getErrorStack } from './pure/helpers-pure';
import { CompletedQueryInfo, LocalQueryInfo, QueryHistoryInfo } from './query-results';
import { QueryEvaluationInfo } from './run-queries';
export async function slurpQueryHistory(fsPath: string, config: QueryHistoryConfig): Promise<QueryHistoryInfo[]> {
export async function slurpQueryHistory(fsPath: string): Promise<QueryHistoryInfo[]> {
try {
if (!(await fs.pathExists(fsPath))) {
return [];
@@ -29,10 +28,6 @@ export async function slurpQueryHistory(fsPath: string, config: QueryHistoryConf
if (q.t === 'local') {
Object.setPrototypeOf(q, LocalQueryInfo.prototype);
// The config object is a global, se we need to set it explicitly
// and ensure it is not serialized to JSON.
q.setConfig(config);
// Date instances are serialized as strings. Need to
// convert them back to Date instances.
(q.initialInfo as any).start = new Date(q.initialInfo.start);

View File

@@ -110,7 +110,6 @@ export class RemoteQueriesManager extends DisposableObject {
status: QueryStatus.InProgress,
completed: false,
queryId,
label: query.queryName,
remoteQuery: query,
};
await this.prepareStorageDirectory(queryHistoryItem);
@@ -151,7 +150,7 @@ export class RemoteQueriesManager extends DisposableObject {
}
);
} else {
void showAndLogErrorMessage(`There was an issue retrieving the result for the query ${queryItem.label}`);
void showAndLogErrorMessage(`There was an issue retrieving the result for the query ${queryItem.remoteQuery.queryName}`);
queryItem.status = QueryStatus.Failed;
}
} else if (queryWorkflowResult.status === 'CompletedUnsuccessfully') {

View File

@@ -10,6 +10,6 @@ export interface RemoteQueryHistoryItem {
status: QueryStatus;
completed: boolean;
readonly queryId: string,
label: string; // TODO, the query label should have interpolation like local queries
remoteQuery: RemoteQuery;
userSpecifiedLabel?: string;
}

View File

@@ -0,0 +1,139 @@
import { env } from 'vscode';
import { expect } from 'chai';
import { QueryHistoryConfig } from '../../config';
import { HistoryItemLabelProvider } from '../../history-item-label-provider';
import { CompletedLocalQueryInfo, CompletedQueryInfo, InitialQueryInfo } from '../../query-results';
import { RemoteQueryHistoryItem } from '../../remote-queries/remote-query-history-item';
describe('HistoryItemLabelProvider', () => {
let labelProvider: HistoryItemLabelProvider;
let config: QueryHistoryConfig;
const date = new Date('2022-01-01T00:00:00.000Z');
const dateStr = date.toLocaleString(env.language);
beforeEach(() => {
config = {
format: 'xxx %q xxx'
} as unknown as QueryHistoryConfig;
labelProvider = new HistoryItemLabelProvider(config);
});
describe('local queries', () => {
it('should interpolate query when user specified', () => {
const fqi = createMockLocalQueryInfo('xxx');
expect(labelProvider.getLabel(fqi)).to.eq('xxx');
fqi.userSpecifiedLabel = '%t %q %d %s %f %r %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql 456 results %`);
fqi.userSpecifiedLabel = '%t %q %d %s %f %r %%::%t %q %d %s %f %r %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql 456 results %::${dateStr} query-name db-name in progress query-file.ql 456 results %`);
});
it('should interpolate query when not user specified', () => {
const fqi = createMockLocalQueryInfo();
expect(labelProvider.getLabel(fqi)).to.eq('xxx query-name xxx');
config.format = '%t %q %d %s %f %r %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql 456 results %`);
config.format = '%t %q %d %s %f %r %%::%t %q %d %s %f %r %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql 456 results %::${dateStr} query-name db-name in progress query-file.ql 456 results %`);
});
it('should get query short label', () => {
const fqi = createMockLocalQueryInfo('xxx');
// fall back on user specified if one exists.
expect(labelProvider.getShortLabel(fqi)).to.eq('xxx');
// use query name if no user-specified label exists
delete (fqi as any).userSpecifiedLabel;
expect(labelProvider.getShortLabel(fqi)).to.eq('query-name');
});
function createMockLocalQueryInfo(userSpecifiedLabel?: string) {
return {
t: 'local',
userSpecifiedLabel,
startTime: date.toLocaleString(env.language),
getQueryFileName() {
return 'query-file.ql';
},
getQueryName() {
return 'query-name';
},
initialInfo: {
databaseInfo: {
databaseUri: 'unused',
name: 'db-name'
}
} as unknown as InitialQueryInfo,
completedQuery: {
resultCount: 456,
statusString: 'in progress',
} as unknown as CompletedQueryInfo,
} as unknown as CompletedLocalQueryInfo;
}
});
describe('remote queries', () => {
it('should interpolate query when user specified', () => {
const fqi = createMockRemoteQueryInfo('xxx');
expect(labelProvider.getLabel(fqi)).to.eq('xxx');
fqi.userSpecifiedLabel = '%t %q %d %s %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name github/vscode-codeql-integration-tests in progress %`);
fqi.userSpecifiedLabel = '%t %q %d %s %%::%t %q %d %s %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name github/vscode-codeql-integration-tests in progress %::${dateStr} query-name github/vscode-codeql-integration-tests in progress %`);
});
it('should interpolate query when not user specified', () => {
const fqi = createMockRemoteQueryInfo();
expect(labelProvider.getLabel(fqi)).to.eq('xxx query-name xxx');
config.format = '%t %q %d %s %f %r %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name github/vscode-codeql-integration-tests in progress query-file.ql %`);
config.format = '%t %q %d %s %f %r %%::%t %q %d %s %f %r %%';
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name github/vscode-codeql-integration-tests in progress query-file.ql %::${dateStr} query-name github/vscode-codeql-integration-tests in progress query-file.ql %`);
});
it('should get query short label', () => {
const fqi = createMockRemoteQueryInfo('xxx');
// fall back on user specified if one exists.
expect(labelProvider.getShortLabel(fqi)).to.eq('xxx');
// use query name if no user-specified label exists
delete (fqi as any).userSpecifiedLabel;
expect(labelProvider.getShortLabel(fqi)).to.eq('query-name');
});
function createMockRemoteQueryInfo(userSpecifiedLabel?: string) {
return {
t: 'remote',
userSpecifiedLabel,
remoteQuery: {
executionStartTime: date.getTime(),
queryName: 'query-name',
queryFilePath: 'query-file.ql',
controllerRepository: {
owner: 'github',
name: 'vscode-codeql-integration-tests'
}
},
status: 'in progress',
} as unknown as RemoteQueryHistoryItem;
}
});
});

View File

@@ -8,7 +8,7 @@ import { logger } from '../../logging';
import { registerQueryHistoryScubber } from '../../query-history-scrubber';
import { QueryHistoryManager, HistoryTreeDataProvider, SortOrder } from '../../query-history';
import { QueryEvaluationInfo, QueryWithResults } from '../../run-queries';
import { QueryHistoryConfigListener } from '../../config';
import { QueryHistoryConfig, QueryHistoryConfigListener } from '../../config';
import * as messages from '../../pure/messages';
import { QueryServerClient } from '../../queryserver-client';
import { LocalQueryInfo, InitialQueryInfo } from '../../query-results';
@@ -17,6 +17,7 @@ import * as tmp from 'tmp-promise';
import { ONE_DAY_IN_MS, ONE_HOUR_IN_MS, TWO_HOURS_IN_MS, THREE_HOURS_IN_MS } from '../../pure/helpers-pure';
import { tmpDir } from '../../helpers';
import { getErrorMessage } from '../../pure/helpers-pure';
import { HistoryItemLabelProvider } from '../../history-item-label-provider';
describe('query-history', () => {
const mockExtensionLocation = path.join(tmpDir.name, 'mock-extension-location');
@@ -308,8 +309,12 @@ describe('query-history', () => {
describe('HistoryTreeDataProvider', () => {
let historyTreeDataProvider: HistoryTreeDataProvider;
let labelProvider: HistoryItemLabelProvider;
beforeEach(() => {
historyTreeDataProvider = new HistoryTreeDataProvider(vscode.Uri.file(mockExtensionLocation).fsPath);
labelProvider = new HistoryItemLabelProvider({
/**/
} as QueryHistoryConfig);
historyTreeDataProvider = new HistoryTreeDataProvider(vscode.Uri.file(mockExtensionLocation).fsPath, labelProvider);
});
afterEach(() => {
@@ -324,7 +329,7 @@ describe('query-history', () => {
title: 'Query History Item',
command: 'codeQLQueryHistory.itemClicked',
arguments: [mockQuery],
tooltip: mockQuery.label,
tooltip: labelProvider.getLabel(mockQuery),
});
expect(treeItem.label).to.contain('hucairz');
expect(treeItem.contextValue).to.eq('rawResultsItem');
@@ -507,9 +512,17 @@ describe('query-history', () => {
function item(label: string, start: number, t = 'local', resultCount?: number) {
if (t === 'local') {
return {
label,
getQueryName() {
return label;
},
getQueryFileName() {
return label + '.ql';
},
initialInfo: {
start: new Date(start),
databaseInfo: {
name: 'test',
}
},
completedQuery: {
resultCount,
@@ -518,9 +531,16 @@ describe('query-history', () => {
};
} else {
return {
label,
status: 'success',
remoteQuery: {
queryFilePath: label + '.ql',
queryName: label,
executionStartTime: start,
controllerRepository: {
name: 'test',
owner: 'user',
},
repositories: []
},
t
};
@@ -535,7 +555,6 @@ describe('query-history', () => {
start: new Date(),
queryPath: 'hucairz'
} as InitialQueryInfo,
configListener,
{
dispose: () => { /**/ },
} as vscode.CancellationTokenSource
@@ -749,6 +768,7 @@ describe('query-history', () => {
extensionPath: vscode.Uri.file('/x/y/z').fsPath,
} as vscode.ExtensionContext,
configListener,
new HistoryItemLabelProvider({} as QueryHistoryConfig),
doCompareCallback
);
qhm.onWillOpenQueryItem(selectedCallback);

View File

@@ -4,18 +4,15 @@ import * as fs from 'fs-extra';
import * as sinon from 'sinon';
import { LocalQueryInfo, InitialQueryInfo, interpretResultsSarif } from '../../query-results';
import { QueryEvaluationInfo, QueryWithResults } from '../../run-queries';
import { QueryHistoryConfig } from '../../config';
import { EvaluationResult, QueryResultType } from '../../pure/messages';
import { DatabaseInfo, SortDirection, SortedResultSetInfo } from '../../pure/interface-types';
import { CodeQLCliServer, SourceInfo } from '../../cli';
import { CancellationTokenSource, Uri, env } from 'vscode';
import { CancellationTokenSource, Uri } from 'vscode';
import { tmpDir } from '../../helpers';
import { slurpQueryHistory, splatQueryHistory } from '../../query-serialization';
describe('query-results', () => {
let disposeSpy: sinon.SinonSpy;
let onDidChangeQueryHistoryConfigurationSpy: sinon.SinonSpy;
let mockConfig: QueryHistoryConfig;
let sandbox: sinon.SinonSandbox;
let queryPath: string;
let cnt = 0;
@@ -23,8 +20,6 @@ describe('query-results', () => {
beforeEach(() => {
sandbox = sinon.createSandbox();
disposeSpy = sandbox.spy();
onDidChangeQueryHistoryConfigurationSpy = sandbox.spy();
mockConfig = mockQueryHistoryConfig();
queryPath = path.join(Uri.file(tmpDir.name).fsPath, `query-${cnt++}`);
});
@@ -33,17 +28,6 @@ describe('query-results', () => {
});
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 %`);
});
it('should get the query name', () => {
const fqi = createMockFullQueryInfo();
@@ -83,23 +67,6 @@ describe('query-results', () => {
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(queryPath));
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 query = createMockQueryWithResults(queryPath);
const fqi = createMockFullQueryInfo('a', query);
@@ -283,7 +250,7 @@ describe('query-results', () => {
// splat and slurp
await splatQueryHistory(allHistory, allHistoryPath);
const allHistoryActual = await slurpQueryHistory(allHistoryPath, mockConfig);
const allHistoryActual = await slurpQueryHistory(allHistoryPath);
// the dispose methods will be different. Ignore them.
allHistoryActual.forEach(info => {
@@ -325,7 +292,7 @@ describe('query-results', () => {
queries: allHistory
}), 'utf8');
const allHistoryActual = await slurpQueryHistory(badPath, mockConfig);
const allHistoryActual = await slurpQueryHistory(badPath);
// version number is invalid. Should return an empty array.
expect(allHistoryActual).to.deep.eq([]);
});
@@ -386,7 +353,6 @@ describe('query-results', () => {
isQuickEval: false,
id: `some-id-${dbName}`,
} as InitialQueryInfo,
mockQueryHistoryConfig(),
{
dispose: () => { /**/ },
} as CancellationTokenSource
@@ -400,12 +366,4 @@ describe('query-results', () => {
}
return fqi;
}
function mockQueryHistoryConfig(): QueryHistoryConfig {
return {
onDidChangeConfiguration: onDidChangeQueryHistoryConfigurationSpy,
ttlInMillis: 999999,
format: 'from config %q'
};
}
});

View File

@@ -16,6 +16,7 @@ import { DisposableBucket } from '../../disposable-bucket';
import { testDisposeHandler } from '../../test-dispose-handler';
import { walkDirectory } from '../../../helpers';
import { getErrorMessage } from '../../../pure/helpers-pure';
import { HistoryItemLabelProvider } from '../../../history-item-label-provider';
/**
* Tests for remote queries and how they interact with the query history manager.
@@ -71,6 +72,7 @@ describe('Remote queries and query history manager', function() {
{
onDidChangeConfiguration: () => new DisposableBucket(),
} as unknown as QueryHistoryConfig,
new HistoryItemLabelProvider({} as QueryHistoryConfig),
asyncNoop
);
disposables.push(qhm);