Add unit tests for query-results.ts
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user