Skip to content

Commit 0efd029

Browse files
authored
Merge pull request #1242 from github/aeisenberg/analysis-results-on-restart
2 parents bd9776c + 67336a2 commit 0efd029

8 files changed

Lines changed: 134 additions & 19 deletions

File tree

extensions/ql-vscode/src/remote-queries/analyses-results-manager.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as fs from 'fs-extra';
12
import * as os from 'os';
23
import * as path from 'path';
34
import { CancellationToken, ExtensionContext } from 'vscode';
@@ -12,7 +13,8 @@ import { sarifParser } from '../sarif-parser';
1213
import { extractAnalysisAlerts } from './sarif-processing';
1314
import { CodeQLCliServer } from '../cli';
1415
import { extractRawResults } from './bqrs-processing';
15-
import { getErrorMessage } from '../pure/helpers-pure';
16+
import { asyncFilter, getErrorMessage } from '../pure/helpers-pure';
17+
import { createDownloadPath } from './download-link';
1618

1719
export class AnalysesResultsManager {
1820
// Store for the results of various analyses for each remote query.
@@ -44,13 +46,22 @@ export class AnalysesResultsManager {
4446
await this.downloadSingleAnalysisResults(analysisSummary, credentials, publishResults);
4547
}
4648

47-
public async downloadAnalysesResults(
48-
allAnalysesToDownload: AnalysisSummary[],
49-
token: CancellationToken | undefined,
50-
publishResults: (analysesResults: AnalysisResults[]) => Promise<void>
49+
/**
50+
* Loads the array analysis results. For each analysis results, if it is not downloaded yet,
51+
* it will be downloaded. If it is already downloaded, it will be loaded into memory.
52+
* If it is already in memory, this will be a no-op.
53+
*
54+
* @param allAnalysesToLoad List of analyses to ensure are downloaded and in memory
55+
* @param token Optional cancellation token
56+
* @param publishResults Optional function to publish the results after loading
57+
*/
58+
public async loadAnalysesResults(
59+
allAnalysesToLoad: AnalysisSummary[],
60+
token?: CancellationToken,
61+
publishResults: (analysesResults: AnalysisResults[]) => Promise<void> = () => Promise.resolve()
5162
): Promise<void> {
5263
// Filter out analyses that we have already in memory.
53-
const analysesToDownload = allAnalysesToDownload.filter(x => !this.isAnalysisInMemory(x));
64+
const analysesToDownload = allAnalysesToLoad.filter(x => !this.isAnalysisInMemory(x));
5465

5566
const credentials = await Credentials.initialize(this.ctx);
5667

@@ -151,6 +162,21 @@ export class AnalysesResultsManager {
151162
void publishResults([...resultsForQuery]);
152163
}
153164

165+
166+
public async loadDownloadedAnalyses(
167+
allAnalysesToCheck: AnalysisSummary[]
168+
) {
169+
170+
// Find all analyses that are already downloaded.
171+
const allDownloadedAnalyses = await asyncFilter(allAnalysesToCheck, x => this.isAnalysisDownloaded(x));
172+
// Now, ensure that all of these analyses are in memory. Some may already be in memory. These are ignored.
173+
await this.loadAnalysesResults(allDownloadedAnalyses);
174+
}
175+
176+
private async isAnalysisDownloaded(analysis: AnalysisSummary): Promise<boolean> {
177+
return await fs.pathExists(createDownloadPath(this.storagePath, analysis.downloadLink));
178+
}
179+
154180
private async readBqrsResults(filePath: string, fileLinkPrefix: string): Promise<AnalysisRawResults> {
155181
return await extractRawResults(this.cliServer, this.logger, filePath, fileLinkPrefix);
156182
}

extensions/ql-vscode/src/remote-queries/download-link.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as path from 'path';
2+
13
/**
24
* Represents a link to an artifact to be downloaded.
35
*/
@@ -23,3 +25,16 @@ export interface DownloadLink {
2325
*/
2426
queryId: string;
2527
}
28+
29+
/**
30+
* Converts a downloadLink to the path where the artifact should be stored.
31+
*
32+
* @param storagePath The base directory to store artifacts in.
33+
* @param downloadLink The DownloadLink
34+
* @param extension An optional file extension to append to the artifact (no `.`).
35+
*
36+
* @returns A full path to the download location of the artifact
37+
*/
38+
export function createDownloadPath(storagePath: string, downloadLink: DownloadLink, extension = '') {
39+
return path.join(storagePath, downloadLink.queryId, downloadLink.id + (extension ? `.${extension}` : ''));
40+
}

extensions/ql-vscode/src/remote-queries/gh-actions-api-client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { showAndLogWarningMessage, tmpDir } from '../helpers';
55
import { Credentials } from '../authentication';
66
import { logger } from '../logging';
77
import { RemoteQueryWorkflowResult } from './remote-query-workflow-result';
8-
import { DownloadLink } from './download-link';
8+
import { DownloadLink, createDownloadPath } from './download-link';
99
import { RemoteQuery } from './remote-query';
1010
import { RemoteQueryFailureIndexItem, RemoteQueryResultIndex, RemoteQuerySuccessIndexItem } from './remote-query-result-index';
1111

@@ -82,14 +82,14 @@ export async function downloadArtifactFromLink(
8282

8383
const octokit = await credentials.getOctokit();
8484

85-
const extractedPath = path.join(storagePath, downloadLink.queryId, downloadLink.id);
85+
const extractedPath = createDownloadPath(storagePath, downloadLink);
8686

8787
// first check if we already have the artifact
8888
if (!(await fs.pathExists(extractedPath))) {
8989
// Download the zipped artifact.
9090
const response = await octokit.request(`GET ${downloadLink.urlPath}/zip`, {});
9191

92-
const zipFilePath = path.join(storagePath, downloadLink.queryId, `${downloadLink.id}.zip`);
92+
const zipFilePath = createDownloadPath(storagePath, downloadLink, 'zip');
9393
await saveFile(`${zipFilePath}`, response.data as ArrayBuffer);
9494

9595
// Extract the zipped artifact.

extensions/ql-vscode/src/remote-queries/remote-queries-interface.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,15 @@ export class RemoteQueriesInterfaceManager {
4646
this.getPanel().reveal(undefined, true);
4747

4848
await this.waitForPanelLoaded();
49+
const model = this.buildViewModel(query, queryResult);
4950
await this.postMessage({
5051
t: 'setRemoteQueryResult',
51-
queryResult: this.buildViewModel(query, queryResult)
52+
queryResult: model
5253
});
5354

55+
// Ensure all pre-downloaded artifacts are loaded into memory
56+
await this.analysesResultsManager.loadDownloadedAnalyses(model.analysisSummaries);
57+
5458
await this.setAnalysisResults(this.analysesResultsManager.getAnalysesResults(queryResult.queryId));
5559
}
5660

@@ -213,7 +217,7 @@ export class RemoteQueriesInterfaceManager {
213217
}
214218

215219
private async downloadAllAnalysesResults(msg: RemoteQueryDownloadAllAnalysesResultsMessage): Promise<void> {
216-
await this.analysesResultsManager.downloadAnalysesResults(
220+
await this.analysesResultsManager.loadAnalysesResults(
217221
msg.analysisSummaries,
218222
undefined,
219223
results => this.setAnalysisResults(results));

extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ export class RemoteQueriesManager extends DisposableObject {
187187
fileSize: String(a.fileSizeInBytes)
188188
}));
189189

190-
await this.analysesResultsManager.downloadAnalysesResults(
190+
await this.analysesResultsManager.loadAnalysesResults(
191191
analysesToDownload,
192192
token,
193193
results => this.interfaceManager.setAnalysisResults(results));

extensions/ql-vscode/src/vscode-tests/no-workspace/data/remote-queries/queries/MRVA Integration test 1-6sBi6oaky_fxqXW2NA4bx/query-result.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@
2222
"innerFilePath": "results.sarif",
2323
"queryId": "MRVA Integration test 1-6sBi6oaky_fxqXW2NA4bx"
2424
}
25+
},
26+
{
27+
"nwo": "hucairz/i-dont-exist",
28+
"resultCount": 5,
29+
"fileSizeInBytes": 81237,
30+
"downloadLink": {
31+
"id": "999999",
32+
"urlPath": "/these/results/will/never/be/downloaded/999999",
33+
"innerFilePath": "results.sarif",
34+
"queryId": "MRVA Integration test 2-UL-vbKAjP8ffObxjsp7hN"
35+
}
2536
}
2637
],
2738
"analysisFailures": [],
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { expect } from 'chai';
2+
import 'mocha';
3+
import * as path from 'path';
4+
5+
import { DownloadLink, createDownloadPath } from '../../remote-queries/download-link';
6+
7+
describe('createDownloadPath', () => {
8+
it('should return the correct path', () => {
9+
const downloadLink: DownloadLink = {
10+
id: 'abc',
11+
urlPath: '',
12+
innerFilePath: '',
13+
queryId: 'def'
14+
};
15+
const expectedPath = path.join('storage', 'def', 'abc');
16+
17+
const actualPath = createDownloadPath('storage', downloadLink);
18+
19+
expect(actualPath).to.equal(expectedPath);
20+
});
21+
22+
it('should return the correct path with extension', () => {
23+
const downloadLink: DownloadLink = {
24+
id: 'abc',
25+
urlPath: '',
26+
innerFilePath: '',
27+
queryId: 'def'
28+
};
29+
30+
const expectedPath = path.join('storage', 'def', 'abc.zip');
31+
32+
const actualPath = createDownloadPath('storage', downloadLink, 'zip');
33+
34+
expect(actualPath).to.equal(expectedPath);
35+
});
36+
});

extensions/ql-vscode/src/vscode-tests/no-workspace/remote-query-history.test.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,8 @@ describe('Remote queries and query history manager', function() {
245245

246246
it('should download two artifacts at once', async () => {
247247
const publisher = sandbox.spy();
248-
const analysisSummaries = [...remoteQueryResult0.analysisSummaries];
249-
await arm.downloadAnalysesResults(analysisSummaries, undefined, publisher);
248+
const analysisSummaries = [remoteQueryResult0.analysisSummaries[0], remoteQueryResult0.analysisSummaries[1]];
249+
await arm.loadAnalysesResults(analysisSummaries, undefined, publisher);
250250

251251
const trimmed = publisher.getCalls().map(call => call.args[0]).map(args => {
252252
args.forEach((analysisResult: any) => delete analysisResult.interpretedResults);
@@ -287,7 +287,7 @@ describe('Remote queries and query history manager', function() {
287287
const analysisSummaries = [...remoteQueryResult0.analysisSummaries];
288288

289289
try {
290-
await arm.downloadAnalysesResults(analysisSummaries, {
290+
await arm.loadAnalysesResults(analysisSummaries, {
291291
isCancellationRequested: true
292292
} as CancellationToken, publisher);
293293
expect.fail('Should have thrown');
@@ -300,11 +300,11 @@ describe('Remote queries and query history manager', function() {
300300

301301
it('should get the analysis results', async () => {
302302
const publisher = sandbox.spy();
303-
const analysisSummaries0 = [...remoteQueryResult0.analysisSummaries];
303+
const analysisSummaries0 = [remoteQueryResult0.analysisSummaries[0], remoteQueryResult0.analysisSummaries[1]];
304304
const analysisSummaries1 = [...remoteQueryResult1.analysisSummaries];
305305

306-
await arm.downloadAnalysesResults(analysisSummaries0, undefined, publisher);
307-
await arm.downloadAnalysesResults(analysisSummaries1, undefined, publisher);
306+
await arm.loadAnalysesResults(analysisSummaries0, undefined, publisher);
307+
await arm.loadAnalysesResults(analysisSummaries1, undefined, publisher);
308308

309309
const result0 = arm.getAnalysesResults(rawQueryHistory[0].queryId);
310310
const result0Again = arm.getAnalysesResults(rawQueryHistory[0].queryId);
@@ -323,7 +323,7 @@ describe('Remote queries and query history manager', function() {
323323
it.skip('should read sarif', async () => {
324324
const publisher = sandbox.spy();
325325
const analysisSummaries0 = [remoteQueryResult0.analysisSummaries[0]];
326-
await arm.downloadAnalysesResults(analysisSummaries0, undefined, publisher);
326+
await arm.loadAnalysesResults(analysisSummaries0, undefined, publisher);
327327

328328
const sarif = fs.readJSONSync(path.join(STORAGE_DIR, 'queries', rawQueryHistory[0].queryId, '171543249', 'results.sarif'));
329329
const queryResults = sarif.runs
@@ -332,6 +332,29 @@ describe('Remote queries and query history manager', function() {
332332

333333
expect(publisher.getCall(1).args[0][0].results).to.deep.eq(queryResults);
334334
});
335+
336+
it('should check if an artifact is downloaded and not in memory', async () => {
337+
// Load remoteQueryResult0.analysisSummaries[1] into memory
338+
await arm.downloadAnalysisResults(remoteQueryResult0.analysisSummaries[1], () => Promise.resolve());
339+
340+
// on disk
341+
expect(await (arm as any).isAnalysisDownloaded(remoteQueryResult0.analysisSummaries[0])).to.be.true;
342+
343+
// in memory
344+
expect(await (arm as any).isAnalysisDownloaded(remoteQueryResult0.analysisSummaries[1])).to.be.true;
345+
346+
// not downloaded
347+
expect(await (arm as any).isAnalysisDownloaded(remoteQueryResult0.analysisSummaries[2])).to.be.false;
348+
});
349+
350+
it('should load downloaded artifacts', async () => {
351+
await arm.loadDownloadedAnalyses(remoteQueryResult0.analysisSummaries);
352+
const queryId = rawQueryHistory[0].queryId;
353+
const analysesResultsNwos = arm.getAnalysesResults(queryId).map(ar => ar.nwo).sort();
354+
expect(analysesResultsNwos[0]).to.eq('github/vscode-codeql');
355+
expect(analysesResultsNwos[1]).to.eq('other/hucairz');
356+
expect(analysesResultsNwos.length).to.eq(2);
357+
});
335358
});
336359

337360
async function copyHistoryState() {

0 commit comments

Comments
 (0)