Refactor: Invert dependency between query history and remote quries managers (#1396)

This commit is contained in:
Charis Kyriakou
2022-06-23 13:28:57 +01:00
committed by GitHub
parent 58bbb59e39
commit fd4b6022a9
5 changed files with 248 additions and 164 deletions

View File

@@ -95,9 +95,9 @@ import { RemoteQueriesManager } from './remote-queries/remote-queries-manager';
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';
import { exportRemoteQueryResults } from './remote-queries/export-results';
import { RemoteQuery } from './remote-queries/remote-query';
/**
* extension.ts
@@ -451,10 +451,20 @@ async function activateWithInstalledDistribution(
await fs.ensureDir(queryStorageDir);
const labelProvider = new HistoryItemLabelProvider(queryHistoryConfigurationListener);
void logger.log('Initializing results panel interface.');
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger, labelProvider);
ctx.subscriptions.push(intm);
void logger.log('Initializing variant analysis manager.');
const rqm = new RemoteQueriesManager(ctx, cliServer, queryStorageDir, logger);
ctx.subscriptions.push(rqm);
void logger.log('Initializing query history.');
const qhm = new QueryHistoryManager(
qs,
dbm,
intm,
rqm,
queryStorageDir,
ctx,
queryHistoryConfigurationListener,
@@ -463,17 +473,11 @@ async function activateWithInstalledDistribution(
showResultsForComparison(from, to),
);
qhm.onWillOpenQueryItem(async item => {
if (item.t === 'local' && item.completed) {
await showResultsForCompletedQuery(item as CompletedLocalQueryInfo, WebviewReveal.Forced);
}
});
ctx.subscriptions.push(qhm);
void logger.log('Initializing results panel interface.');
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger, labelProvider);
ctx.subscriptions.push(intm);
void logger.log('Reading query history');
await qhm.readQueryHistory();
void logger.log('Initializing compare panel interface.');
const cmpm = new CompareInterfaceManager(
@@ -844,14 +848,6 @@ async function activateWithInstalledDistribution(
)
);
void logger.log('Initializing variant analysis results view.');
const rqm = new RemoteQueriesManager(ctx, cliServer, qhm, queryStorageDir, logger);
ctx.subscriptions.push(rqm);
// wait until after the remote queries manager is initialized to read the query history
// since the rqm is notified of queries being added.
await qhm.readQueryHistory();
registerRemoteQueryTextProvider();
@@ -884,9 +880,10 @@ async function activateWithInstalledDistribution(
ctx.subscriptions.push(
commandRunner('codeQL.monitorRemoteQuery', async (
queryItem: RemoteQueryHistoryItem,
queryId: string,
query: RemoteQuery,
token: CancellationToken) => {
await rqm.monitorRemoteQuery(queryItem, token);
await rqm.monitorRemoteQuery(queryId, query, token);
}));
ctx.subscriptions.push(

View File

@@ -40,6 +40,10 @@ import { CliVersionConstraint } from './cli';
import { HistoryItemLabelProvider } from './history-item-label-provider';
import { Credentials } from './authentication';
import { cancelRemoteQuery } from './remote-queries/gh-actions-api-client';
import { RemoteQueriesManager } from './remote-queries/remote-queries-manager';
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
import { InterfaceManager } from './interface';
import { WebviewReveal } from './interface-utils';
/**
* query-history.ts
@@ -307,21 +311,11 @@ export class QueryHistoryManager extends DisposableObject {
queryHistoryScrubber: Disposable | undefined;
private queryMetadataStorageLocation;
private readonly _onDidAddQueryItem = super.push(new EventEmitter<QueryHistoryInfo>());
readonly onDidAddQueryItem: Event<QueryHistoryInfo> = this
._onDidAddQueryItem.event;
private readonly _onDidRemoveQueryItem = super.push(new EventEmitter<QueryHistoryInfo>());
readonly onDidRemoveQueryItem: Event<QueryHistoryInfo> = this
._onDidRemoveQueryItem.event;
private readonly _onWillOpenQueryItem = super.push(new EventEmitter<QueryHistoryInfo>());
readonly onWillOpenQueryItem: Event<QueryHistoryInfo> = this
._onWillOpenQueryItem.event;
constructor(
private readonly qs: QueryServerClient,
private readonly dbm: DatabaseManager,
private readonly localQueriesInterfaceManager: InterfaceManager,
private readonly remoteQueriesManager: RemoteQueriesManager,
private readonly queryStorageDir: string,
private readonly ctx: ExtensionContext,
private readonly queryHistoryConfigListener: QueryHistoryConfig,
@@ -525,6 +519,7 @@ export class QueryHistoryManager extends DisposableObject {
}));
this.registerQueryHistoryScrubber(queryHistoryConfigListener, ctx);
this.registerToRemoteQueriesEvents();
}
private getCredentials() {
@@ -548,12 +543,49 @@ export class QueryHistoryManager extends DisposableObject {
);
}
private registerToRemoteQueriesEvents() {
const queryAddedSubscription = this.remoteQueriesManager.onRemoteQueryAdded(event => {
this.addQuery({
t: 'remote',
status: QueryStatus.InProgress,
completed: false,
queryId: event.queryId,
remoteQuery: event.query,
});
});
const queryRemovedSubscription = this.remoteQueriesManager.onRemoteQueryRemoved(async (event) => {
const item = this.treeDataProvider.allHistory.find(i => i.t === 'remote' && i.queryId === event.queryId);
if (item) {
await this.removeRemoteQuery(item as RemoteQueryHistoryItem);
}
});
const queryStatusUpdateSubscription = this.remoteQueriesManager.onRemoteQueryStatusUpdate(async (event) => {
const item = this.treeDataProvider.allHistory.find(i => i.t === 'remote' && i.queryId === event.queryId);
if (item) {
const remoteQueryHistoryItem = item as RemoteQueryHistoryItem;
remoteQueryHistoryItem.status = event.status;
remoteQueryHistoryItem.failureReason = event.failureReason;
await this.refreshTreeView();
} else {
void logger.log('Variant analysis status update event received for unknown variant analysis');
}
});
this.push(queryAddedSubscription);
this.push(queryRemovedSubscription);
this.push(queryStatusUpdateSubscription);
}
async readQueryHistory(): Promise<void> {
void logger.log(`Reading cached query history from '${this.queryMetadataStorageLocation}'.`);
const history = await slurpQueryHistory(this.queryMetadataStorageLocation);
this.treeDataProvider.allHistory = history;
this.treeDataProvider.allHistory.forEach((item) => {
this._onDidAddQueryItem.fire(item);
this.treeDataProvider.allHistory.forEach(async (item) => {
if (item.t === 'remote') {
await this.remoteQueriesManager.rehydrateRemoteQuery(item.queryId, item.remoteQuery, item.status);
}
});
}
@@ -619,26 +651,30 @@ export class QueryHistoryManager extends DisposableObject {
await item.completedQuery?.query.deleteQuery();
}
} else {
// 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 ${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.');
}
this._onDidRemoveQueryItem.fire(item);
await this.removeRemoteQuery(item);
}
}));
await this.writeQueryHistory();
const current = this.treeDataProvider.getCurrent();
if (current !== undefined) {
await this.treeView.reveal(current, { select: true });
this._onWillOpenQueryItem.fire(current);
await this.openQueryResults(current);
}
}
private async removeRemoteQuery(item: RemoteQueryHistoryItem): Promise<void> {
// 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 ${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.');
}
await this.remoteQueriesManager.removeRemoteQuery(item.queryId);
}
async handleSortByName() {
if (this.treeDataProvider.sortOrder === SortOrder.NameAsc) {
this.treeDataProvider.sortOrder = SortOrder.NameDesc;
@@ -739,7 +775,7 @@ export class QueryHistoryManager extends DisposableObject {
} else {
// show results on single click only if query is completed successfully.
if (finalSingleItem.status === QueryStatus.Completed) {
await this._onWillOpenQueryItem.fire(finalSingleItem);
await this.openQueryResults(finalSingleItem);
}
}
}
@@ -1027,7 +1063,6 @@ export class QueryHistoryManager extends DisposableObject {
addQuery(item: QueryHistoryInfo) {
this.treeDataProvider.pushQuery(item);
this.updateTreeViewSelectionIfVisible();
this._onDidAddQueryItem.fire(item);
}
/**
@@ -1228,4 +1263,13 @@ the file in the file explorer and dragging it into the workspace.`
this.treeDataProvider.refresh();
await this.writeQueryHistory();
}
private async openQueryResults(item: QueryHistoryInfo) {
if (item.t === 'local') {
await this.localQueriesInterfaceManager.showResults(item as CompletedLocalQueryInfo, WebviewReveal.Forced, false);
}
else if (item.t === 'remote') {
await this.remoteQueriesManager.openRemoteQueryResults(item.queryId);
}
}
}

View File

@@ -1,4 +1,4 @@
import { CancellationToken, commands, ExtensionContext, Uri, window } from 'vscode';
import { CancellationToken, commands, EventEmitter, ExtensionContext, Uri, window } from 'vscode';
import { nanoid } from 'nanoid';
import * as path from 'path';
import * as fs from 'fs-extra';
@@ -18,18 +18,39 @@ import { RemoteQueryResult } from './remote-query-result';
import { DownloadLink } from './download-link';
import { AnalysesResultsManager } from './analyses-results-manager';
import { assertNever } from '../pure/helpers-pure';
import { RemoteQueryHistoryItem } from './remote-query-history-item';
import { QueryHistoryManager } from '../query-history';
import { QueryStatus } from '../query-status';
import { DisposableObject } from '../pure/disposable-object';
import { QueryHistoryInfo } from '../query-results';
import { AnalysisResults } from './shared/analysis-result';
const autoDownloadMaxSize = 300 * 1024;
const autoDownloadMaxCount = 100;
const noop = () => { /* do nothing */ };
export interface NewQueryEvent {
queryId: string;
query: RemoteQuery
}
export interface RemovedQueryEvent {
queryId: string;
}
export interface UpdatedQueryStatusEvent {
queryId: string;
status: QueryStatus;
failureReason?: string;
}
export class RemoteQueriesManager extends DisposableObject {
public readonly onRemoteQueryAdded;
public readonly onRemoteQueryRemoved;
public readonly onRemoteQueryStatusUpdate;
private readonly remoteQueryAddedEventEmitter;
private readonly remoteQueryRemovedEventEmitter;
private readonly remoteQueryStatusUpdateEventEmitter;
private readonly remoteQueriesMonitor: RemoteQueriesMonitor;
private readonly analysesResultsManager: AnalysesResultsManager;
private readonly interfaceManager: RemoteQueriesInterfaceManager;
@@ -37,7 +58,6 @@ export class RemoteQueriesManager extends DisposableObject {
constructor(
private readonly ctx: ExtensionContext,
private readonly cliServer: CodeQLCliServer,
private readonly qhm: QueryHistoryManager,
private readonly storagePath: string,
logger: Logger,
) {
@@ -46,45 +66,43 @@ export class RemoteQueriesManager extends DisposableObject {
this.interfaceManager = new RemoteQueriesInterfaceManager(ctx, logger, this.analysesResultsManager);
this.remoteQueriesMonitor = new RemoteQueriesMonitor(ctx, logger);
// Handle events from the query history manager
this.push(this.qhm.onDidAddQueryItem(this.handleAddQueryItem.bind(this)));
this.push(this.qhm.onDidRemoveQueryItem(this.handleRemoveQueryItem.bind(this)));
this.push(this.qhm.onWillOpenQueryItem(this.handleOpenQueryItem.bind(this)));
this.remoteQueryAddedEventEmitter = this.push(new EventEmitter<NewQueryEvent>());
this.remoteQueryRemovedEventEmitter = this.push(new EventEmitter<RemovedQueryEvent>());
this.remoteQueryStatusUpdateEventEmitter = this.push(new EventEmitter<UpdatedQueryStatusEvent>());
this.onRemoteQueryAdded = this.remoteQueryAddedEventEmitter.event;
this.onRemoteQueryRemoved = this.remoteQueryRemovedEventEmitter.event;
this.onRemoteQueryStatusUpdate = this.remoteQueryStatusUpdateEventEmitter.event;
}
private async handleAddQueryItem(queryItem: QueryHistoryInfo) {
if (queryItem?.t === 'remote') {
if (!(await this.queryHistoryItemExists(queryItem))) {
// In this case, the query was deleted from disk, most likely because it was purged
// by another workspace. We should remove it from the history manager.
await this.qhm.handleRemoveHistoryItem(queryItem);
} else if (queryItem.status === QueryStatus.InProgress) {
// In this case, last time we checked, the query was still in progress.
// We need to setup the monitor to check for completion.
await commands.executeCommand('codeQL.monitorRemoteQuery', queryItem);
}
public async rehydrateRemoteQuery(queryId: string, query: RemoteQuery, status: QueryStatus) {
if (!(await this.queryRecordExists(queryId))) {
// In this case, the query was deleted from disk, most likely because it was purged
// by another workspace.
this.remoteQueryRemovedEventEmitter.fire({ queryId });
} else if (status === QueryStatus.InProgress) {
// In this case, last time we checked, the query was still in progress.
// We need to setup the monitor to check for completion.
await commands.executeCommand('codeQL.monitorRemoteQuery', queryId, query);
}
}
private async handleRemoveQueryItem(queryItem: QueryHistoryInfo) {
if (queryItem?.t === 'remote') {
this.analysesResultsManager.removeAnalysesResults(queryItem.queryId);
await this.removeStorageDirectory(queryItem);
}
public async removeRemoteQuery(queryId: string) {
this.analysesResultsManager.removeAnalysesResults(queryId);
await this.removeStorageDirectory(queryId);
}
private async handleOpenQueryItem(queryItem: QueryHistoryInfo) {
if (queryItem?.t === 'remote') {
try {
const remoteQueryResult = await this.retrieveJsonFile(queryItem, 'query-result.json') as RemoteQueryResult;
// open results in the background
void this.openResults(queryItem.remoteQuery, remoteQueryResult).then(
noop,
err => void showAndLogErrorMessage(err)
);
} catch (e) {
void showAndLogErrorMessage(`Could not open query results. ${e}`);
}
public async openRemoteQueryResults(queryId: string) {
try {
const remoteQuery = await this.retrieveJsonFile(queryId, 'query.json') as RemoteQuery;
const remoteQueryResult = await this.retrieveJsonFile(queryId, 'query-result.json') as RemoteQueryResult;
// Open results in the background
void this.openResults(remoteQuery, remoteQueryResult).then(
noop,
err => void showAndLogErrorMessage(err)
);
} catch (e) {
void showAndLogErrorMessage(`Could not open query results. ${e}`);
}
}
@@ -106,49 +124,40 @@ export class RemoteQueriesManager extends DisposableObject {
const query = querySubmission.query;
const queryId = this.createQueryId(query.queryName);
const queryHistoryItem: RemoteQueryHistoryItem = {
t: 'remote',
status: QueryStatus.InProgress,
completed: false,
queryId,
remoteQuery: query,
};
await this.prepareStorageDirectory(queryHistoryItem);
await this.storeJsonFile(queryHistoryItem, 'query.json', query);
await this.prepareStorageDirectory(queryId);
await this.storeJsonFile(queryId, 'query.json', query);
this.qhm.addQuery(queryHistoryItem);
await this.qhm.refreshTreeView();
this.remoteQueryAddedEventEmitter.fire({ queryId, query });
void commands.executeCommand('codeQL.monitorRemoteQuery', queryId, query);
}
}
public async monitorRemoteQuery(
queryItem: RemoteQueryHistoryItem,
queryId: string,
remoteQuery: RemoteQuery,
cancellationToken: CancellationToken
): Promise<void> {
const credentials = await Credentials.initialize(this.ctx);
const queryWorkflowResult = await this.remoteQueriesMonitor.monitorQuery(queryItem.remoteQuery, cancellationToken);
const queryWorkflowResult = await this.remoteQueriesMonitor.monitorQuery(remoteQuery, cancellationToken);
const executionEndTime = Date.now();
if (queryWorkflowResult.status === 'CompletedSuccessfully') {
await this.downloadAvailableResults(queryItem, credentials, executionEndTime);
await this.downloadAvailableResults(queryId, remoteQuery, credentials, executionEndTime);
} else if (queryWorkflowResult.status === 'CompletedUnsuccessfully') {
if (queryWorkflowResult.error?.includes('cancelled')) {
// workflow was cancelled on the server
queryItem.failureReason = 'Cancelled';
queryItem.status = QueryStatus.Failed;
await this.downloadAvailableResults(queryItem, credentials, executionEndTime);
// Workflow was cancelled on the server
this.remoteQueryStatusUpdateEventEmitter.fire({ queryId, status: QueryStatus.Failed, failureReason: 'Cancelled' });
await this.downloadAvailableResults(queryId, remoteQuery, credentials, executionEndTime);
void showAndLogInformationMessage('Variant analysis was cancelled');
} else {
queryItem.failureReason = queryWorkflowResult.error;
queryItem.status = QueryStatus.Failed;
this.remoteQueryStatusUpdateEventEmitter.fire({ queryId, status: QueryStatus.Failed, failureReason: queryWorkflowResult.error });
void showAndLogErrorMessage(`Variant analysis execution failed. Error: ${queryWorkflowResult.error}`);
}
} else if (queryWorkflowResult.status === 'Cancelled') {
queryItem.failureReason = 'Cancelled';
queryItem.status = QueryStatus.Failed;
await this.downloadAvailableResults(queryItem, credentials, executionEndTime);
this.remoteQueryStatusUpdateEventEmitter.fire({ queryId, status: QueryStatus.Failed, failureReason: 'Cancelled' });
await this.downloadAvailableResults(queryId, remoteQuery, credentials, executionEndTime);
void showAndLogInformationMessage('Variant analysis was cancelled');
} else if (queryWorkflowResult.status === 'InProgress') {
// Should not get here. Only including this to ensure `assertNever` uses proper type checking.
@@ -157,7 +166,6 @@ export class RemoteQueriesManager extends DisposableObject {
// Ensure all cases are covered
assertNever(queryWorkflowResult.status);
}
await this.qhm.refreshTreeView();
}
public async autoDownloadRemoteQueryResults(
@@ -238,7 +246,6 @@ export class RemoteQueriesManager extends DisposableObject {
*/
private createQueryId(queryName: string): string {
return `${queryName}-${nanoid()}`;
}
/**
@@ -247,29 +254,28 @@ export class RemoteQueriesManager extends DisposableObject {
* used by the query history manager to determine when the directory
* should be deleted.
*
* @param queryName The name of the query that was run.
*/
private async prepareStorageDirectory(queryHistoryItem: RemoteQueryHistoryItem): Promise<void> {
await createTimestampFile(path.join(this.storagePath, queryHistoryItem.queryId));
private async prepareStorageDirectory(queryId: string): Promise<void> {
await createTimestampFile(path.join(this.storagePath, queryId));
}
private async storeJsonFile<T>(queryHistoryItem: RemoteQueryHistoryItem, fileName: string, obj: T): Promise<void> {
const filePath = path.join(this.storagePath, queryHistoryItem.queryId, fileName);
private async storeJsonFile<T>(queryId: string, fileName: string, obj: T): Promise<void> {
const filePath = path.join(this.storagePath, queryId, fileName);
await fs.writeFile(filePath, JSON.stringify(obj, null, 2), 'utf8');
}
private async retrieveJsonFile<T>(queryHistoryItem: RemoteQueryHistoryItem, fileName: string): Promise<T> {
const filePath = path.join(this.storagePath, queryHistoryItem.queryId, fileName);
private async retrieveJsonFile<T>(queryId: string, fileName: string): Promise<T> {
const filePath = path.join(this.storagePath, queryId, fileName);
return JSON.parse(await fs.readFile(filePath, 'utf8'));
}
private async removeStorageDirectory(queryItem: RemoteQueryHistoryItem): Promise<void> {
const filePath = path.join(this.storagePath, queryItem.queryId);
private async removeStorageDirectory(queryId: string): Promise<void> {
const filePath = path.join(this.storagePath, queryId);
await fs.remove(filePath);
}
private async queryHistoryItemExists(queryItem: RemoteQueryHistoryItem): Promise<boolean> {
const filePath = path.join(this.storagePath, queryItem.queryId);
private async queryRecordExists(queryId: string): Promise<boolean> {
const filePath = path.join(this.storagePath, queryId);
return await fs.pathExists(filePath);
}
@@ -278,37 +284,36 @@ export class RemoteQueriesManager extends DisposableObject {
* If so, set the query status to `Completed` and auto-download the results.
*/
private async downloadAvailableResults(
queryItem: RemoteQueryHistoryItem,
queryId: string,
remoteQuery: RemoteQuery,
credentials: Credentials,
executionEndTime: number
): Promise<void> {
const resultIndex = await getRemoteQueryIndex(credentials, queryItem.remoteQuery);
const resultIndex = await getRemoteQueryIndex(credentials, remoteQuery);
if (resultIndex) {
queryItem.completed = true;
queryItem.status = QueryStatus.Completed;
queryItem.failureReason = undefined;
this.remoteQueryStatusUpdateEventEmitter.fire({ queryId, status: QueryStatus.Completed });
const metadata = await this.getRepositoriesMetadata(resultIndex, credentials);
const queryResult = this.mapQueryResult(executionEndTime, resultIndex, queryItem.queryId, metadata);
const queryResult = this.mapQueryResult(executionEndTime, resultIndex, queryId, metadata);
await this.storeJsonFile(queryItem, 'query-result.json', queryResult);
await this.storeJsonFile(queryId, 'query-result.json', queryResult);
// Kick off auto-download of results in the background.
void commands.executeCommand('codeQL.autoDownloadRemoteQueryResults', queryResult);
// Ask if the user wants to open the results in the background.
void this.askToOpenResults(queryItem.remoteQuery, queryResult).then(
void this.askToOpenResults(remoteQuery, queryResult).then(
noop,
err => {
void showAndLogErrorMessage(err);
}
);
} else {
const controllerRepo = `${queryItem.remoteQuery.controllerRepository.owner}/${queryItem.remoteQuery.controllerRepository.name}`;
const workflowRunUrl = `https://github.com/${controllerRepo}/actions/runs/${queryItem.remoteQuery.actionsWorkflowRunId}`;
const controllerRepo = `${remoteQuery.controllerRepository.owner}/${remoteQuery.controllerRepository.name}`;
const workflowRunUrl = `https://github.com/${controllerRepo}/actions/runs/${remoteQuery.actionsWorkflowRunId}`;
void showAndLogErrorMessage(
`There was an issue retrieving the result for the query [${queryItem.remoteQuery.queryName}](${workflowRunUrl}).`
`There was an issue retrieving the result for the query [${remoteQuery.queryName}](${workflowRunUrl}).`
);
queryItem.status = QueryStatus.Failed;
this.remoteQueryStatusUpdateEventEmitter.fire({ queryId, status: QueryStatus.Failed });
}
}

View File

@@ -18,6 +18,8 @@ import { ONE_DAY_IN_MS, ONE_HOUR_IN_MS, TWO_HOURS_IN_MS, THREE_HOURS_IN_MS } fro
import { tmpDir } from '../../helpers';
import { getErrorMessage } from '../../pure/helpers-pure';
import { HistoryItemLabelProvider } from '../../history-item-label-provider';
import { RemoteQueriesManager } from '../../remote-queries/remote-queries-manager';
import { InterfaceManager } from '../../interface';
describe('query-history', () => {
const mockExtensionLocation = path.join(tmpDir.name, 'mock-extension-location');
@@ -27,9 +29,11 @@ describe('query-history', () => {
let executeCommandSpy: sinon.SinonStub;
let showQuickPickSpy: sinon.SinonStub;
let queryHistoryManager: QueryHistoryManager | undefined;
let selectedCallback: sinon.SinonStub;
let doCompareCallback: sinon.SinonStub;
let localQueriesInterfaceManagerStub: InterfaceManager;
let remoteQueriesManagerStub: RemoteQueriesManager;
let tryOpenExternalFile: Function;
let sandbox: sinon.SinonSandbox;
@@ -49,8 +53,15 @@ describe('query-history', () => {
sandbox.stub(logger, 'log');
tryOpenExternalFile = (QueryHistoryManager.prototype as any).tryOpenExternalFile;
configListener = new QueryHistoryConfigListener();
selectedCallback = sandbox.stub();
doCompareCallback = sandbox.stub();
localQueriesInterfaceManagerStub = {
showResults: sandbox.stub()
} as any as InterfaceManager;
remoteQueriesManagerStub = {
onRemoteQueryAdded: sandbox.stub(),
onRemoteQueryRemoved: sandbox.stub(),
onRemoteQueryStatusUpdate: sandbox.stub()
} as any as RemoteQueriesManager;
});
afterEach(async () => {
@@ -190,22 +201,28 @@ 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(localQueriesInterfaceManagerStub.showResults).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(localQueriesInterfaceManagerStub.showResults).not.to.have.been.called;
expect(queryHistoryManager.treeDataProvider.getCurrent()).to.be.undefined;
});
it('should do nothing if there is no selection', async () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
await queryHistoryManager.handleItemClicked(undefined!, []);
expect(selectedCallback).not.to.have.been.called;
expect(localQueriesInterfaceManagerStub.showResults).not.to.have.been.called;
expect(queryHistoryManager.treeDataProvider.getCurrent()).to.be.undefined;
});
});
@@ -234,7 +251,7 @@ describe('query-history', () => {
expect(queryHistoryManager.treeDataProvider.allHistory).not.to.contain(toDelete);
// the same item should be selected
expect(selectedCallback).to.have.been.calledOnceWith(selected);
expect(localQueriesInterfaceManagerStub.showResults).to.have.been.calledOnceWith(selected);
});
it('should remove an item and select a new one', async () => {
@@ -254,7 +271,7 @@ describe('query-history', () => {
expect(queryHistoryManager.treeDataProvider.allHistory).not.to.contain(toDelete);
// the current item should have been selected
expect(selectedCallback).to.have.been.calledOnceWith(newSelected);
expect(localQueriesInterfaceManagerStub.showResults).to.have.been.calledOnceWith(newSelected);
});
describe('Compare callback', () => {
@@ -776,6 +793,8 @@ describe('query-history', () => {
const qhm = new QueryHistoryManager(
{} as QueryServerClient,
{} as DatabaseManager,
localQueriesInterfaceManagerStub,
remoteQueriesManagerStub,
'xxx',
{
globalStorageUri: vscode.Uri.file(mockExtensionLocation),
@@ -785,7 +804,6 @@ describe('query-history', () => {
new HistoryItemLabelProvider({} as QueryHistoryConfig),
doCompareCallback
);
qhm.onWillOpenQueryItem(selectedCallback);
(qhm.treeDataProvider as any).history = [...allHistory];
await vscode.workspace.saveAll();
await qhm.refreshTreeView();

View File

@@ -17,6 +17,8 @@ import { testDisposeHandler } from '../../test-dispose-handler';
import { walkDirectory } from '../../../helpers';
import { getErrorMessage } from '../../../pure/helpers-pure';
import { HistoryItemLabelProvider } from '../../../history-item-label-provider';
import { RemoteQueriesManager } from '../../../remote-queries/remote-queries-manager';
import { InterfaceManager } from '../../../interface';
/**
* Tests for remote queries and how they interact with the query history manager.
@@ -30,6 +32,8 @@ describe('Remote queries and query history manager', function() {
let sandbox: sinon.SinonSandbox;
let qhm: QueryHistoryManager;
let localQueriesInterfaceManagerStub: InterfaceManager;
let remoteQueriesManagerStub: RemoteQueriesManager;
let rawQueryHistory: any;
let remoteQueryResult0: RemoteQueryResult;
let remoteQueryResult1: RemoteQueryResult;
@@ -37,6 +41,10 @@ describe('Remote queries and query history manager', function() {
let showTextDocumentSpy: sinon.SinonSpy;
let openTextDocumentSpy: sinon.SinonSpy;
let rehydrateRemoteQueryStub: sinon.SinonStub;
let removeRemoteQueryStub: sinon.SinonStub;
let openRemoteQueryResultsStub: sinon.SinonStub;
beforeEach(async function() {
// set a higher timeout since recursive delete below may take a while, expecially on Windows.
@@ -45,6 +53,25 @@ describe('Remote queries and query history manager', function() {
// Since these tests change the state of the query history manager, we need to copy the original
// to a temporary folder where we can manipulate it for tests
await copyHistoryState();
sandbox = sinon.createSandbox();
localQueriesInterfaceManagerStub = {
showResults: sandbox.stub()
} as any as InterfaceManager;
rehydrateRemoteQueryStub = sandbox.stub();
removeRemoteQueryStub = sandbox.stub();
openRemoteQueryResultsStub = sandbox.stub();
remoteQueriesManagerStub = {
onRemoteQueryAdded: sandbox.stub(),
onRemoteQueryRemoved: sandbox.stub(),
onRemoteQueryStatusUpdate: sandbox.stub(),
rehydrateRemoteQuery: rehydrateRemoteQueryStub,
removeRemoteQuery: removeRemoteQueryStub,
openRemoteQueryResults: openRemoteQueryResultsStub
} as any as RemoteQueriesManager;
});
afterEach(function() {
@@ -64,6 +91,8 @@ describe('Remote queries and query history manager', function() {
qhm = new QueryHistoryManager(
{} as QueryServerClient,
{} as DatabaseManager,
localQueriesInterfaceManagerStub,
remoteQueriesManagerStub,
STORAGE_DIR,
{
globalStorageUri: Uri.file(STORAGE_DIR),
@@ -87,14 +116,12 @@ describe('Remote queries and query history manager', function() {
});
it('should read query history', async () => {
const spy = sandbox.spy();
disposables.push(qhm.onDidAddQueryItem(spy));
await qhm.readQueryHistory();
// Should have added the query history. Contents are directly from the file
expect(spy.getCall(0).args[0]).to.deep.eq(rawQueryHistory[0]);
expect(spy.getCall(1).args[0]).to.deep.eq(rawQueryHistory[1]);
expect(spy.callCount).to.eq(2);
expect(rehydrateRemoteQueryStub).to.have.callCount(2);
expect(rehydrateRemoteQueryStub.getCall(0).args[1]).to.deep.eq(rawQueryHistory[0].remoteQuery);
expect(rehydrateRemoteQueryStub.getCall(1).args[1]).to.deep.eq(rawQueryHistory[1].remoteQuery);
expect(qhm.treeDataProvider.allHistory[0]).to.deep.eq(rawQueryHistory[0]);
expect(qhm.treeDataProvider.allHistory[1]).to.deep.eq(rawQueryHistory[1]);
@@ -103,40 +130,35 @@ describe('Remote queries and query history manager', function() {
it('should remove and then add query from history', async () => {
await qhm.readQueryHistory();
const addSpy = sandbox.spy();
disposables.push(qhm.onDidAddQueryItem(addSpy));
const removeSpy = sandbox.spy();
disposables.push(qhm.onDidRemoveQueryItem(removeSpy));
// Remove the first query
await qhm.handleRemoveHistoryItem(qhm.treeDataProvider.allHistory[0]);
expect(removeSpy.getCall(0).args[0]).to.deep.eq(rawQueryHistory[0]);
expect(removeSpy.callCount).to.eq(1);
expect(addSpy.callCount).to.eq(0);
expect(removeRemoteQueryStub).calledOnceWithExactly(rawQueryHistory[0].queryId);
expect(rehydrateRemoteQueryStub).to.have.callCount(2);
expect(rehydrateRemoteQueryStub.getCall(0).args[1]).to.deep.eq(rawQueryHistory[0].remoteQuery);
expect(rehydrateRemoteQueryStub.getCall(1).args[1]).to.deep.eq(rawQueryHistory[1].remoteQuery);
expect(openRemoteQueryResultsStub).calledOnceWithExactly(rawQueryHistory[1].queryId);
expect(qhm.treeDataProvider.allHistory).to.deep.eq(rawQueryHistory.slice(1));
// Add it back
qhm.addQuery(rawQueryHistory[0]);
expect(removeSpy.callCount).to.eq(1);
expect(addSpy.getCall(0).args[0]).to.deep.eq(rawQueryHistory[0]);
expect(addSpy.callCount).to.eq(1);
expect(removeRemoteQueryStub).to.have.callCount(1);
expect(rehydrateRemoteQueryStub).to.have.callCount(2);
expect(qhm.treeDataProvider.allHistory).to.deep.eq([rawQueryHistory[1], rawQueryHistory[0]]);
});
it('should remove two queries from history', async () => {
await qhm.readQueryHistory();
const addSpy = sandbox.spy();
disposables.push(qhm.onDidAddQueryItem(addSpy));
const removeSpy = sandbox.spy();
disposables.push(qhm.onDidRemoveQueryItem(removeSpy));
// Remove the both queries
// Just for fun, let's do it in reverse order
await qhm.handleRemoveHistoryItem(undefined!, [qhm.treeDataProvider.allHistory[1], qhm.treeDataProvider.allHistory[0]]);
expect(removeSpy.getCall(0).args[0]).to.deep.eq(rawQueryHistory[1]);
expect(removeSpy.getCall(1).args[0]).to.deep.eq(rawQueryHistory[0]);
expect(removeRemoteQueryStub.callCount).to.eq(2);
expect(removeRemoteQueryStub.getCall(0).args[0]).to.eq(rawQueryHistory[1].queryId);
expect(removeRemoteQueryStub.getCall(1).args[0]).to.eq(rawQueryHistory[0].queryId);
expect(qhm.treeDataProvider.allHistory).to.deep.eq([]);
expect(removeSpy.callCount).to.eq(2);
// also, both queries should be removed from on disk storage
expect(fs.readJSONSync(path.join(STORAGE_DIR, 'workspace-query-history.json'))).to.deep.eq({
@@ -147,11 +169,9 @@ describe('Remote queries and query history manager', function() {
it('should handle a click', async () => {
await qhm.readQueryHistory();
const openSpy = sandbox.spy();
disposables.push(qhm.onWillOpenQueryItem(openSpy));
await qhm.handleItemClicked(qhm.treeDataProvider.allHistory[0], []);
expect(openSpy.getCall(0).args[0]).to.deep.eq(rawQueryHistory[0]);
expect(openRemoteQueryResultsStub).calledOnceWithExactly(rawQueryHistory[0].queryId);
});
it('should get the query text', async () => {