Add Multi-select support to query history view

This is not quite ideal due to
https://github.com/microsoft/vscode/issues/99767

Allow multiselection in the query-history view. For commands
that shouldn't accept multiple options, show a user message
to that effect.

For remove query, allow multiple removals at once.

For compare query, allow selecting of exactly two queries.
Otherwise, throw an error. Also, verify that the selected queries
are compatible to compare.
This commit is contained in:
Andrew Eisenberg
2020-06-10 09:01:50 -07:00
parent dd44bf74e3
commit bd6a6ff40d
5 changed files with 492 additions and 211 deletions

View File

@@ -34,5 +34,7 @@
"resolvePluginsRelativeTo": "./extensions/ql-vscode"
},
"editor.formatOnSave": false,
"prettier.singleQuote": true
"prettier.singleQuote": true,
"typescript.preferences.quoteStyle": "single",
"javascript.preferences.quoteStyle": "single"
}

View File

@@ -302,14 +302,6 @@
"command": "codeQLQueryHistory.viewSarif",
"title": "View SARIF"
},
{
"command": "codeQLQueryResults.nextPathStep",
"title": "CodeQL: Show Next Step on Path"
},
{
"command": "codeQLQueryResults.previousPathStep",
"title": "CodeQL: Show Previous Step on Path"
},
{
"command": "codeQLQueryHistory.setLabel",
"title": "Set Label"
@@ -318,6 +310,14 @@
"command": "codeQLQueryHistory.compareWith",
"title": "Compare with..."
},
{
"command": "codeQLQueryResults.nextPathStep",
"title": "CodeQL: Show Next Step on Path"
},
{
"command": "codeQLQueryResults.previousPathStep",
"title": "CodeQL: Show Previous Step on Path"
},
{
"command": "codeQL.restartQueryServer",
"title": "CodeQL: Restart Query Server"

View File

@@ -21,7 +21,7 @@ export type QueryHistoryItemOptions = {
label?: string; // user-settable label
queryText?: string; // text of the selected file
isQuickQuery?: boolean;
}
};
const SHOW_QUERY_TEXT_MSG = `\
////////////////////////////////////////////////////////////////////////////////////
@@ -53,16 +53,19 @@ const FAILED_QUERY_HISTORY_ITEM_ICON = 'media/red-x.svg';
/**
* Tree data provider for the query history view.
*/
class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery> {
class HistoryTreeDataProvider
implements vscode.TreeDataProvider<CompletedQuery> {
/**
* XXX: This idiom for how to get a `.fire()`-able event emitter was
* cargo culted from another vscode extension. It seems rather
* involved and I hope there's something better that can be done
* instead.
*/
private _onDidChangeTreeData: vscode.EventEmitter<CompletedQuery | undefined> = new vscode.EventEmitter<CompletedQuery | undefined>();
readonly onDidChangeTreeData: vscode.Event<CompletedQuery | undefined> = this._onDidChangeTreeData.event;
private _onDidChangeTreeData: vscode.EventEmitter<
CompletedQuery | undefined
> = new vscode.EventEmitter<CompletedQuery | undefined>();
readonly onDidChangeTreeData: vscode.Event<CompletedQuery | undefined> = this
._onDidChangeTreeData.event;
private history: CompletedQuery[] = [];
@@ -71,8 +74,7 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
*/
private current: CompletedQuery | undefined;
constructor(private ctx: ExtensionContext) {
}
constructor(private ctx: ExtensionContext) { }
async getTreeItem(element: CompletedQuery): Promise<vscode.TreeItem> {
const it = new vscode.TreeItem(element.toString());
@@ -86,20 +88,26 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
// Mark this query history item according to whether it has a
// SARIF file so that we can make context menu items conditionally
// available.
it.contextValue = await element.query.hasInterpretedResults() ? 'interpretedResultsItem' : 'rawResultsItem';
it.contextValue = (await element.query.hasInterpretedResults())
? 'interpretedResultsItem'
: 'rawResultsItem';
if (!element.didRunSuccessfully) {
it.iconPath = path.join(this.ctx.extensionPath, FAILED_QUERY_HISTORY_ITEM_ICON);
it.iconPath = path.join(
this.ctx.extensionPath,
FAILED_QUERY_HISTORY_ITEM_ICON
);
}
return it;
}
getChildren(element?: CompletedQuery): vscode.ProviderResult<CompletedQuery[]> {
getChildren(
element?: CompletedQuery
): vscode.ProviderResult<CompletedQuery[]> {
if (element == undefined) {
return this.history;
}
else {
} else {
return [];
}
}
@@ -123,9 +131,8 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
}
remove(item: CompletedQuery) {
if (this.current === item)
this.current = undefined;
const index = this.history.findIndex(i => i === item);
if (this.current === item) this.current = undefined;
const index = this.history.findIndex((i) => i === item);
if (index >= 0) {
this.history.splice(index, 1);
if (this.current === undefined && this.history.length > 0) {
@@ -135,7 +142,6 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
}
this.refresh();
}
}
get allHistory(): CompletedQuery[] {
@@ -147,7 +153,7 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
}
find(queryId: number): CompletedQuery | undefined {
return this.allHistory.find(query => query.query.queryID === queryId);
return this.allHistory.find((query) => query.query.queryID === queryId);
}
}
@@ -162,6 +168,105 @@ export class QueryHistoryManager {
treeView: vscode.TreeView<CompletedQuery>;
lastItemClick: { time: Date; item: CompletedQuery } | undefined;
constructor(
ctx: ExtensionContext,
private queryHistoryConfigListener: QueryHistoryConfig,
private selectedCallback: (item: CompletedQuery) => Promise<void>,
private doCompareCallback: (
from: CompletedQuery,
to: CompletedQuery
) => Promise<void>
) {
const treeDataProvider = (this.treeDataProvider = new HistoryTreeDataProvider(
ctx
));
this.treeView = Window.createTreeView('codeQLQueryHistory', {
treeDataProvider,
canSelectMany: true,
});
// Lazily update the tree view selection due to limitations of TreeView API (see
// `updateTreeViewSelectionIfVisible` doc for details)
this.treeView.onDidChangeVisibility(async (_ev) =>
this.updateTreeViewSelectionIfVisible()
);
// Don't allow the selection to become empty
this.treeView.onDidChangeSelection(async (ev) => {
if (ev.selection.length == 0) {
this.updateTreeViewSelectionIfVisible();
}
});
logger.log('Registering query history panel commands.');
ctx.subscriptions.push(
vscode.commands.registerCommand(
'codeQLQueryHistory.openQuery',
this.handleOpenQuery.bind(this)
)
);
ctx.subscriptions.push(
vscode.commands.registerCommand(
'codeQLQueryHistory.removeHistoryItem',
this.handleRemoveHistoryItem.bind(this)
)
);
ctx.subscriptions.push(
vscode.commands.registerCommand(
'codeQLQueryHistory.setLabel',
this.handleSetLabel.bind(this)
)
);
ctx.subscriptions.push(
vscode.commands.registerCommand(
'codeQLQueryHistory.compareWith',
this.handleCompareWith.bind(this)
)
);
ctx.subscriptions.push(
vscode.commands.registerCommand(
'codeQLQueryHistory.showQueryLog',
this.handleShowQueryLog.bind(this)
)
);
ctx.subscriptions.push(
vscode.commands.registerCommand(
'codeQLQueryHistory.showQueryText',
this.handleShowQueryText.bind(this)
)
);
ctx.subscriptions.push(
vscode.commands.registerCommand(
'codeQLQueryHistory.viewSarif',
this.handleViewSarif.bind(this)
)
);
ctx.subscriptions.push(
vscode.commands.registerCommand(
'codeQLQueryHistory.itemClicked',
async (item) => {
return this.handleItemClicked(item, [item]);
}
)
);
queryHistoryConfigListener.onDidChangeQueryHistoryConfiguration(() => {
this.treeDataProvider.refresh();
});
// displays query text in a read-only document
vscode.workspace.registerTextDocumentContentProvider('codeql', {
provideTextDocumentContent(
uri: vscode.Uri
): vscode.ProviderResult<string> {
const params = new URLSearchParams(uri.query);
return (
(JSON.parse(params.get('isQuickEval') || '')
? SHOW_QUERY_TEXT_QUICK_EVAL_MSG
: SHOW_QUERY_TEXT_MSG) + params.get('queryText')
);
},
});
}
async invokeCallbackOn(queryHistoryItem: CompletedQuery) {
if (this.selectedCallback !== undefined) {
const sc = this.selectedCallback;
@@ -169,20 +274,42 @@ export class QueryHistoryManager {
}
}
async handleOpenQuery(queryHistoryItem: CompletedQuery): Promise<void> {
const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(queryHistoryItem.query.program.queryPath));
const editor = await vscode.window.showTextDocument(textDocument, vscode.ViewColumn.One);
const queryText = queryHistoryItem.options.queryText;
if (queryText !== undefined && queryHistoryItem.options.isQuickQuery) {
await editor.edit(edit => edit.replace(textDocument.validateRange(
new vscode.Range(0, 0, textDocument.lineCount, 0)), queryText)
async handleOpenQuery(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
): Promise<void> {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
const textDocument = await vscode.workspace.openTextDocument(
vscode.Uri.file(singleItem.query.program.queryPath)
);
const editor = await vscode.window.showTextDocument(
textDocument,
vscode.ViewColumn.One
);
const queryText = singleItem.options.queryText;
if (queryText !== undefined && singleItem.options.isQuickQuery) {
await editor.edit((edit) =>
edit.replace(
textDocument.validateRange(
new vscode.Range(0, 0, textDocument.lineCount, 0)
),
queryText
)
);
}
}
async handleRemoveHistoryItem(queryHistoryItem: CompletedQuery) {
this.treeDataProvider.remove(queryHistoryItem);
queryHistoryItem.dispose();
async handleRemoveHistoryItem(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
) {
(multiSelect || [singleItem]).forEach((item) => {
this.treeDataProvider.remove(item);
item.dispose();
});
const current = this.treeDataProvider.getCurrent();
if (current !== undefined) {
this.treeView.reveal(current);
@@ -190,74 +317,109 @@ export class QueryHistoryManager {
}
}
async handleSetLabel(queryHistoryItem: CompletedQuery) {
async handleSetLabel(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
): Promise<void> {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
const response = await vscode.window.showInputBox({
prompt: 'Label:',
placeHolder: '(use default)',
value: queryHistoryItem.getLabel(),
value: singleItem.getLabel(),
});
// undefined response means the user cancelled the dialog; don't change anything
if (response !== undefined) {
if (response === '')
// Interpret empty string response as "go back to using default"
queryHistoryItem.options.label = undefined;
else
queryHistoryItem.options.label = response;
// Interpret empty string response as 'go back to using default'
singleItem.options.label = undefined;
else singleItem.options.label = response;
this.treeDataProvider.refresh();
}
}
async handleCompareWith(query: CompletedQuery) {
const dbName = query.database.name;
const comparableQueryLabels = this.treeDataProvider.allHistory
.filter((otherQuery) => otherQuery !== query && otherQuery.didRunSuccessfully && otherQuery.database.name === dbName)
.map(otherQuery => ({
label: otherQuery.toString(),
description: otherQuery.databaseName,
detail: otherQuery.statusString,
query: otherQuery
}));
const choice = await vscode.window.showQuickPick(comparableQueryLabels);
if (choice) {
this.doCompareCallback(query, choice.query);
async handleCompareWith(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
) {
try {
if (!singleItem.didRunSuccessfully) {
throw new Error('Please select a successful query.');
}
const from = singleItem;
const to = await this.findOtherQueryToCompare(singleItem, multiSelect);
if (from && to) {
this.doCompareCallback(from, to);
}
} catch (e) {
helpers.showAndLogErrorMessage(e.message);
}
}
async handleItemClicked(queryHistoryItem: CompletedQuery) {
this.treeDataProvider.setCurrentItem(queryHistoryItem);
async handleItemClicked(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
this.treeDataProvider.setCurrentItem(singleItem);
const now = new Date();
const prevItemClick = this.lastItemClick;
this.lastItemClick = { time: now, item: queryHistoryItem };
this.lastItemClick = { time: now, item: singleItem };
if (prevItemClick !== undefined
&& (now.valueOf() - prevItemClick.time.valueOf()) < DOUBLE_CLICK_TIME
&& queryHistoryItem == prevItemClick.item) {
if (
prevItemClick !== undefined &&
now.valueOf() - prevItemClick.time.valueOf() < DOUBLE_CLICK_TIME &&
singleItem == prevItemClick.item
) {
// show original query file on double click
await this.handleOpenQuery(queryHistoryItem);
}
else {
await this.handleOpenQuery(singleItem, [singleItem]);
} else {
// show results on single click
await this.invokeCallbackOn(queryHistoryItem);
await this.invokeCallbackOn(singleItem);
}
}
async handleShowQueryLog(queryHistoryItem: CompletedQuery) {
if (queryHistoryItem.logFileLocation) {
await this.tryOpenExternalFile(queryHistoryItem.logFileLocation);
async handleShowQueryLog(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
if (singleItem.logFileLocation) {
await this.tryOpenExternalFile(singleItem.logFileLocation);
} else {
helpers.showAndLogWarningMessage('No log file available');
}
}
async handleShowQueryText(queryHistoryItem: CompletedQuery) {
async handleShowQueryText(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
try {
const queryName = queryHistoryItem.queryName.endsWith('.ql') ? queryHistoryItem.queryName : queryHistoryItem.queryName + '.ql';
const queryName = singleItem.queryName.endsWith('.ql')
? singleItem.queryName
: singleItem.queryName + '.ql';
const params = new URLSearchParams({
isQuickEval: String(!!queryHistoryItem.query.quickEvalPosition),
queryText: await this.getQueryText(queryHistoryItem)
isQuickEval: String(!!singleItem.query.quickEvalPosition),
queryText: await this.getQueryText(singleItem),
});
const uri = vscode.Uri.parse(`codeql:${queryHistoryItem.query.queryID}-${queryName}?${params.toString()}`);
const uri = vscode.Uri.parse(
`codeql:${singleItem.query.queryID}-${queryName}?${params.toString()}`
);
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc, { preview: false });
} catch (e) {
@@ -265,17 +427,25 @@ export class QueryHistoryManager {
}
}
async handleViewSarif(queryHistoryItem: CompletedQuery) {
async handleViewSarif(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
try {
const hasInterpretedResults = await queryHistoryItem.query.canHaveInterpretedResults();
const hasInterpretedResults = await singleItem.query.canHaveInterpretedResults();
if (hasInterpretedResults) {
await this.tryOpenExternalFile(
queryHistoryItem.query.resultsPaths.interpretedResultsPath
singleItem.query.resultsPaths.interpretedResultsPath
);
} else {
const label = singleItem.getLabel();
helpers.showAndLogInformationMessage(
`Query ${label} has no interpreted results.`
);
}
else {
const label = queryHistoryItem.getLabel();
helpers.showAndLogInformationMessage(`Query ${label} has no interpreted results.`);
}
} catch (e) {
helpers.showAndLogErrorMessage(e.message);
@@ -289,58 +459,17 @@ export class QueryHistoryManager {
// 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);
return textDocument.getText(new vscode.Range(startLine - 1, 0, endLine, 0));
const textDocument = await vscode.workspace.openTextDocument(
queryHistoryItem.query.quickEvalPosition.fileName
);
return textDocument.getText(
new vscode.Range(startLine - 1, 0, endLine, 0)
);
} else {
return '';
}
}
constructor(
ctx: ExtensionContext,
private queryHistoryConfigListener: QueryHistoryConfig,
private selectedCallback: (item: CompletedQuery) => Promise<void>,
private doCompareCallback: (from: CompletedQuery, to: CompletedQuery) => Promise<void>,
) {
const treeDataProvider = this.treeDataProvider = new HistoryTreeDataProvider(ctx);
this.treeView = Window.createTreeView('codeQLQueryHistory', { treeDataProvider });
// Lazily update the tree view selection due to limitations of TreeView API (see
// `updateTreeViewSelectionIfVisible` doc for details)
this.treeView.onDidChangeVisibility(async _ev => this.updateTreeViewSelectionIfVisible());
// Don't allow the selection to become empty
this.treeView.onDidChangeSelection(async ev => {
if (ev.selection.length == 0) {
this.updateTreeViewSelectionIfVisible();
}
});
logger.log('Registering query history panel commands.');
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.openQuery', this.handleOpenQuery));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.removeHistoryItem', this.handleRemoveHistoryItem.bind(this)));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.setLabel', this.handleSetLabel.bind(this)));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.compareWith', this.handleCompareWith.bind(this)));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.showQueryLog', this.handleShowQueryLog.bind(this)));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.showQueryText', this.handleShowQueryText.bind(this)));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.viewSarif', this.handleViewSarif.bind(this)));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.itemClicked', async (item) => {
return this.handleItemClicked(item);
}));
queryHistoryConfigListener.onDidChangeQueryHistoryConfiguration(() => {
this.treeDataProvider.refresh();
});
// displays query text in a read-only document
vscode.workspace.registerTextDocumentContentProvider('codeql', {
provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult<string> {
const params = new URLSearchParams(uri.query);
return (
JSON.parse(params.get('isQuickEval') || '') ? SHOW_QUERY_TEXT_QUICK_EVAL_MSG : SHOW_QUERY_TEXT_MSG
) + params.get('queryText');
}
});
}
addQuery(info: QueryWithResults): CompletedQuery {
const item = new CompletedQuery(info, this.queryHistoryConfigListener);
this.treeDataProvider.push(item);
@@ -404,4 +533,56 @@ 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;
// 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) {
throw new Error('Please select a successful query.');
}
if (otherQuery.database.name !== dbName) {
throw new Error('Query databases must be the same.');
}
return otherQuery;
}
if (multiSelect.length > 1) {
throw new Error('Please select no more than 2 queries.');
}
// otherwise, let the user choose
const comparableQueryLabels = this.treeDataProvider.allHistory
.filter(
(otherQuery) =>
otherQuery !== singleItem &&
otherQuery.didRunSuccessfully &&
otherQuery.database.name === dbName
)
.map((otherQuery) => ({
label: otherQuery.toString(),
description: otherQuery.databaseName,
detail: otherQuery.statusString,
query: otherQuery,
}));
const choice = await vscode.window.showQuickPick(comparableQueryLabels);
return choice?.query;
}
private assertSingleQuery(multiSelect: CompletedQuery[] = [], message = 'Please select a single query.') {
if (multiSelect.length > 1) {
helpers.showAndLogErrorMessage(
message
);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,182 @@
import * as chai from 'chai';
import 'mocha';
import 'sinon-chai';
import * as vscode from 'vscode';
import * as sinon from 'sinon';
import * as chaiAsPromised from 'chai-as-promised';
import { logger } from '../../logging';
import { QueryHistoryManager } from '../../query-history';
chai.use(chaiAsPromised);
const expect = chai.expect;
const assert = chai.assert;
describe('query-history', () => {
let showTextDocumentSpy: sinon.SinonStub;
let showInformationMessageSpy: sinon.SinonStub;
let executeCommandSpy: sinon.SinonStub;
let showQuickPickSpy: sinon.SinonStub;
let tryOpenExternalFile: Function;
beforeEach(() => {
showTextDocumentSpy = sinon.stub(vscode.window, 'showTextDocument');
showInformationMessageSpy = sinon.stub(
vscode.window,
'showInformationMessage'
);
showQuickPickSpy = sinon.stub(
vscode.window,
'showQuickPick'
);
executeCommandSpy = sinon.stub(vscode.commands, 'executeCommand');
sinon.stub(logger, 'log');
tryOpenExternalFile = (QueryHistoryManager.prototype as any).tryOpenExternalFile;
});
afterEach(() => {
(vscode.window.showTextDocument as sinon.SinonStub).restore();
(vscode.commands.executeCommand as sinon.SinonStub).restore();
(logger.log as sinon.SinonStub).restore();
(vscode.window.showInformationMessage as sinon.SinonStub).restore();
(vscode.window.showQuickPick as sinon.SinonStub).restore();
});
describe('tryOpenExternalFile', () => {
it('should open an external file', async () => {
await tryOpenExternalFile('xxx');
expect(showTextDocumentSpy).to.have.been.calledOnceWith(
vscode.Uri.file('xxx')
);
expect(executeCommandSpy).not.to.have.been.called;
});
[
'too large to open',
'Files above 50MB cannot be synchronized with extensions',
].forEach(msg => {
it(`should fail to open a file because "${msg}" and open externally`, async () => {
showTextDocumentSpy.throws(new Error(msg));
showInformationMessageSpy.returns({ title: 'Yes' });
await tryOpenExternalFile('xxx');
const uri = vscode.Uri.file('xxx');
expect(showTextDocumentSpy).to.have.been.calledOnceWith(
uri
);
expect(executeCommandSpy).to.have.been.calledOnceWith(
'revealFileInOS',
uri
);
});
it(`should fail to open a file because "${msg}" and NOT open externally`, async () => {
showTextDocumentSpy.throws(new Error(msg));
showInformationMessageSpy.returns({ title: 'No' });
await tryOpenExternalFile('xxx');
const uri = vscode.Uri.file('xxx');
expect(showTextDocumentSpy).to.have.been.calledOnceWith(uri);
expect(showInformationMessageSpy).to.have.been.called;
expect(executeCommandSpy).not.to.have.been.called;
});
});
});
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);
showQuickPickSpy.returns({ query: allHistory[0] });
const otherQuery = await queryHistory.findOtherQueryToCompare(thisQuery, []);
expect(otherQuery).to.eq(allHistory[0]);
// only called with first item, other items filtered out
expect(showQuickPickSpy.getCalls().length).to.eq(1);
expect(showQuickPickSpy.firstCall.args[0][0].query).to.eq(allHistory[0]);
});
it('should handle cancelling out of the quick select', async () => {
const thisQuery = allHistory[3];
const queryHistory = createMockQueryHistory(allHistory);
const otherQuery = await queryHistory.findOtherQueryToCompare(thisQuery, []);
expect(otherQuery).to.be.undefined;
// only called with first item, other items filtered out
expect(showQuickPickSpy.getCalls().length).to.eq(1);
expect(showQuickPickSpy.firstCall.args[0][0].query).to.eq(allHistory[0]);
});
it('should compare against 2 queries', async () => {
const thisQuery = allHistory[3];
const queryHistory = createMockQueryHistory(allHistory);
const otherQuery = await queryHistory.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;
try {
await queryHistory.findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0]]);
assert(false, 'Should have thrown');
} catch (e) {
expect(e.message).to.eq('Please select a successful query.');
}
});
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';
try {
await queryHistory.findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0]]);
assert(false, 'Should have thrown');
} catch (e) {
expect(e.message).to.eq('Query databases must be the same.');
}
});
it('should throw an error when more than 2 queries selected', async () => {
const thisQuery = allHistory[3];
const queryHistory = createMockQueryHistory(allHistory);
try {
await queryHistory.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.');
}
});
});
});
function createMockQueryHistory(allHistory: {}[]) {
return {
assertSingleQuery: (QueryHistoryManager.prototype as any).assertSingleQuery,
findOtherQueryToCompare: (QueryHistoryManager.prototype as any).findOtherQueryToCompare,
treeDataProvider: {
allHistory
}
};
}

View File

@@ -1,84 +0,0 @@
import * as chai from 'chai';
import 'mocha';
import * as vscode from 'vscode';
import * as sinon from 'sinon';
// import * as sinonChai from 'sinon-chai';
import * as chaiAsPromised from 'chai-as-promised';
import { logger } from '../../logging';
import { QueryHistoryManager } from '../../query-history';
chai.use(chaiAsPromised);
const expect = chai.expect;
describe('query-history', () => {
describe('tryOpenExternalFile', () => {
let showTextDocumentSpy: sinon.SinonStub;
let showInformationMessageSpy: sinon.SinonStub;
let executeCommandSpy: sinon.SinonStub;
let logSpy: sinon.SinonStub;
let tryOpenExternalFile: Function;
beforeEach(() => {
showTextDocumentSpy = sinon.stub(vscode.window, 'showTextDocument');
showInformationMessageSpy = sinon.stub(
vscode.window,
'showInformationMessage'
);
executeCommandSpy = sinon.stub(vscode.commands, 'executeCommand');
logSpy = sinon.stub(logger, 'log');
tryOpenExternalFile = (QueryHistoryManager.prototype as any).tryOpenExternalFile;
logSpy;
executeCommandSpy;
});
afterEach(() => {
(vscode.window.showTextDocument as sinon.SinonStub).restore();
(vscode.commands.executeCommand as sinon.SinonStub).restore();
(logger.log as sinon.SinonStub).restore();
(vscode.window.showInformationMessage as sinon.SinonStub).restore();
});
it('should open an external file', async () => {
await tryOpenExternalFile('xxx');
expect(showTextDocumentSpy).to.have.been.calledOnceWith(
vscode.Uri.file('xxx')
);
expect(executeCommandSpy).not.to.have.been.called;
});
[
'too large to open',
'Files above 50MB cannot be synchronized with extensions',
].forEach(msg => {
it(`should fail to open a file because "${msg}" and open externally`, async () => {
showTextDocumentSpy.throws(new Error(msg));
showInformationMessageSpy.returns({ title: 'Yes' });
await tryOpenExternalFile('xxx');
const uri = vscode.Uri.file('xxx');
expect(showTextDocumentSpy).to.have.been.calledOnceWith(
uri
);
expect(executeCommandSpy).to.have.been.calledOnceWith(
'revealFileInOS',
uri
);
});
it(`should fail to open a file because "${msg}" and NOT open externally`, async () => {
showTextDocumentSpy.throws(new Error(msg));
showInformationMessageSpy.returns({ title: 'No' });
await tryOpenExternalFile('xxx');
const uri = vscode.Uri.file('xxx');
expect(showTextDocumentSpy).to.have.been.calledOnceWith(uri);
expect(showInformationMessageSpy).to.have.been.called;
expect(executeCommandSpy).not.to.have.been.called;
});
});
});
});