Add a tree viewer UI for the evaluator logs (#1433)

Co-authored-by: Aditya Sharad <6874315+adityasharad@users.noreply.github.com>
This commit is contained in:
Angela P Wen
2022-07-22 12:01:39 +02:00
committed by GitHub
parent 747049ed1b
commit 833f8e06ca
14 changed files with 500 additions and 92 deletions

View File

@@ -38,6 +38,7 @@
"onView:codeQLDatabases",
"onView:codeQLQueryHistory",
"onView:codeQLAstViewer",
"onView:codeQLEvalLogViewer",
"onView:test-explorer",
"onCommand:codeQL.checkForUpdatesToCLI",
"onCommand:codeQL.authenticateToGitHub",
@@ -527,11 +528,15 @@
},
{
"command": "codeQLQueryHistory.showEvalLog",
"title": "Show Evaluator Log (Raw)"
"title": "Show Evaluator Log (Raw JSON)"
},
{
"command": "codeQLQueryHistory.showEvalLogSummary",
"title": "Show Evaluator Log (Summary)"
"title": "Show Evaluator Log (Summary Text)"
},
{
"command": "codeQLQueryHistory.showEvalLogViewer",
"title": "Show Evaluator Log (UI)"
},
{
"command": "codeQLQueryHistory.cancel",
@@ -608,6 +613,14 @@
"light": "media/light/clear-all.svg",
"dark": "media/dark/clear-all.svg"
}
},
{
"command": "codeQLEvalLogViewer.clear",
"title": "Clear Viewer",
"icon": {
"light": "media/light/clear-all.svg",
"dark": "media/dark/clear-all.svg"
}
}
],
"menus": {
@@ -681,6 +694,11 @@
"command": "codeQLAstViewer.clear",
"when": "view == codeQLAstViewer",
"group": "navigation"
},
{
"command": "codeQLEvalLogViewer.clear",
"when": "view == codeQLEvalLogViewer",
"group": "navigation"
}
],
"view/item/context": [
@@ -754,6 +772,11 @@
"group": "9_qlCommands",
"when": "codeql.supportsEvalLog && viewItem == rawResultsItem || codeql.supportsEvalLog && viewItem == interpretedResultsItem || codeql.supportsEvalLog && viewItem == cancelledResultsItem"
},
{
"command": "codeQLQueryHistory.showEvalLogViewer",
"group": "9_qlCommands",
"when": "config.codeQL.canary && codeql.supportsEvalLog && viewItem == rawResultsItem || config.codeQL.canary && codeql.supportsEvalLog && viewItem == interpretedResultsItem || config.codeQL.canary && codeql.supportsEvalLog && viewItem == cancelledResultsItem"
},
{
"command": "codeQLQueryHistory.showQueryText",
"group": "9_qlCommands",
@@ -975,6 +998,10 @@
"command": "codeQLQueryHistory.showEvalLogSummary",
"when": "false"
},
{
"command": "codeQLQueryHistory.showEvalLogViewer",
"when": "false"
},
{
"command": "codeQLQueryHistory.openQueryDirectory",
"when": "false"
@@ -1043,6 +1070,10 @@
"command": "codeQLAstViewer.clear",
"when": "false"
},
{
"command": "codeQLEvalLogViewer.clear",
"when": "false"
},
{
"command": "codeQLTests.acceptOutput",
"when": "false"
@@ -1109,6 +1140,11 @@
{
"id": "codeQLAstViewer",
"name": "AST Viewer"
},
{
"id": "codeQLEvalLogViewer",
"name": "Evaluator Log Viewer",
"when": "config.codeQL.canary"
}
]
},
@@ -1124,6 +1160,10 @@
{
"view": "codeQLDatabases",
"contents": "Add a CodeQL database:\n[From a folder](command:codeQLDatabases.chooseDatabaseFolder)\n[From an archive](command:codeQLDatabases.chooseDatabaseArchive)\n[From a URL (as a zip file)](command:codeQLDatabases.chooseDatabaseInternet)\n[From LGTM](command:codeQLDatabases.chooseDatabaseLgtm)"
},
{
"view": "codeQLEvalLogViewer",
"contents": "Run the 'Show Evaluator Log (UI)' command on a CodeQL query run in the Query History view."
}
]
},

View File

