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:
@@ -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."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
67
extensions/ql-vscode/src/eval-log-tree-builder.ts
Normal file
67
extensions/ql-vscode/src/eval-log-tree-builder.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
92
extensions/ql-vscode/src/eval-log-viewer.ts
Normal file
92
extensions/ql-vscode/src/eval-log-viewer.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user