Refactor: Invert dependency between query history and remote quries managers (#1396)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user