@@ -0,0 +1,67 @@
import { ChildEvalLogTreeItem, EvalLogTreeItem } from './eval-log-viewer';
import { EvalLogData as EvalLogData } from './pure/log-summary-parser';
/** Builds the tree data for the evaluator log viewer for a single query run. */
export default class EvalLogTreeBuilder {
private queryName: string;
private evalLogDataItems: EvalLogData[];
constructor(queryName: string, evaluatorLogDataItems: EvalLogData[]) {
this.queryName = queryName;
this.evalLogDataItems = evaluatorLogDataItems;
}
async getRoots(): Promise<EvalLogTreeItem[]> {
return await this.parseRoots();
}
private async parseRoots(): Promise<EvalLogTreeItem[]> {
const roots: EvalLogTreeItem[] = [];
// Once the viewer can show logs for multiple queries, there will be more than 1 item at the root
// level. For now, there will always be one root (the one query being shown).
const queryItem: EvalLogTreeItem = {
label: this.queryName,
children: [] // Will assign predicate items as children shortly.
};
// Display descriptive message when no data exists
if (this.evalLogDataItems.length === 0) {
const noResultsItem: ChildEvalLogTreeItem = {
label: 'No predicates evaluated in this query run.',
parent: queryItem,
children: [],
};
queryItem.children.push(noResultsItem);
}
// For each predicate, create a TreeItem object with appropriate parents/children
this.evalLogDataItems.forEach(logDataItem => {
const predicateLabel = `${logDataItem.predicateName} (${logDataItem.resultSize} tuples, ${logDataItem.millis} ms)`;
const predicateItem: ChildEvalLogTreeItem = {
label: predicateLabel,
parent: queryItem,
children: [] // Will assign pipeline items as children shortly.
};
for (const [pipelineName, steps] of Object.entries(logDataItem.ra)) {
const pipelineLabel = `Pipeline: ${pipelineName}`;
const pipelineItem: ChildEvalLogTreeItem = {
label: pipelineLabel,
parent: predicateItem,
children: [] // Will assign step items as children shortly.
};
predicateItem.children.push(pipelineItem);
pipelineItem.children = steps.map((step: string) => ({
label: step,
parent: pipelineItem,
children: []
}));
}
queryItem.children.push(predicateItem);
});
roots.push(queryItem);
return roots;
}
}

View File

@@ -0,0 +1,92 @@
import { window, TreeDataProvider, TreeView, TreeItem, ProviderResult, Event, EventEmitter, TreeItemCollapsibleState } from 'vscode';
import { commandRunner } from './commandRunner';
import { DisposableObject } from './pure/disposable-object';
import { showAndLogErrorMessage } from './helpers';
export interface EvalLogTreeItem {
label?: string;
children: ChildEvalLogTreeItem[];
}
export interface ChildEvalLogTreeItem extends EvalLogTreeItem {
parent: ChildEvalLogTreeItem | EvalLogTreeItem;
}
/** Provides data from parsed CodeQL evaluator logs to be rendered in a tree view. */
class EvalLogDataProvider extends DisposableObject implements TreeDataProvider<EvalLogTreeItem> {
public roots: EvalLogTreeItem[] = [];
private _onDidChangeTreeData: EventEmitter<EvalLogTreeItem | undefined | null | void> = new EventEmitter<EvalLogTreeItem | undefined | null | void>();
readonly onDidChangeTreeData: Event<EvalLogTreeItem | undefined | null | void> = this._onDidChangeTreeData.event;
refresh(): void {
this._onDidChangeTreeData.fire();
}
getTreeItem(element: EvalLogTreeItem): TreeItem | Thenable<TreeItem> {
const state = element.children.length
? TreeItemCollapsibleState.Collapsed
: TreeItemCollapsibleState.None;
const treeItem = new TreeItem(element.label || '', state);
treeItem.tooltip = `${treeItem.label} || ''}`;
return treeItem;
}
getChildren(element?: EvalLogTreeItem): ProviderResult<EvalLogTreeItem[]> {
// If no item is passed, return the root.
if (!element) {
return this.roots || [];
}
// Otherwise it is called with an existing item, to load its children.
return element.children;
}
getParent(element: ChildEvalLogTreeItem): ProviderResult<EvalLogTreeItem> {
return element.parent;
}
}
/** Manages a tree viewer of structured evaluator logs. */
export class EvalLogViewer extends DisposableObject {
private treeView: TreeView<EvalLogTreeItem>;
private treeDataProvider: EvalLogDataProvider;
constructor() {
super();
this.treeDataProvider = new EvalLogDataProvider();
this.treeView = window.createTreeView('codeQLEvalLogViewer', {
treeDataProvider: this.treeDataProvider,
showCollapseAll: true
});
this.push(this.treeView);
this.push(this.treeDataProvider);
this.push(
commandRunner('codeQLEvalLogViewer.clear', async () => {
this.clear();
})
);
}
private clear(): void {
this.treeDataProvider.roots = [];
this.treeDataProvider.refresh();
this.treeView.message = undefined;
}
// Called when the Show Evaluator Log (UI) command is run on a new query.
updateRoots(roots: EvalLogTreeItem[]): void {
this.treeDataProvider.roots = roots;
this.treeDataProvider.refresh();
this.treeView.message = 'Viewer for query run:'; // Currently only one query supported at a time.
// Handle error on reveal. This could happen if
// the tree view is disposed during the reveal.
this.treeView.reveal(roots[0], { focus: false })?.then(
() => { /**/ },
err => showAndLogErrorMessage(err)
);
}
}

