Skip to content

Commit aa270e5

Browse files
committed
Refactor exportCsvResults and create test
1. `exportCsvResults` now no longer requires an `onFinish` callback. 2. The test adds a generic framework for creating a mock cli server. This should be used in future tests.
1 parent fe7eb07 commit aa270e5

3 files changed

Lines changed: 92 additions & 18 deletions

File tree

extensions/ql-vscode/src/query-history.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -956,11 +956,11 @@ export class QueryHistoryManager extends DisposableObject {
956956
void this.tryOpenExternalFile(query.csvPath);
957957
return;
958958
}
959-
await query.exportCsvResults(this.qs, query.csvPath, () => {
959+
if (await query.exportCsvResults(this.qs, query.csvPath)) {
960960
void this.tryOpenExternalFile(
961961
query.csvPath
962962
);
963-
});
963+
}
964964
}
965965

966966
async handleViewCsvAlerts(

extensions/ql-vscode/src/run-queries.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -341,24 +341,33 @@ export class QueryEvaluationInfo {
341341
/**
342342
* Creates the CSV file containing the results of this query. This will only be called if the query
343343
* does not have interpreted results and the CSV file does not already exist.
344+
*
345+
* @return Promise<true> if the operation creates the file. Promise<false> if the operation does
346+
* not create the file.
347+
*
348+
* @throws Error if the operation fails.
344349
*/
345-
async exportCsvResults(qs: qsClient.QueryServerClient, csvPath: string, onFinish: () => void): Promise<void> {
346-
let stopDecoding = false;
347-
const out = fs.createWriteStream(csvPath);
348-
out.on('finish', onFinish);
349-
out.on('error', () => {
350-
if (!stopDecoding) {
351-
stopDecoding = true;
352-
void showAndLogErrorMessage(`Failed to write CSV results to ${csvPath}`);
353-
}
354-
});
350+
async exportCsvResults(qs: qsClient.QueryServerClient, csvPath: string): Promise<boolean> {
355351
const resultSet = await this.chooseResultSet(qs);
356352
if (!resultSet) {
357353
void showAndLogWarningMessage('Query has no result set.');
358-
return;
354+
return false;
359355
}
356+
let stopDecoding = false;
357+
const out = fs.createWriteStream(csvPath);
358+
359+
const promise: Promise<boolean> = new Promise((resolve, reject) => {
360+
out.on('finish', () => resolve(true));
361+
out.on('error', () => {
362+
if (!stopDecoding) {
363+
stopDecoding = true;
364+
reject(new Error(`Failed to write CSV results to ${csvPath}`));
365+
}
366+
});
367+
});
368+
360369
let nextOffset: number | undefined = 0;
361-
while (nextOffset !== undefined && !stopDecoding) {
370+
do {
362371
const chunk: DecodedBqrsChunk = await qs.cliServer.bqrsDecode(this.resultsPaths.resultsPath, resultSet, {
363372
pageSize: 100,
364373
offset: nextOffset,
@@ -370,8 +379,10 @@ export class QueryEvaluationInfo {
370379
}).join(',') + '\n');
371380
});
372381
nextOffset = chunk.next;
373-
}
382+
} while (nextOffset && !stopDecoding);
374383
out.end();
384+
385+
return promise;
375386
}
376387

377388
/**

extensions/ql-vscode/src/vscode-tests/no-workspace/run-queries.test.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { expect } from 'chai';
22
import * as path from 'path';
3+
import * as fs from 'fs-extra';
34
import * as sinon from 'sinon';
45
import { Uri } from 'vscode';
56

67
import { QueryEvaluationInfo } from '../../run-queries';
78
import { Severity, compileQuery } from '../../pure/messages';
89
import * as config from '../../config';
10+
import { tmpDir } from '../../helpers';
11+
import { QueryServerClient } from '../../queryserver-client';
12+
import { CodeQLCliServer } from '../../cli';
13+
import { SELECT_QUERY_NAME } from '../../contextual/locationFinder';
914

1015
describe('run-queries', () => {
1116
let sandbox: sinon.SinonSandbox;
@@ -53,6 +58,51 @@ describe('run-queries', () => {
5358
expect(info.canHaveInterpretedResults()).to.eq(true);
5459
});
5560

61+
[SELECT_QUERY_NAME, 'other'].forEach(resultSetName => {
62+
it(`should export csv results for result set ${resultSetName}`, async () => {
63+
const csvLocation = path.join(tmpDir.name, 'test.csv');
64+
const qs = createMockQueryServerClient(
65+
createMockCliServer({
66+
bqrsInfo: [{ 'result-sets': [{ name: resultSetName }, { name: 'hucairz' }] }],
67+
bqrsDecode: [{
68+
columns: [{ kind: 'NotString' }, { kind: 'String' }],
69+
tuples: [['a', 'b'], ['c', 'd']],
70+
next: 1
71+
}, {
72+
// just for fun, give a different set of columns here
73+
// this won't happen with the real CLI, but it's a good test
74+
columns: [{ kind: 'String' }, { kind: 'NotString' }, { kind: 'StillNotString' }],
75+
tuples: [['a', 'b', 'c']]
76+
}]
77+
})
78+
);
79+
const info = createMockQueryInfo();
80+
const promise = info.exportCsvResults(qs, csvLocation);
81+
82+
const result = await promise;
83+
expect(result).to.eq(true);
84+
85+
const csv = fs.readFileSync(csvLocation, 'utf8');
86+
expect(csv).to.eq('a,"b"\nc,"d"\n"a",b,c\n');
87+
88+
// now verify that we are using the expected result set
89+
expect((qs.cliServer.bqrsDecode as sinon.SinonStub).callCount).to.eq(2);
90+
expect((qs.cliServer.bqrsDecode as sinon.SinonStub).getCall(0).args[1]).to.eq(resultSetName);
91+
});
92+
});
93+
94+
it('should handle csv exports for a query with no result sets', async () => {
95+
const csvLocation = path.join(tmpDir.name, 'test.csv');
96+
const qs = createMockQueryServerClient(
97+
createMockCliServer({
98+
bqrsInfo: [{ 'result-sets': [] }]
99+
})
100+
);
101+
const info = createMockQueryInfo();
102+
const result = await info.exportCsvResults(qs, csvLocation);
103+
expect(result).to.eq(false);
104+
});
105+
56106
describe('compile', () => {
57107
it('should compile', async () => {
58108
const info = createMockQueryInfo();
@@ -116,7 +166,7 @@ describe('run-queries', () => {
116166
);
117167
}
118168

119-
function createMockQueryServerClient() {
169+
function createMockQueryServerClient(cliServer?: CodeQLCliServer): QueryServerClient {
120170
return {
121171
config: {
122172
timeoutSecs: 5
@@ -131,7 +181,20 @@ describe('run-queries', () => {
131181
})),
132182
logger: {
133183
log: sandbox.spy()
134-
}
135-
};
184+
},
185+
cliServer
186+
} as unknown as QueryServerClient;
187+
}
188+
189+
function createMockCliServer(mockOperations: Record<string, any[]>): CodeQLCliServer {
190+
const mockServer: Record<string, any> = {};
191+
for (const [operation, returns] of Object.entries(mockOperations)) {
192+
mockServer[operation] = sandbox.stub();
193+
returns.forEach((returnValue, i) => {
194+
mockServer[operation].onCall(i).resolves(returnValue);
195+
});
196+
}
197+
198+
return mockServer as unknown as CodeQLCliServer;
136199
}
137200
});

0 commit comments

Comments
 (0)