Add unit tests for query-results.ts

This commit is contained in:
Andrew Eisenberg
2020-09-28 23:10:40 -07:00
parent 72160a24bd
commit 3e3a31d5e2
4 changed files with 308 additions and 48 deletions

View File

@@ -2,10 +2,15 @@ import * as fs from 'fs-extra';
import * as glob from 'glob-promise';
import * as yaml from 'js-yaml';
import * as path from 'path';
import { CancellationToken, ExtensionContext, ProgressOptions, window as Window, workspace } from 'vscode';
import {
CancellationToken,
ExtensionContext,
ProgressOptions,
window as Window,
workspace
} from 'vscode';
import { CodeQLCliServer } from './cli';
import { logger } from './logging';
import { QueryInfo } from './run-queries';
export interface ProgressUpdate {
/**
@@ -145,24 +150,6 @@ export function getOnDiskWorkspaceFolders() {
return diskWorkspaceFolders;
}
/**
* Gets a human-readable name for an evaluated query.
* Uses metadata if it exists, and defaults to the query file name.
*/
export function getQueryName(query: QueryInfo) {
// Queries run through quick evaluation are not usually the entire query file.
// Label them differently and include the line numbers.
if (query.quickEvalPosition !== undefined) {
const { line, endLine, fileName } = query.quickEvalPosition;
const lineInfo = line === endLine ? `${line}` : `${line}-${endLine}`;
return `Quick evaluation of ${path.basename(fileName)}:${lineInfo}`;
} else if (query.metadata && query.metadata.name) {
return query.metadata.name;
} else {
return path.basename(query.program.queryPath);
}
}
/**
* Provides a utility method to invoke a function only if a minimum time interval has elapsed since
* the last invocation of that function.

View File

@@ -365,14 +365,16 @@ export class InterfaceManager extends DisposableObject {
// Use sorted results path if it exists. This may happen if we are
// reloading the results view after it has been sorted in the past.
const resultsPath = results.sortedResultsInfo.get(selectedTable)?.resultsPath
|| results.query.resultsPaths.resultsPath;
const resultsPath = results.getResultsPath(selectedTable);
const chunk = await this.cliServer.bqrsDecode(
resultsPath,
schema.name,
{
// always use the first page.
// Always send the first page.
// It may not wind up being the page we actually show,
// if there are interpreted results, but speculatively
// send anyway.
offset: schema.pagination?.offsets[0],
pageSize: RAW_RESULTS_PAGE_SIZE
}
@@ -433,8 +435,7 @@ export class InterfaceManager extends DisposableObject {
}
private async getResultSetSchemas(results: CompletedQuery, selectedTable = ''): Promise<ResultSetSchema[]> {
const resultsPath = results.sortedResultsInfo.get(selectedTable)?.resultsPath
|| results.query.resultsPaths.resultsPath;
const resultsPath = results.getResultsPath(selectedTable);
const schemas = await this.cliServer.bqrsInfo(
resultsPath,
RAW_RESULTS_PAGE_SIZE
@@ -470,21 +471,8 @@ export class InterfaceManager extends DisposableObject {
if (schema === undefined)
throw new Error(`Query result set '${selectedTable}' not found.`);
const getResultsPath = () => {
if (sorted) {
const resultsPath = results.sortedResultsInfo.get(selectedTable)?.resultsPath;
if (resultsPath === undefined) {
throw new Error(`Can't find sorted results for table ${selectedTable}`);
}
return resultsPath;
}
else {
return results.query.resultsPaths.resultsPath;
}
};
const chunk = await this.cliServer.bqrsDecode(
getResultsPath(),
results.getResultsPath(selectedTable, sorted),
schema.name,
{
offset: schema.pagination?.offsets[pageNumber],

View File

@@ -2,7 +2,6 @@ import { env } from 'vscode';
import { QueryWithResults, tmpDir, QueryInfo } from './run-queries';
import * as messages from './messages';
import * as helpers from './helpers';
import * as cli from './cli';
import * as sarif from 'sarif';
import * as fs from 'fs-extra';
@@ -53,7 +52,7 @@ export class CompletedQuery implements QueryWithResults {
return this.database.name;
}
get queryName(): string {
return helpers.getQueryName(this.query);
return getQueryName(this.query);
}
get statusString(): string {
@@ -72,6 +71,13 @@ export class CompletedQuery implements QueryWithResults {
}
}
getResultsPath(selectedTable: string, useSorted = true): string {
if (!useSorted) {
return this.query.resultsPaths.resultsPath;
}
return this.sortedResultsInfo.get(selectedTable)?.resultsPath
|| this.query.resultsPaths.resultsPath;
}
interpolate(template: string): string {
const { databaseName, queryName, time, statusString } = this;
@@ -89,9 +95,8 @@ export class CompletedQuery implements QueryWithResults {
}
getLabel(): string {
if (this.options.label !== undefined)
return this.options.label;
return this.config.format;
return this.options?.label
|| this.config.format;
}
get didRunSuccessfully(): boolean {
@@ -102,7 +107,11 @@ export class CompletedQuery implements QueryWithResults {
return this.interpolate(this.getLabel());
}
async updateSortState(server: cli.CodeQLCliServer, resultSetName: string, sortState: RawResultsSortState | undefined): Promise<void> {
async updateSortState(
server: cli.CodeQLCliServer,
resultSetName: string,
sortState?: RawResultsSortState
): Promise<void> {
if (sortState === undefined) {
this.sortedResultsInfo.delete(resultSetName);
return;
@@ -113,19 +122,50 @@ export class CompletedQuery implements QueryWithResults {
sortState
};
await server.sortBqrs(this.query.resultsPaths.resultsPath, sortedResultSetInfo.resultsPath, resultSetName, [sortState.columnIndex], [sortState.sortDirection]);
await server.sortBqrs(
this.query.resultsPaths.resultsPath,
sortedResultSetInfo.resultsPath,
resultSetName,
[sortState.columnIndex],
[sortState.sortDirection]
);
this.sortedResultsInfo.set(resultSetName, sortedResultSetInfo);
}
async updateInterpretedSortState(sortState: InterpretedResultsSortState | undefined): Promise<void> {
async updateInterpretedSortState(sortState?: InterpretedResultsSortState): Promise<void> {
this.interpretedResultsSortState = sortState;
}
}
/**
* Gets a human-readable name for an evaluated query.
* Uses metadata if it exists, and defaults to the query file name.
*/
export function getQueryName(query: QueryInfo) {
// Queries run through quick evaluation are not usually the entire query file.
// Label them differently and include the line numbers.
if (query.quickEvalPosition !== undefined) {
const { line, endLine, fileName } = query.quickEvalPosition;
const lineInfo = line === endLine ? `${line}` : `${line}-${endLine}`;
return `Quick evaluation of ${path.basename(fileName)}:${lineInfo}`;
} else if (query.metadata?.name) {
return query.metadata.name;
} else {
return path.basename(query.program.queryPath);
}
}
/**
* Call cli command to interpret results.
*/
export async function interpretResults(server: cli.CodeQLCliServer, metadata: QueryMetadata | undefined, resultsPaths: ResultsPaths, sourceInfo?: cli.SourceInfo): Promise<sarif.Log> {
export async function interpretResults(
server: cli.CodeQLCliServer,
metadata: QueryMetadata | undefined,
resultsPaths: ResultsPaths,
sourceInfo?: cli.SourceInfo
): Promise<sarif.Log> {
const { resultsPath, interpretedResultsPath } = resultsPaths;
if (await fs.pathExists(interpretedResultsPath)) {
return JSON.parse(await fs.readFile(interpretedResultsPath, 'utf8'));

View File

@@ -0,0 +1,245 @@
import * as chai from 'chai';
import * as path from 'path';
import * as fs from 'fs-extra';
import 'mocha';
import 'sinon-chai';
import * as Sinon from 'sinon';
import * as chaiAsPromised from 'chai-as-promised';
import { CompletedQuery, interpretResults } from '../../query-results';
import { QueryInfo, QueryWithResults, tmpDir } from '../../run-queries';
import { QueryHistoryConfig } from '../../config';
import { EvaluationResult, QueryResultType } from '../../messages';
import { SortDirection, SortedResultSetInfo } from '../../interface-types';
import { CodeQLCliServer, SourceInfo } from '../../cli';
chai.use(chaiAsPromised);
const expect = chai.expect;
describe('CompletedQuery', () => {
let disposeSpy: Sinon.SinonSpy;
let onDidChangeQueryHistoryConfigurationSpy: Sinon.SinonSpy;
beforeEach(() => {
disposeSpy = Sinon.spy();
onDidChangeQueryHistoryConfigurationSpy = Sinon.spy();
});
it('should construct a CompletedQuery', () => {
const completedQuery = mockCompletedQuery();
expect(completedQuery.logFileLocation).to.eq('mno');
expect(completedQuery.databaseName).to.eq('def');
});
it('should get the query name', () => {
const completedQuery = mockCompletedQuery();
// from the query path
expect(completedQuery.queryName).to.eq('stu');
// from the metadata
(completedQuery.query as any).metadata = {
name: 'vwx'
};
expect(completedQuery.queryName).to.eq('vwx');
// from quick eval position
(completedQuery.query as any).quickEvalPosition = {
line: 1,
endLine: 2,
fileName: '/home/users/yz'
};
expect(completedQuery.queryName).to.eq('Quick evaluation of yz:1-2');
(completedQuery.query as any).quickEvalPosition.endLine = 1;
expect(completedQuery.queryName).to.eq('Quick evaluation of yz:1');
});
it('should get the label', () => {
const completedQuery = mockCompletedQuery();
expect(completedQuery.getLabel()).to.eq('ghi');
completedQuery.options.label = '';
expect(completedQuery.getLabel()).to.eq('pqr');
});
it('should get the getResultsPath', () => {
const completedQuery = mockCompletedQuery();
// from results path
expect(completedQuery.getResultsPath('zxa', false)).to.eq('axa');
completedQuery.sortedResultsInfo.set('zxa', {
resultsPath: 'bxa'
} as SortedResultSetInfo);
// still from results path
expect(completedQuery.getResultsPath('zxa', false)).to.eq('axa');
// from sortedResultsInfo
expect(completedQuery.getResultsPath('zxa')).to.eq('bxa');
});
it('should get the statusString', () => {
const completedQuery = mockCompletedQuery();
expect(completedQuery.statusString).to.eq('failed');
completedQuery.result.message = 'Tremendously';
expect(completedQuery.statusString).to.eq('failed: Tremendously');
completedQuery.result.resultType = QueryResultType.OTHER_ERROR;
expect(completedQuery.statusString).to.eq('failed: Tremendously');
completedQuery.result.resultType = QueryResultType.CANCELLATION;
completedQuery.result.evaluationTime = 2000;
expect(completedQuery.statusString).to.eq('cancelled after 2 seconds');
completedQuery.result.resultType = QueryResultType.OOM;
expect(completedQuery.statusString).to.eq('out of memory');
completedQuery.result.resultType = QueryResultType.SUCCESS;
expect(completedQuery.statusString).to.eq('finished in 2 seconds');
completedQuery.result.resultType = QueryResultType.TIMEOUT;
expect(completedQuery.statusString).to.eq('timed out after 2 seconds');
});
it('should updateSortState', async () => {
const completedQuery = mockCompletedQuery();
const spy = Sinon.spy();
const mockServer = {
sortBqrs: spy
} as unknown as CodeQLCliServer;
const sortState = {
columnIndex: 1,
sortDirection: SortDirection.desc
};
await completedQuery.updateSortState(mockServer, 'result-name', sortState);
const expectedPath = path.join(tmpDir.name, 'sortedResults111-result-name.bqrs');
expect(spy).to.have.been.calledWith(
'axa',
expectedPath,
'result-name',
[sortState.columnIndex],
[sortState.sortDirection],
);
expect(completedQuery.sortedResultsInfo.get('result-name')).to.deep.equal({
resultsPath: expectedPath,
sortState
});
// delete the sort stae
await completedQuery.updateSortState(mockServer, 'result-name');
expect(completedQuery.sortedResultsInfo.size).to.eq(0);
});
it('should interpolate', () => {
const completedQuery = mockCompletedQuery();
(completedQuery as any).time = '123';
expect(completedQuery.interpolate('xxx')).to.eq('xxx');
expect(completedQuery.interpolate('%t %q %d %s %%')).to.eq('123 stu def failed %');
expect(completedQuery.interpolate('%t %q %d %s %%::%t %q %d %s %%')).to.eq('123 stu def failed %::123 stu def failed %');
});
it('should interpretResults', async () => {
const spy = Sinon.mock();
spy.returns('1234');
const mockServer = {
interpretBqrs: spy
} as unknown as CodeQLCliServer;
const interpretedResultsPath = path.join(tmpDir.name, 'interpreted.json');
const resultsPath = '123';
const sourceInfo = {};
const metadata = {
kind: 'my-kind',
id: 'my-id' as string | undefined
};
const results1 = await interpretResults(
mockServer,
metadata,
{
resultsPath, interpretedResultsPath
},
sourceInfo as SourceInfo
);
expect(results1).to.eq('1234');
expect(spy).to.have.been.calledWith(
metadata,
resultsPath, interpretedResultsPath, sourceInfo
);
// Try again, but with no id
spy.reset();
spy.returns('1234');
delete metadata.id;
const results2 = await interpretResults(
mockServer,
metadata,
{
resultsPath, interpretedResultsPath
},
sourceInfo as SourceInfo
);
expect(results2).to.eq('1234');
expect(spy).to.have.been.calledWith(
{ kind: 'my-kind', id: 'dummy-id' },
resultsPath, interpretedResultsPath, sourceInfo
);
// try a third time, but this time we get from file
spy.reset();
fs.writeFileSync(interpretedResultsPath, JSON.stringify({
a: 6
}), 'utf8');
const results3 = await interpretResults(
mockServer,
metadata,
{
resultsPath, interpretedResultsPath
},
sourceInfo as SourceInfo
);
expect(results3).to.deep.eq({ a: 6 });
});
function mockCompletedQuery() {
return new CompletedQuery(
mockQueryWithResults(),
mockQueryHistoryConfig()
);
}
function mockQueryWithResults(): QueryWithResults {
return {
query: {
program: {
queryPath: 'stu'
},
resultsPaths: {
resultsPath: 'axa'
},
queryID: 111
} as never as QueryInfo,
result: {} as never as EvaluationResult,
database: {
databaseUri: 'abc',
name: 'def'
},
options: {
label: 'ghi',
queryText: 'jkl',
isQuickQuery: false
},
logFileLocation: 'mno',
dispose: disposeSpy
};
}
function mockQueryHistoryConfig(): QueryHistoryConfig {
return {
onDidChangeQueryHistoryConfiguration: onDidChangeQueryHistoryConfigurationSpy,
format: 'pqr'
};
}
});