View File

@@ -98,6 +98,7 @@ import { handleDownloadPacks, handleInstallPackDependencies } from './packaging'
import { HistoryItemLabelProvider } from './history-item-label-provider';
import { exportRemoteQueryResults } from './remote-queries/export-results';
import { RemoteQuery } from './remote-queries/remote-query';
import { EvalLogViewer } from './eval-log-viewer';
/**
* extension.ts
@@ -442,6 +443,10 @@ async function activateWithInstalledDistribution(
databaseUI.init();
ctx.subscriptions.push(databaseUI);
void logger.log('Initializing evaluator log viewer.');
const evalLogViewer = new EvalLogViewer();
ctx.subscriptions.push(evalLogViewer);
void logger.log('Initializing query history manager.');
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
ctx.subscriptions.push(queryHistoryConfigurationListener);
@@ -465,6 +470,7 @@ async function activateWithInstalledDistribution(
dbm,
intm,
rqm,
evalLogViewer,
queryStorageDir,
ctx,
queryHistoryConfigurationListener,

View File

@@ -1,42 +1,36 @@
// TODO(angelapwen): Only load in necessary information and
// location in bytes for this log to save memory.
export interface EvaluatorLogData {
queryCausingWork: string;
predicateName: string;
millis: number;
resultSize: number;
ra: Pipelines;
}
interface Pipelines {
// Key: pipeline identifier; Value: array of pipeline steps
pipelineNamesToSteps: Map<string, string[]>;
// location in bytes for this log to save memory.
export interface EvalLogData {
predicateName: string;
millis: number;
resultSize: number;
// Key: pipeline identifier; Value: array of pipeline steps
ra: Record<string, string[]>;
}
/**
* A pure method that parses a string of evaluator log summaries into
* an array of EvaluatorLogData objects.
* an array of EvalLogData objects.
*
*/
export function parseVisualizerData(logSummary: string): EvaluatorLogData[] {
// Remove newline delimiters because summary is in .jsonl format.
const jsonSummaryObjects: string[] = logSummary.split(/\r?\n\r?\n/g);
const visualizerData: EvaluatorLogData[] = [];
export function parseViewerData(logSummary: string): EvalLogData[] {
// Remove newline delimiters because summary is in .jsonl format.
const jsonSummaryObjects: string[] = logSummary.split(/\r?\n\r?\n/g);
const viewerData: EvalLogData[] = [];
for (const obj of jsonSummaryObjects) {
const jsonObj = JSON.parse(obj);
for (const obj of jsonSummaryObjects) {
const jsonObj = JSON.parse(obj);
// Only convert log items that have an RA and millis field
if (jsonObj.ra !== undefined && jsonObj.millis !== undefined) {
const newLogData: EvaluatorLogData = {
queryCausingWork: jsonObj.queryCausingWork,
predicateName: jsonObj.predicateName,
millis: jsonObj.millis,
resultSize: jsonObj.resultSize,
ra: jsonObj.ra
};
visualizerData.push(newLogData);
}
// Only convert log items that have an RA and millis field
if (jsonObj.ra !== undefined && jsonObj.millis !== undefined) {
const newLogData: EvalLogData = {
predicateName: jsonObj.predicateName,
millis: jsonObj.millis,
resultSize: jsonObj.resultSize,
ra: jsonObj.ra
};
viewerData.push(newLogData);
}
return visualizerData;
}
return viewerData;
}

View File

@@ -44,6 +44,9 @@ 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';
import { EvalLogViewer } from './eval-log-viewer';
import EvalLogTreeBuilder from './eval-log-tree-builder';
import { EvalLogData, parseViewerData } from './pure/log-summary-parser';
/**
* query-history.ts
@@ -315,6 +318,7 @@ export class QueryHistoryManager extends DisposableObject {
private readonly dbm: DatabaseManager,
private readonly localQueriesInterfaceManager: InterfaceManager,
private readonly remoteQueriesManager: RemoteQueriesManager,
private readonly evalLogViewer: EvalLogViewer,
private readonly queryStorageDir: string,
private readonly ctx: ExtensionContext,
private readonly queryHistoryConfigListener: QueryHistoryConfig,
@@ -432,6 +436,12 @@ export class QueryHistoryManager extends DisposableObject {
this.handleShowEvalLogSummary.bind(this)
)
);
this.push(
commandRunner(
'codeQLQueryHistory.showEvalLogViewer',
this.handleShowEvalLogViewer.bind(this)
)
);
this.push(
commandRunner(
'codeQLQueryHistory.cancel',
@@ -867,16 +877,16 @@ export class QueryHistoryManager extends DisposableObject {
}
}
private warnNoEvalLog() {
void showAndLogWarningMessage(`No evaluator log is available for this run. Perhaps it failed before evaluation, or you are running with a version of CodeQL before ' + ${CliVersionConstraint.CLI_VERSION_WITH_PER_QUERY_EVAL_LOG}?`);
}
private warnNoEvalLogSummary() {
void showAndLogWarningMessage(`Evaluator log summary and evaluator log are not available for this run. Perhaps they failed before evaluation, or you are running with a version of CodeQL before ${CliVersionConstraint.CLI_VERSION_WITH_PER_QUERY_EVAL_LOG}?`);
private warnNoEvalLogs() {
void showAndLogWarningMessage(`Evaluator log, summary, and viewer are not available for this run. Perhaps it failed before evaluation, or you are running with a version of CodeQL before ' + ${CliVersionConstraint.CLI_VERSION_WITH_PER_QUERY_EVAL_LOG}?`);
}
private warnInProgressEvalLogSummary() {
void showAndLogWarningMessage('The evaluator log summary is still being generated. Please try again later. The summary generation process is tracked in the "CodeQL Extension Log" view.');
void showAndLogWarningMessage('The evaluator log summary is still being generated for this run. Please try again later. The summary generation process is tracked in the "CodeQL Extension Log" view.');
}
private warnInProgressEvalLogViewer() {
void showAndLogWarningMessage('The viewer\'s data is still being generated for this run. Please try again or re-run the query.');
}
async handleShowEvalLog(
@@ -893,7 +903,7 @@ export class QueryHistoryManager extends DisposableObject {
if (finalSingleItem.evalLogLocation) {
await this.tryOpenExternalFile(finalSingleItem.evalLogLocation);
} else {
this.warnNoEvalLog();
this.warnNoEvalLogs();
}
}
@@ -910,18 +920,45 @@ export class QueryHistoryManager extends DisposableObject {
if (finalSingleItem.evalLogSummaryLocation) {
await this.tryOpenExternalFile(finalSingleItem.evalLogSummaryLocation);
return;
}
// Summary log file doesn't exist.
else {
if (finalSingleItem.evalLogLocation && fs.pathExists(finalSingleItem.evalLogLocation)) {
// If raw log does exist, then the summary log is still being generated.
this.warnInProgressEvalLogSummary();
} else {
this.warnNoEvalLogSummary();
}
if (finalSingleItem.evalLogLocation && fs.pathExists(finalSingleItem.evalLogLocation)) {
// If raw log does exist, then the summary log is still being generated.
this.warnInProgressEvalLogSummary();
} else {
this.warnNoEvalLogs();
}
}
async handleShowEvalLogViewer(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
// Only applicable to an individual local query
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem || finalSingleItem.t !== 'local') {
return;
}
// If the JSON summary file location wasn't saved, display error
if (finalSingleItem.jsonEvalLogSummaryLocation == undefined) {
this.warnInProgressEvalLogViewer();
return;
}
// TODO(angelapwen): Stream the file in.
void fs.readFile(finalSingleItem.jsonEvalLogSummaryLocation, async (err, buffer) => {
if (err) {
throw new Error(`Could not read evaluator log summary JSON file to generate viewer data at ${finalSingleItem.jsonEvalLogSummaryLocation}.`);
}
const evalLogData: EvalLogData[] = parseViewerData(buffer.toString());
const evalLogTreeBuilder = new EvalLogTreeBuilder(finalSingleItem.getQueryName(), evalLogData);
this.evalLogViewer.updateRoots(await evalLogTreeBuilder.getRoots());
});
}
async handleCancel(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]

View File

@@ -217,6 +217,7 @@ export class LocalQueryInfo {
public completedQuery: CompletedQueryInfo | undefined;
public evalLogLocation: string | undefined;
public evalLogSummaryLocation: string | undefined;
public jsonEvalLogSummaryLocation: string | undefined;
/**
* Note that in the {@link slurpQueryHistory} method, we create a FullQueryInfo instance

View File

@@ -37,7 +37,6 @@ import { ensureMetadataIsComplete } from './query-results';
import { SELECT_QUERY_NAME } from './contextual/locationFinder';
import { DecodedBqrsChunk } from './pure/bqrs-cli-types';
import { getErrorMessage } from './pure/helpers-pure';
import { parseVisualizerData } from './pure/log-summary-parser';
/**
* run-queries.ts
@@ -106,8 +105,8 @@ export class QueryEvaluationInfo {
get jsonEvalLogSummaryPath() {
return qsClient.findJsonQueryEvalLogSummaryFile(this.querySaveDir);
}
}
get evalLogEndSummaryPath() {
return qsClient.findQueryEvalLogEndSummaryFile(this.querySaveDir);
}
@@ -204,8 +203,9 @@ export class QueryEvaluationInfo {
});
if (await this.hasEvalLog()) {
this.displayHumanReadableLogSummary(queryInfo, qs);
if (config.isCanary()) {
this.parseJsonLogSummary(qs.cliServer);
if (config.isCanary()) { // Generate JSON summary for viewer.
await qs.cliServer.generateJsonLogSummary(this.evalLogPath, this.jsonEvalLogSummaryPath);
queryInfo.jsonEvalLogSummaryLocation = this.jsonEvalLogSummaryPath;
}
} else {
void showAndLogWarningMessage(`Failed to write structured evaluator log to ${this.evalLogPath}.`);
@@ -353,26 +353,6 @@ export class QueryEvaluationInfo {
});
}
/**
* Calls the appropriate CLI command to generate a JSON log summary and parse it
* into the appropriate data model for the log visualizer.
*/
parseJsonLogSummary(cliServer: cli.CodeQLCliServer): void {
void cliServer.generateJsonLogSummary(this.evalLogPath, this.jsonEvalLogSummaryPath)
.then(() => {
// TODO(angelapwen): Stream the file in.
fs.readFile(this.jsonEvalLogSummaryPath, (err, buffer) => {
if (err) {
throw new Error(`Could not read structured evaluator log summary JSON file at ${this.jsonEvalLogSummaryPath}.`);
}
parseVisualizerData(buffer.toString()); // Eventually this return value will feed into the tree visualizer.
});
})
.catch(err => {
void showAndLogWarningMessage(`Failed to generate JSON structured evaluator log summary. Reason: ${err.message}`);
});
}
/**
* Creates the CSV file containing the results of this query. This will only be called if the query
* does not have interpreted results and the CSV file does not already exist.

View File

@@ -0,0 +1,109 @@
import { expect } from 'chai';
import EvalLogTreeBuilder from '../../eval-log-tree-builder';
import { EvalLogData } from '../../pure/log-summary-parser';
describe('EvalLogTreeBuilder', () => {
it('should build the log tree roots', async () => {
const evalLogDataItems: EvalLogData[] = [
{
predicateName: 'quick_eval#query#ffffffff',
millis: 1,
resultSize: 596,
ra: {
pipeline: [
'{1} r1',
'{2} r2',
'return r2'
]
},
}
];
const expectedRoots = [
{
label: 'test-query.ql',
children: undefined
}
];
const expectedPredicate = [
{
label: 'quick_eval#query#ffffffff (596 tuples, 1 ms)',
children: undefined,
parent: undefined
},
];
const expectedRA = [
{
label: 'Pipeline: pipeline',
children: undefined,
parent: undefined
}
];
const expectedPipelineSteps = [{
label: '{1} r1',
children: [],
parent: undefined
},
{
label: '{2} r2',
children: [],
parent: undefined
},
{
label: 'return r2',
children: [],
parent: undefined
}];
const builder = new EvalLogTreeBuilder('test-query.ql', evalLogDataItems);
const roots = await builder.getRoots();
// Force children, parent to be undefined for ease of testing.
expect(roots.map(
r => ({ ...r, children: undefined })
)).to.deep.eq(expectedRoots);
expect((roots[0].children.map(
pred => ({ ...pred, children: undefined, parent: undefined })
))).to.deep.eq(expectedPredicate);
expect((roots[0].children[0].children.map(
ra => ({ ...ra, children: undefined, parent: undefined })
))).to.deep.eq(expectedRA);
// Pipeline steps' children should be empty so do not force undefined children here.
expect(roots[0].children[0].children[0].children.map(
step => ({ ...step, parent: undefined })
)).to.deep.eq(expectedPipelineSteps);
});
it('should build the tree with descriptive message when no data exists', async () => {
// Force children, parent to be undefined for ease of testing.
const expectedRoots = [
{
label: 'test-query-cached.ql',
children: undefined
}
];
const expectedNoPredicates = [
{
label: 'No predicates evaluated in this query run.',
children: [], // Should be empty so do not force empty here.
parent: undefined
}
];
const builder = new EvalLogTreeBuilder('test-query-cached.ql', []);
const roots = await builder.getRoots();
expect(roots.map(
r => ({ ...r, children: undefined })
)).to.deep.eq(expectedRoots);
expect(roots[0].children.map(
noPreds => ({ ...noPreds, parent: undefined })
)).to.deep.eq(expectedNoPredicates);
});
});

View File

@@ -0,0 +1,78 @@
import { expect } from 'chai';
import sinon = require('sinon');
import { commands } from 'vscode';
import { ChildEvalLogTreeItem, EvalLogTreeItem, EvalLogViewer } from '../../eval-log-viewer';
import { testDisposeHandler } from '../test-dispose-handler';
describe('EvalLogViewer', () => {
let roots: EvalLogTreeItem[];
let viewer: EvalLogViewer;
let sandbox: sinon.SinonSandbox;
beforeEach(async () => {
sandbox = sinon.createSandbox();
viewer = new EvalLogViewer();
sandbox.stub(commands, 'registerCommand');
sandbox.stub(commands, 'executeCommand');
});
afterEach(() => {
sandbox.restore();
if (viewer) {
viewer.dispose(testDisposeHandler);
}
});
it('should update the viewer\'s roots', () => {
const rootItem1: EvalLogTreeItem = {
label: 'root-1',
children: []
};
const childItem1: ChildEvalLogTreeItem = {
label: 'child-1',
parent: rootItem1,
children: [],
};
rootItem1.children.push(childItem1);
const rootItem2: EvalLogTreeItem = {
label: 'root-2',
children: []
};
const childItem2: ChildEvalLogTreeItem = {
label: 'child-2',
parent: rootItem2,
children: [],
};
rootItem2.children.push(childItem2);
const childItem3: ChildEvalLogTreeItem = {
label: 'child-3',
parent: rootItem2,
children: [],
};
rootItem2.children.push(childItem3);
const grandchildItem1: ChildEvalLogTreeItem = {
label: 'grandchild-1',
parent: childItem3,
children: []
};
childItem3.children.push(grandchildItem1);
roots = [rootItem1, rootItem2];
viewer.updateRoots(roots);
expect((viewer as any).treeDataProvider.roots).to.eq(roots);
expect((viewer as any).treeView.message).to.eq('Viewer for query run:');
});
it('should clear the viewer\'s roots', () => {
viewer.dispose(testDisposeHandler);
expect((viewer as any).treeDataProvider.roots.length).to.eq(0);
});
});

View File

@@ -20,6 +20,7 @@ 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';
import { EvalLogViewer } from '../../eval-log-viewer';
describe('query-history', () => {
const mockExtensionLocation = path.join(tmpDir.name, 'mock-extension-location');
@@ -799,6 +800,7 @@ describe('query-history', () => {
{} as DatabaseManager,
localQueriesInterfaceManagerStub,
remoteQueriesManagerStub,
{} as EvalLogViewer,
'xxx',
{
globalStorageUri: vscode.Uri.file(mockExtensionLocation),

View File

@@ -19,6 +19,7 @@ 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';
import { EvalLogViewer } from '../../../eval-log-viewer';
/**
* Tests for remote queries and how they interact with the query history manager.
@@ -93,6 +94,7 @@ describe('Remote queries and query history manager', function() {
{} as DatabaseManager,
localQueriesInterfaceManagerStub,
remoteQueriesManagerStub,
{} as EvalLogViewer,
STORAGE_DIR,
{
globalStorageUri: Uri.file(STORAGE_DIR),

View File

@@ -41,6 +41,7 @@ describe('commands declared in package.json', function() {
command.match(/^codeQLDatabases\./)
|| command.match(/^codeQLQueryHistory\./)
|| command.match(/^codeQLAstViewer\./)
|| command.match(/^codeQLEvalLogViewer\./)
|| command.match(/^codeQLTests\./)
) {
scopedCmds.add(command);

View File

@@ -3,40 +3,39 @@ import * as fs from 'fs-extra';
import * as path from 'path';
import 'mocha';
import { parseVisualizerData } from '../../src/pure/log-summary-parser';
import { parseViewerData } from '../../src/pure/log-summary-parser';
describe('Evaluator log summary tests', async function () {
describe('for a valid summary text', async function () {
it('should return only valid EvaluatorLogData objects', async function () {
describe('Evaluator log summary tests', async function() {
describe('for a valid summary text', async function() {
it('should return only valid EvalLogData objects', async function() {
const validSummaryText = await fs.readFile(path.join(__dirname, 'evaluator-log-summaries/valid-summary.jsonl'), 'utf8');
const logDataItems = parseVisualizerData(validSummaryText.toString());
const logDataItems = parseViewerData(validSummaryText.toString());
expect(logDataItems).to.not.be.undefined;
expect (logDataItems.length).to.eq(3);
expect(logDataItems.length).to.eq(3);
for (const item of logDataItems) {
expect(item.queryCausingWork).to.not.be.empty;
expect(item.predicateName).to.not.be.empty;
expect(item.millis).to.be.a('number');
expect(item.resultSize).to.be.a('number');
expect(item.ra).to.not.be.undefined;
expect(item.ra).to.not.be.empty;
for (const [pipeline, steps] of Object.entries(item.ra)) {
expect (pipeline).to.not.be.empty;
expect (steps).to.not.be.undefined;
expect (steps.length).to.be.greaterThan(0);
expect(pipeline).to.not.be.empty;
expect(steps).to.not.be.undefined;
expect(steps.length).to.be.greaterThan(0);
}
}
});
it('should not parse a summary header object', async function () {
it('should not parse a summary header object', async function() {
const invalidHeaderText = await fs.readFile(path.join(__dirname, 'evaluator-log-summaries/invalid-header.jsonl'), 'utf8');
const logDataItems = parseVisualizerData(invalidHeaderText);
expect (logDataItems.length).to.eq(0);
const logDataItems = parseViewerData(invalidHeaderText);
expect(logDataItems.length).to.eq(0);
});
it('should not parse a log event missing RA or millis fields', async function () {
it('should not parse a log event missing RA or millis fields', async function() {
const invalidSummaryText = await fs.readFile(path.join(__dirname, 'evaluator-log-summaries/invalid-summary.jsonl'), 'utf8');
const logDataItems = parseVisualizerData(invalidSummaryText);
expect (logDataItems.length).to.eq(0);
const logDataItems = parseViewerData(invalidSummaryText);
expect(logDataItems.length).to.eq(0);
});
});
});
});