diff --git a/src/commands/dataSourceCommand.ts b/src/commands/dataSourceCommand.ts index 083d9c53..fec01007 100644 --- a/src/commands/dataSourceCommand.ts +++ b/src/commands/dataSourceCommand.ts @@ -34,6 +34,7 @@ import { getMeta, importScratchpad, writeQueryResultsToConsole, + writeQueryResultsToView, } from "./serverCommand"; export async function addDataSource(): Promise { @@ -205,136 +206,171 @@ export async function runDataSource(dataSourceForm: any): Promise { const fileContent = convertDataSourceFormToDataSourceFile(dataSourceForm); let res: any; - const selectedType = - fileContent.dataSource.selectedType.toString() === "API" - ? "API" - : fileContent.dataSource.selectedType.toString() === "QSQL" - ? "QSQL" - : "SQL"; + const selectedType = getSelectedType(fileContent); switch (selectedType) { case "API": - const isTimeCorrect = checkIfTimeParamIsCorrect( - fileContent.dataSource.api.startTS, - fileContent.dataSource.api.endTS - ); - if (!isTimeCorrect) { - window.showErrorMessage( - "The time parameters(startTS and endTS) are not correct, please check the format or if the startTS is before the endTS" - ); - break; - } - const startTS = - fileContent.dataSource.api.startTS !== "" - ? convertTimeToTimestamp(fileContent.dataSource.api.startTS) - : undefined; - const endTS = - fileContent.dataSource.api.endTS !== "" - ? convertTimeToTimestamp(fileContent.dataSource.api.endTS) - : undefined; - const fill = - fileContent.dataSource.api.fill !== "" - ? fileContent.dataSource.api.fill - : undefined; - const temporality = - fileContent.dataSource.api.temporality !== "" - ? fileContent.dataSource.api.temporality - : undefined; - const filter = - fileContent.dataSource.api.filter.length > 0 - ? fileContent.dataSource.api.filter - : undefined; - const groupBy = - fileContent.dataSource.api.groupBy.length > 0 - ? fileContent.dataSource.api.groupBy - : undefined; - const agg = - fileContent.dataSource.api.agg.length > 0 - ? fileContent.dataSource.api.agg - : undefined; - const sortCols = - fileContent.dataSource.api.sortCols.length > 0 - ? fileContent.dataSource.api.sortCols - : undefined; - const slice = - fileContent.dataSource.api.slice.length > 0 - ? fileContent.dataSource.api.slice - : undefined; - const labels = - fileContent.dataSource.api.labels.length > 0 - ? fileContent.dataSource.api.labels - : undefined; - const apiBody: getDataBodyPayload = { - table: fileContent.dataSource.api.table, - }; - - apiBody.startTS = startTS; - apiBody.endTS = endTS; - apiBody.fill = fill; - apiBody.temporality = temporality; - apiBody.groupBy = groupBy; - apiBody.agg = agg; - apiBody.sortCols = sortCols; - apiBody.slice = slice; - apiBody.labels = labels; - - if (filter !== undefined) { - apiBody.filter = filter.map((filterEl: string) => { - return filterEl.split(";"); - }); - } - - const apiCall = await getDataInsights( - ext.insightsAuthUrls.dataURL, - JSON.stringify(apiBody) - ); - if (apiCall?.arrayBuffer) { - res = handleWSResults(apiCall.arrayBuffer); - } - writeQueryResultsToConsole( - res, - "GetData - table: " + apiBody.table, - selectedType - ); + res = await runApiDataSource(fileContent); break; case "QSQL": - const assembly = fileContent.dataSource.qsql.selectedTarget.slice(0, -4); - const target = fileContent.dataSource.qsql.selectedTarget.slice(-3); - const qsqlBody = { - assembly: assembly, - target: target, - query: fileContent.dataSource.qsql.query, - }; - const qsqlCall = await getDataInsights( - ext.insightsAuthUrls.qsqlURL, - JSON.stringify(qsqlBody) - ); - if (qsqlCall?.arrayBuffer) { - res = handleWSResults(qsqlCall.arrayBuffer); - } - writeQueryResultsToConsole( - res, - fileContent.dataSource.qsql.query, - selectedType - ); + res = await runQsqlDataSource(fileContent); break; case "SQL": default: - const sqlBody = { - query: fileContent.dataSource.sql.query, - }; - const sqlCall = await getDataInsights( - ext.insightsAuthUrls.sqlURL, - JSON.stringify(sqlBody) - ); - if (sqlCall?.arrayBuffer) { - res = handleWSResults(sqlCall.arrayBuffer); - } - writeQueryResultsToConsole( - res, - fileContent.dataSource.sql.query, - selectedType - ); + res = await runSqlDataSource(fileContent); break; } + + if (ext.resultsViewProvider.isVisible()) { + writeQueryResultsToView(res, selectedType); + } else { + writeQueryResultsToConsole( + res, + getQuery(fileContent, selectedType), + selectedType + ); + } +} + +export function getSelectedType(fileContent: DataSourceFiles): string { + const selectedType = fileContent.dataSource.selectedType.toString(); + switch (selectedType) { + case "API": + case "0": + return "API"; + case "QSQL": + case "1": + return "QSQL"; + case "SQL": + case "2": + return "SQL"; + default: + throw new Error(`Invalid selectedType: ${selectedType}`); + } +} + +export async function runApiDataSource( + fileContent: DataSourceFiles +): Promise { + const isTimeCorrect = checkIfTimeParamIsCorrect( + fileContent.dataSource.api.startTS, + fileContent.dataSource.api.endTS + ); + if (!isTimeCorrect) { + window.showErrorMessage( + "The time parameters(startTS and endTS) are not correct, please check the format or if the startTS is before the endTS" + ); + return; + } + const apiBody = getApiBody(fileContent); + const apiCall = await getDataInsights( + ext.insightsAuthUrls.dataURL, + JSON.stringify(apiBody) + ); + if (apiCall?.arrayBuffer) { + return handleWSResults(apiCall.arrayBuffer); + } +} + +export function getApiBody( + fileContent: DataSourceFiles +): Partial { + const { + startTS, + endTS, + fill, + temporality, + filter, + groupBy, + agg, + sortCols, + slice, + labels, + table, + } = fileContent.dataSource.api; + + const startTSValue = startTS?.trim() ? convertTimeToTimestamp(startTS) : ""; + const endTSValue = endTS?.trim() ? convertTimeToTimestamp(endTS) : ""; + const fillValue = fill?.trim() || undefined; + const temporalityValue = temporality?.trim() || undefined; + const filterValue = filter.length ? filter : undefined; + const groupByValue = groupBy.length ? groupBy : undefined; + const aggValue = agg.length ? agg : undefined; + const sortColsValue = sortCols.length ? sortCols : undefined; + const sliceValue = slice.length ? slice : undefined; + const labelsValue = labels.length ? labels : undefined; + + const apiBodyAux: getDataBodyPayload = { + table, + startTS: startTSValue, + endTS: endTSValue, + fill: fillValue, + temporality: temporalityValue, + groupBy: groupByValue, + agg: aggValue, + sortCols: sortColsValue, + slice: sliceValue, + labels: labelsValue, + }; + + if (filterValue !== undefined) { + apiBodyAux.filter = filterValue.map((filterEl: string) => { + return filterEl.split(";"); + }); + } + + const apiBody = Object.fromEntries( + Object.entries(apiBodyAux).filter(([_, value]) => value !== undefined) + ); + + return apiBody; +} + +export async function runQsqlDataSource( + fileContent: DataSourceFiles +): Promise { + const assembly = fileContent.dataSource.qsql.selectedTarget.slice(0, -4); + const target = fileContent.dataSource.qsql.selectedTarget.slice(-3); + const qsqlBody = { + assembly: assembly, + target: target, + query: fileContent.dataSource.qsql.query, + }; + const qsqlCall = await getDataInsights( + ext.insightsAuthUrls.qsqlURL, + JSON.stringify(qsqlBody) + ); + if (qsqlCall?.arrayBuffer) { + return handleWSResults(qsqlCall.arrayBuffer); + } +} + +export async function runSqlDataSource( + fileContent: DataSourceFiles +): Promise { + const sqlBody = { + query: fileContent.dataSource.sql.query, + }; + const sqlCall = await getDataInsights( + ext.insightsAuthUrls.sqlURL, + JSON.stringify(sqlBody) + ); + if (sqlCall?.arrayBuffer) { + return handleWSResults(sqlCall.arrayBuffer); + } +} + +export function getQuery( + fileContent: DataSourceFiles, + selectedType: string +): string { + switch (selectedType) { + case "API": + return `GetData - table: ${fileContent.dataSource.api.table}`; + case "QSQL": + return fileContent.dataSource.qsql.query; + case "SQL": + default: + return fileContent.dataSource.sql.query; + } } diff --git a/src/commands/serverCommand.ts b/src/commands/serverCommand.ts index eed6658f..ac464d0d 100644 --- a/src/commands/serverCommand.ts +++ b/src/commands/serverCommand.ts @@ -71,7 +71,7 @@ import { import { refreshDataSourcesPanel } from "../utils/dataSource"; import { ExecutionConsole } from "../utils/executionConsole"; import { openUrl } from "../utils/openUrl"; -import { sanitizeQuery } from "../utils/queryUtils"; +import { handleWSResults, sanitizeQuery } from "../utils/queryUtils"; import { validateServerAlias, validateServerName, @@ -582,6 +582,7 @@ export async function getScratchpadQuery( context?: string ): Promise { if (ext.connectionNode instanceof InsightsNode) { + const isTableView = ext.resultsViewProvider.isVisible(); const scratchpadURL = new url.URL( ext.insightsAuthUrls.scratchpadURL, ext.connectionNode.details.server @@ -612,7 +613,14 @@ export async function getScratchpadQuery( Authorization: `Bearer ${token.accessToken}`, Username: username.preferred_username, }, - body: { expression: query, language: "q", context: context || "." }, + body: { + expression: query, + isTableView, + language: "q", + context: context || ".", + sampleFn: "first", + sampleSize: 10000, + }, json: true, }; @@ -632,7 +640,17 @@ export async function getScratchpadQuery( scratchpadURL.toString(), options ); - + if ( + isTableView && + spRes?.data && + Array.isArray(spRes.data) && + !spRes.error + ) { + const buffer = new Uint8Array( + spRes.data.map((x: string) => parseInt(x, 16)) + ).buffer; + return handleWSResults(buffer); + } return spRes; } ); @@ -942,7 +960,10 @@ export function writeQueryResultsToView( commands.executeCommand("kdb.resultsPanel.update", result, dataSourceType); } -function writeScratchpadResult(result: ScratchpadResult, query: string): void { +export function writeScratchpadResult( + result: ScratchpadResult, + query: string +): void { const queryConsole = ExecutionConsole.start(); if (result.error) { @@ -953,10 +974,10 @@ function writeScratchpadResult(result: ScratchpadResult, query: string): void { ext.connectionNode?.label ? ext.connectionNode.label : "" ); } else { - queryConsole.append( - result.data, - query, - ext.connectionNode?.label ? ext.connectionNode.label : "" - ); + if (ext.resultsViewProvider.isVisible()) { + writeQueryResultsToView(result, "SCRATCHPAD"); + } else { + writeQueryResultsToConsole(result.data, query); + } } } diff --git a/src/models/data.ts b/src/models/data.ts index cae921a3..1ec908e2 100644 --- a/src/models/data.ts +++ b/src/models/data.ts @@ -25,8 +25,8 @@ export type GetDataObjectPayload = { export type getDataBodyPayload = { table: string; - startTS?: string; - endTS?: string; + startTS: string; + endTS: string; fill?: string; temporality?: string; filter?: string[][]; diff --git a/src/services/resultsPanelProvider.ts b/src/services/resultsPanelProvider.ts index 99178455..ca8f2d6d 100644 --- a/src/services/resultsPanelProvider.ts +++ b/src/services/resultsPanelProvider.ts @@ -65,6 +65,15 @@ export class KdbResultsViewProvider implements WebviewViewProvider { } } + public removeEndCommaFromStrings(data: string[]): string[] { + return data.map((element) => { + if (element.endsWith(",")) { + return element.slice(0, -1); + } + return element; + }); + } + convertToCsv(data: any[]): string[] { const keys = Object.keys(data[0]); const header = keys.join(","); @@ -131,7 +140,7 @@ export class KdbResultsViewProvider implements WebviewViewProvider { sanitizeString(str: string | string[]): string { if (str instanceof Array) { - str = str.join(","); + str = str.join(" "); } str = str.toString(); str = str.trim(); diff --git a/src/utils/queryUtils.ts b/src/utils/queryUtils.ts index 88ed93e3..8bedff78 100644 --- a/src/utils/queryUtils.ts +++ b/src/utils/queryUtils.ts @@ -47,6 +47,9 @@ export function handleWSResults(ab: ArrayBuffer): any { if (res.rows.length === 0) { return "No results found."; } + if (ext.resultsViewProvider.isVisible()) { + return getValueFromArray(res.rows); + } return convertRows(res.rows); } catch (error) { console.log(error); @@ -54,15 +57,31 @@ export function handleWSResults(ab: ArrayBuffer): any { } } +export function getValueFromArray(arr: any[]): string | any[] { + if (arr.length === 1 && typeof arr[0] === "object" && arr[0] !== null) { + const obj = arr[0]; + const keys = Object.keys(obj); + if (keys.length === 1 && keys[0] === "Value") { + return String(obj.Value); + } + } + return arr; +} + export function convertRows(rows: any[]): any[] { if (rows.length === 0) { return []; } const keys = Object.keys(rows[0]); - const result = [keys.join(",")]; + const result = [keys.join("#$#;#$#")]; for (const row of rows) { - const values = keys.map((key) => row[key]); - result.push(values.join(",")); + const values = keys.map((key) => { + if (Array.isArray(row[key])) { + return row[key].join(" "); + } + return row[key]; + }); + result.push(values.join("#$#;#$#")); } return result; } @@ -72,36 +91,28 @@ export function convertRowsToConsole(rows: string[]): string[] { return []; } - const vector = []; - for (let i = 0; i < rows.length; i++) { - vector.push(rows[i].split(",")); - } + const vector = rows.map((row) => row.split("#$#;#$#")); - const columnCounters = []; - for (let j = 0; j < vector[0].length; j++) { - let maxLength = 0; - for (let i = 0; i < vector.length; i++) { - maxLength = Math.max(maxLength, vector[i][j].length); - } - columnCounters.push(maxLength + 2); - } + const columnCounters = vector[0].reduce((counters: number[], _, j) => { + const maxLength = vector.reduce( + (max, row) => Math.max(max, row[j].length), + 0 + ); + counters.push(maxLength + 2); + return counters; + }, []); - for (let i = 0; i < vector.length; i++) { - const row = vector[i]; - for (let j = 0; j < row.length; j++) { - const value = row[j]; + vector.forEach((row) => { + row.forEach((value, j) => { const counter = columnCounters[j]; const diff = counter - value.length; if (diff > 0) { row[j] = value + " ".repeat(diff); } - } - } + }); + }); - const result = []; - for (let i = 0; i < vector.length; i++) { - result.push(vector[i].join("")); - } + const result = vector.map((row) => row.join("")); const totalCount = columnCounters.reduce((sum, count) => sum + count, 0); const totalCounter = "-".repeat(totalCount); diff --git a/src/webview/styles/resultsPanel.css b/src/webview/styles/resultsPanel.css index b9cba75f..1ac18633 100644 --- a/src/webview/styles/resultsPanel.css +++ b/src/webview/styles/resultsPanel.css @@ -1,6 +1,6 @@ html, body { - height: 100vh; + height: 86vh; width: 100%; box-sizing: border-box; -webkit-overflow-scrolling: touch; diff --git a/test/suite/commands.test.ts b/test/suite/commands.test.ts index cadf70c8..6bf02210 100644 --- a/test/suite/commands.test.ts +++ b/test/suite/commands.test.ts @@ -11,6 +11,7 @@ * specific language governing permissions and limitations under the License. */ +import assert from "assert"; import * as sinon from "sinon"; import * as vscode from "vscode"; import * as dataSourceCommand from "../../src/commands/dataSourceCommand"; @@ -18,23 +19,507 @@ import * as installTools from "../../src/commands/installTools"; import * as serverCommand from "../../src/commands/serverCommand"; import * as walkthroughCommand from "../../src/commands/walkthroughCommand"; import { ext } from "../../src/extensionVariables"; +import { DataSourceFiles, DataSourceTypes } from "../../src/models/dataSource"; +import { ScratchpadResult } from "../../src/models/scratchpadResult"; import { KdbTreeProvider } from "../../src/services/kdbTreeProvider"; +import { KdbResultsViewProvider } from "../../src/services/resultsPanelProvider"; import * as coreUtils from "../../src/utils/core"; +import * as dataSourceUtils from "../../src/utils/dataSource"; +import { ExecutionConsole } from "../../src/utils/executionConsole"; +import * as queryUtils from "../../src/utils/queryUtils"; describe("dataSourceCommand", () => { - //write tests for src/commands/dataSourceCommand.ts - //function to be deleted after write the tests - dataSourceCommand.renameDataSource("test", "test2"); + let dummyDataSourceFiles: DataSourceFiles; + const uriTest: vscode.Uri = vscode.Uri.parse("test"); + let resultsPanel: KdbResultsViewProvider; + ext.outputChannel = vscode.window.createOutputChannel("kdb"); + const view: vscode.WebviewView = { + visible: true, + // eslint-disable-next-line @typescript-eslint/no-empty-function + show: (): void => {}, + viewType: "kdb-results", + webview: { + options: {}, + html: "", + cspSource: "", + asWebviewUri: (uri: vscode.Uri) => uri, + onDidReceiveMessage: new vscode.EventEmitter().event, + postMessage: (): Thenable => { + return Promise.resolve(true); + }, + }, + onDidDispose: new vscode.EventEmitter().event, + onDidChangeVisibility: new vscode.EventEmitter().event, + }; + + beforeEach(() => { + dummyDataSourceFiles = { + name: "dummy ds", + insightsNode: "dummy insights", + dataSource: { + selectedType: DataSourceTypes.API, + api: { + selectedApi: "getData", + table: "dummy_table", + startTS: "2023-09-10T09:30", + endTS: "2023-09-19T12:30", + fill: "", + filter: [], + groupBy: [], + labels: [], + slice: [], + sortCols: [], + temporality: "", + agg: [], + }, + qsql: { + selectedTarget: "dummy_table rdb", + query: "dummy QSQL query", + }, + sql: { + query: "dummy SQL query", + }, + }, + }; + resultsPanel = new KdbResultsViewProvider(uriTest); + }); + describe("getSelectedType", () => { + it("should return selectedType if it is API", () => { + const result = dataSourceCommand.getSelectedType(dummyDataSourceFiles); + sinon.assert.match(result, "API"); + }); + + it("should return selectedType if it is QSQL", () => { + dummyDataSourceFiles.dataSource.selectedType = DataSourceTypes.QSQL; + const result2 = dataSourceCommand.getSelectedType(dummyDataSourceFiles); + sinon.assert.match(result2, "QSQL"); + }); + + it("should return selectedType if it is SQL", () => { + dummyDataSourceFiles.dataSource.selectedType = DataSourceTypes.SQL; + const result3 = dataSourceCommand.getSelectedType(dummyDataSourceFiles); + sinon.assert.match(result3, "SQL"); + }); + }); + + describe("getQuery", () => { + it("should return the correct query for API data sources", () => { + const query = dataSourceCommand.getQuery(dummyDataSourceFiles, "API"); + assert.strictEqual(query, "GetData - table: dummy_table"); + }); + + it("should return the correct query for QSQL data sources", () => { + const query = dataSourceCommand.getQuery(dummyDataSourceFiles, "QSQL"); + assert.strictEqual(query, "dummy QSQL query"); + }); + + it("should return the correct query for SQL data sources", () => { + const query = dataSourceCommand.getQuery(dummyDataSourceFiles, "SQL"); + assert.strictEqual(query, "dummy SQL query"); + }); + }); + + describe("getApiBody", () => { + it("should return the correct API body for a data source with all fields", () => { + dummyDataSourceFiles.dataSource.api.startTS = "2022-01-01T00:00:00Z"; + dummyDataSourceFiles.dataSource.api.endTS = "2022-01-02T00:00:00Z"; + dummyDataSourceFiles.dataSource.api.fill = "none"; + dummyDataSourceFiles.dataSource.api.temporality = "1h"; + dummyDataSourceFiles.dataSource.api.filter = [ + "col1=val1;col2=val2", + "col3=val3", + ]; + dummyDataSourceFiles.dataSource.api.groupBy = ["col1", "col2"]; + dummyDataSourceFiles.dataSource.api.agg = ["sum(col3)", "avg(col4)"]; + dummyDataSourceFiles.dataSource.api.sortCols = ["col1 ASC", "col2 DESC"]; + dummyDataSourceFiles.dataSource.api.slice = ["10", "20"]; + dummyDataSourceFiles.dataSource.api.labels = ["label1", "label2"]; + dummyDataSourceFiles.dataSource.api.table = "myTable"; + const apiBody = dataSourceCommand.getApiBody(dummyDataSourceFiles); + assert.deepStrictEqual(apiBody, { + table: "myTable", + startTS: "2022-01-01T00:00:00.000000000", + endTS: "2022-01-02T00:00:00.000000000", + fill: "none", + temporality: "1h", + filter: [["col1=val1", "col2=val2"], ["col3=val3"]], + groupBy: ["col1", "col2"], + agg: ["sum(col3)", "avg(col4)"], + sortCols: ["col1 ASC", "col2 DESC"], + slice: ["10", "20"], + labels: ["label1", "label2"], + }); + }); + + it("should return the correct API body for a data source with only required fields", () => { + dummyDataSourceFiles.dataSource.api.startTS = "2022-01-01T00:00:00Z"; + dummyDataSourceFiles.dataSource.api.endTS = "2022-01-02T00:00:00Z"; + dummyDataSourceFiles.dataSource.api.fill = ""; + dummyDataSourceFiles.dataSource.api.temporality = ""; + dummyDataSourceFiles.dataSource.api.filter = []; + dummyDataSourceFiles.dataSource.api.groupBy = []; + dummyDataSourceFiles.dataSource.api.agg = []; + dummyDataSourceFiles.dataSource.api.sortCols = []; + dummyDataSourceFiles.dataSource.api.slice = []; + dummyDataSourceFiles.dataSource.api.labels = []; + dummyDataSourceFiles.dataSource.api.table = "myTable"; + const apiBody = dataSourceCommand.getApiBody(dummyDataSourceFiles); + assert.deepStrictEqual(apiBody, { + table: "myTable", + startTS: "2022-01-01T00:00:00.000000000", + endTS: "2022-01-02T00:00:00.000000000", + }); + }); + }); + describe("runApiDataSource", () => { + let getApiBodyStub: sinon.SinonStub; + let checkIfTimeParamIsCorrectStub: sinon.SinonStub; + let getDataInsightsStub: sinon.SinonStub; + let handleWSResultsStub: sinon.SinonStub; + + beforeEach(() => { + getApiBodyStub = sinon.stub(dataSourceCommand, "getApiBody"); + checkIfTimeParamIsCorrectStub = sinon.stub( + dataSourceUtils, + "checkIfTimeParamIsCorrect" + ); + getDataInsightsStub = sinon.stub(serverCommand, "getDataInsights"); + handleWSResultsStub = sinon.stub(queryUtils, "handleWSResults"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should show an error message if the time parameters are incorrect", async () => { + const windowMock = sinon.mock(vscode.window); + checkIfTimeParamIsCorrectStub.returns(false); + + await dataSourceCommand.runApiDataSource(dummyDataSourceFiles); + windowMock + .expects("showErrorMessage") + .once() + .withArgs( + "The time parameters(startTS and endTS) are not correct, please check the format or if the startTS is before the endTS" + ); + sinon.assert.notCalled(getApiBodyStub); + sinon.assert.notCalled(getDataInsightsStub); + sinon.assert.notCalled(handleWSResultsStub); + }); + + it("should call the API and handle the results if the time parameters are correct", async () => { + checkIfTimeParamIsCorrectStub.returns(true); + getApiBodyStub.returns({ table: "myTable" }); + getDataInsightsStub.resolves({ arrayBuffer: true }); + handleWSResultsStub.resolves([ + { a: "2", b: "3" }, + { a: "4", b: "6" }, + { a: "6", b: "9" }, + ]); + + const result = await dataSourceCommand.runApiDataSource( + dummyDataSourceFiles + ); + + sinon.assert.calledOnce(getDataInsightsStub); + sinon.assert.calledOnce(handleWSResultsStub); + assert.deepStrictEqual(result, [ + { a: "2", b: "3" }, + { a: "4", b: "6" }, + { a: "6", b: "9" }, + ]); + }); + }); + + describe("runQsqlDataSource", () => { + let getDataInsightsStub: sinon.SinonStub; + let handleWSResultsStub: sinon.SinonStub; + + beforeEach(() => { + getDataInsightsStub = sinon.stub(serverCommand, "getDataInsights"); + handleWSResultsStub = sinon.stub(queryUtils, "handleWSResults"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should call the API and handle the results", async () => { + getDataInsightsStub.resolves({ arrayBuffer: true }); + handleWSResultsStub.resolves([ + { a: "2", b: "3" }, + { a: "4", b: "6" }, + { a: "6", b: "9" }, + ]); + + const result = await dataSourceCommand.runQsqlDataSource( + dummyDataSourceFiles + ); + + sinon.assert.calledOnce(getDataInsightsStub); + sinon.assert.calledOnce(handleWSResultsStub); + assert.deepStrictEqual(result, [ + { a: "2", b: "3" }, + { a: "4", b: "6" }, + { a: "6", b: "9" }, + ]); + }); + }); + + describe("runSqlDataSource", () => { + let getDataInsightsStub: sinon.SinonStub; + let handleWSResultsStub: sinon.SinonStub; + + beforeEach(() => { + getDataInsightsStub = sinon.stub(serverCommand, "getDataInsights"); + handleWSResultsStub = sinon.stub(queryUtils, "handleWSResults"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should call the API and handle the results", async () => { + getDataInsightsStub.resolves({ arrayBuffer: true }); + handleWSResultsStub.resolves([ + { a: "2", b: "3" }, + { a: "4", b: "6" }, + { a: "6", b: "9" }, + ]); + + const result = await dataSourceCommand.runSqlDataSource( + dummyDataSourceFiles + ); + + sinon.assert.calledOnce(getDataInsightsStub); + sinon.assert.calledOnce(handleWSResultsStub); + assert.deepStrictEqual(result, [ + { a: "2", b: "3" }, + { a: "4", b: "6" }, + { a: "6", b: "9" }, + ]); + }); + }); + + describe("runDataSource", () => { + const dummyMeta = { + rc: [ + { + api: 3, + agg: 1, + assembly: 1, + schema: 1, + rc: "dummy-rc", + labels: [{ kxname: "dummy-assembly" }], + started: "2023-10-04T17:20:57.659088747", + }, + ], + dap: [ + { + assembly: "dummy-assembly", + instance: "idb", + startTS: "2023-10-25T01:40:03.000000000", + endTS: "2023-10-25T14:00:03.000000000", + }, + ], + api: [ + { + api: ".kxi.getData", + kxname: ["dummy-assembly"], + aggFn: ".sgagg.getData", + custom: false, + full: true, + metadata: { + description: "dummy desc.", + params: [ + { + name: "table", + type: -11, + isReq: true, + description: "dummy desc.", + }, + ], + return: { + type: 0, + description: "dummy desc.", + }, + misc: { safe: true }, + aggReturn: { + type: 98, + description: "dummy desc.", + }, + }, + procs: [], + }, + ], + agg: [ + { + aggFn: ".sgagg.aggFnDflt", + custom: false, + full: true, + metadata: { + description: "dummy desc.", + params: [{ description: "dummy desc." }], + return: { description: "dummy desc." }, + misc: {}, + }, + procs: [], + }, + ], + assembly: [ + { + assembly: "dummy-assembly", + kxname: "dummy-assembly", + tbls: ["dummyTbl"], + }, + ], + schema: [ + { + table: "dummyTbl", + assembly: ["dummy-assembly"], + typ: "partitioned", + pkCols: [], + prtnCol: "srcTime", + sortColsMem: [], + sortColsIDisk: [], + sortColsDisk: [], + isSplayed: true, + isPartitioned: true, + isSharded: false, + columns: [ + { + column: "sym", + typ: 10, + description: "dummy desc.", + oldName: "", + attrMem: "", + attrIDisk: "", + attrDisk: "", + isSerialized: false, + foreign: "", + anymap: false, + backfill: "", + }, + ], + }, + ], + }; + const dummyFileContent = { + name: "dummy-DS", + dataSource: { + selectedType: "QSQL", + api: { + selectedApi: "getData", + table: "dummyTbl", + startTS: "2023-09-10T09:30", + endTS: "2023-09-19T12:30", + fill: "", + temporality: "", + filter: [], + groupBy: [], + agg: [], + sortCols: [], + slice: [], + labels: [], + }, + qsql: { + query: + "n:10;\n([] date:n?(reverse .z.d-1+til 10); instance:n?`inst1`inst2`inst3`inst4; sym:n?`USD`EUR`GBP`JPY; cnt:n?10; lists:{x?10}@/:1+n?10)\n", + selectedTarget: "dummy-target", + }, + sql: { query: "test query" }, + }, + insightsNode: "dummyNode", + }; + const uriTest: vscode.Uri = vscode.Uri.parse("test"); + ext.resultsViewProvider = new KdbResultsViewProvider(uriTest); + let isVisibleStub: sinon.SinonStub; + let getMetaStub: sinon.SinonStub; + let convertDSFormToDSFile: sinon.SinonStub; + let getSelectedTypeStub: sinon.SinonStub; + let runApiDataSourceStub: sinon.SinonStub; + let runQsqlDataSourceStub: sinon.SinonStub; + let runSqlDataSourceStub: sinon.SinonStub; + let writeQueryResultsToViewStub: sinon.SinonStub; + let writeQueryResultsToConsoleStub: sinon.SinonStub; + const appendLineSpy = sinon.spy(ext.outputChannel, "appendLine"); + // const windowErrorSpy = sinon.spy(vscode.window, "showErrorMessage"); + ext.outputChannel = vscode.window.createOutputChannel("kdb"); + + beforeEach(() => { + getMetaStub = sinon.stub(serverCommand, "getMeta"); + convertDSFormToDSFile = sinon.stub( + dataSourceUtils, + "convertDataSourceFormToDataSourceFile" + ); + isVisibleStub = sinon.stub(ext.resultsViewProvider, "isVisible"); + getSelectedTypeStub = sinon.stub(dataSourceCommand, "getSelectedType"); + runApiDataSourceStub = sinon.stub(dataSourceCommand, "runApiDataSource"); + runQsqlDataSourceStub = sinon.stub( + dataSourceCommand, + "runQsqlDataSource" + ); + runSqlDataSourceStub = sinon.stub(dataSourceCommand, "runSqlDataSource"); + writeQueryResultsToViewStub = sinon.stub( + serverCommand, + "writeQueryResultsToView" + ); + writeQueryResultsToConsoleStub = sinon.stub( + serverCommand, + "writeQueryResultsToConsole" + ); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should show an error message if not connected to an Insights server", async () => { + getMetaStub.resolves({}); + await dataSourceCommand.runDataSource({}); + sinon.assert.notCalled(convertDSFormToDSFile); + }); + + it("should return QSQL results)", async () => { + getMetaStub.resolves(dummyMeta); + convertDSFormToDSFile.returns(dummyFileContent); + getSelectedTypeStub.returns("QSQL"); + runQsqlDataSourceStub.resolves("dummy results"); + isVisibleStub.returns(true); + await dataSourceCommand.runDataSource({}); + sinon.assert.calledOnce(writeQueryResultsToViewStub); + }); + + it("should return API results)", async () => { + dummyFileContent.dataSource.selectedType = "API"; + getMetaStub.resolves(dummyMeta); + convertDSFormToDSFile.returns(dummyFileContent); + getSelectedTypeStub.returns("API"); + runApiDataSourceStub.resolves("dummy results"); + isVisibleStub.returns(false); + await dataSourceCommand.runDataSource({}); + sinon.assert.calledOnce(writeQueryResultsToConsoleStub); + }); + + it("should return SQL results)", async () => { + dummyFileContent.dataSource.selectedType = "SQL"; + getMetaStub.resolves(dummyMeta); + convertDSFormToDSFile.returns(dummyFileContent); + getSelectedTypeStub.returns("SQL"); + runSqlDataSourceStub.resolves("dummy results"); + isVisibleStub.returns(false); + await dataSourceCommand.runDataSource({}); + sinon.assert.calledOnce(writeQueryResultsToConsoleStub); + }); + }); }); + describe("installTools", () => { //write tests for src/commands/installTools.ts //function to be deleted after write the tests installTools.installTools(); }); describe("serverCommand", () => { - //write tests for src/commands/serverCommand.ts - //function to be deleted after write the tests - serverCommand.addNewConnection(); describe("writeQueryResultsToView", () => { it("should call executeCommand with correct arguments", () => { const result = { data: [1, 2, 3] }; @@ -156,6 +641,71 @@ describe("serverCommand", () => { sinon.assert.calledOnce(updateServersStub); }); }); + + describe("writeScratchpadResult", () => { + const _console = vscode.window.createOutputChannel("q Console Output"); + const executionConsole = new ExecutionConsole(_console); + const uriTest: vscode.Uri = vscode.Uri.parse("test"); + ext.resultsViewProvider = new KdbResultsViewProvider(uriTest); + let executionConsoleStub: sinon.SinonStub; + let scratchpadResult: ScratchpadResult; + let queryConsoleErrorStub: sinon.SinonStub; + let writeQueryResultsToViewStub: sinon.SinonStub; + let writeQueryResultsToConsoleStub: sinon.SinonStub; + let isVisibleStub: sinon.SinonStub; + + beforeEach(() => { + executionConsoleStub = sinon + .stub(ExecutionConsole, "start") + .returns(executionConsole); + scratchpadResult = { + data: "1234", + error: false, + errorMsg: "", + sessionID: "123", + }; + queryConsoleErrorStub = sinon.stub( + ExecutionConsole.prototype, + "appendQueryError" + ); + writeQueryResultsToViewStub = sinon.stub( + serverCommand, + "writeQueryResultsToView" + ); + writeQueryResultsToConsoleStub = sinon.stub( + serverCommand, + "writeQueryResultsToConsole" + ); + isVisibleStub = sinon.stub(ext.resultsViewProvider, "isVisible"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should write appendQueryError", () => { + scratchpadResult.error = true; + scratchpadResult.errorMsg = "error"; + serverCommand.writeScratchpadResult(scratchpadResult, "dummy query"); + sinon.assert.notCalled(writeQueryResultsToViewStub); + sinon.assert.notCalled(writeQueryResultsToConsoleStub); + }); + + it("should write to view", () => { + scratchpadResult.data = "data"; + isVisibleStub.returns(true); + serverCommand.writeScratchpadResult(scratchpadResult, "dummy query"); + sinon.assert.notCalled(writeQueryResultsToConsoleStub); + sinon.assert.notCalled(queryConsoleErrorStub); + }); + + it("should write to console", () => { + scratchpadResult.data = "data"; + isVisibleStub.returns(false); + serverCommand.writeScratchpadResult(scratchpadResult, "dummy query"); + sinon.assert.notCalled(writeQueryResultsToViewStub); + }); + }); }); describe("walkthroughCommand", () => { diff --git a/test/suite/panels.test.ts b/test/suite/panels.test.ts index 63cbf373..858d6c44 100644 --- a/test/suite/panels.test.ts +++ b/test/suite/panels.test.ts @@ -116,7 +116,7 @@ describe("WebPanels", () => { it("should transform an array of strings into a single string", () => { const inputString = ["test", "string", "with", "array"]; - const expectedString = "test,string,with,array"; + const expectedString = "test string with array"; const actualString = resultsPanel.sanitizeString(inputString); assert.strictEqual(actualString, expectedString); }); @@ -169,6 +169,29 @@ describe("WebPanels", () => { }); }); + describe("removeEndCommaFromStrings", () => { + it("should remove the comma from the end of a string if it ends with a comma", () => { + const input = ["hello,", "world,"]; + const expectedOutput = ["hello", "world"]; + const actualOutput = resultsPanel.removeEndCommaFromStrings(input); + assert.deepStrictEqual(actualOutput, expectedOutput); + }); + + it("should not modify a string if it does not end with a comma", () => { + const input = ["hello", "world"]; + const expectedOutput = ["hello", "world"]; + const actualOutput = resultsPanel.removeEndCommaFromStrings(input); + assert.deepStrictEqual(actualOutput, expectedOutput); + }); + + it("should return an empty array if the input is an empty array", () => { + const input: string[] = []; + const expectedOutput: string[] = []; + const actualOutput = resultsPanel.removeEndCommaFromStrings(input); + assert.deepStrictEqual(actualOutput, expectedOutput); + }); + }); + describe("exportToCsv()", () => { it("should show error message if no results to export", () => { const windowMock = sinon.mock(vscode.window); diff --git a/test/suite/utils.test.ts b/test/suite/utils.test.ts index 68b217f8..66ff0b34 100644 --- a/test/suite/utils.test.ts +++ b/test/suite/utils.test.ts @@ -16,11 +16,13 @@ import * as sinon from "sinon"; import * as vscode from "vscode"; import { TreeItemCollapsibleState } from "vscode"; import { ext } from "../../src/extensionVariables"; +import * as QTable from "../../src/ipc/QTable"; import { CancellationEvent } from "../../src/models/cancellationEvent"; import { QueryResultType } from "../../src/models/queryResult"; import { ServerType } from "../../src/models/server"; import { InsightsNode, KdbNode } from "../../src/services/kdbTreeProvider"; import { QueryHistoryProvider } from "../../src/services/queryHistoryProvider"; +import { KdbResultsViewProvider } from "../../src/services/resultsPanelProvider"; import * as coreUtils from "../../src/utils/core"; import * as dataSourceUtils from "../../src/utils/dataSource"; import * as executionUtils from "../../src/utils/execution"; @@ -562,10 +564,55 @@ describe("Utils", () => { assert.strictEqual(sanitizedQuery2, "select from t"); }); - it("handleWSResults", () => { - const ab = new ArrayBuffer(128); - const result = queryUtils.handleWSResults(ab); - assert.strictEqual(result, "No results found."); + describe("getValueFromArray", () => { + it("should return the value of the 'Value' property if the input is an array with a single object with a 'Value' property", () => { + const input = [{ Value: "hello" }]; + const expectedOutput = "hello"; + const actualOutput = queryUtils.getValueFromArray(input); + assert.strictEqual(actualOutput, expectedOutput); + }); + + it("should return the input array if it is not an array with a single object with a 'Value' property", () => { + const input = ["hello", "world"]; + const expectedOutput = ["hello", "world"]; + const actualOutput = queryUtils.getValueFromArray(input); + assert.deepStrictEqual(actualOutput, expectedOutput); + }); + + it("should return the input array if it is an empty array", () => { + const input: any[] = []; + const expectedOutput: any[] = []; + const actualOutput = queryUtils.getValueFromArray(input); + assert.deepStrictEqual(actualOutput, expectedOutput); + }); + }); + + describe("handleWSResults", () => { + it("should return no results found", () => { + const ab = new ArrayBuffer(128); + const result = queryUtils.handleWSResults(ab); + assert.strictEqual(result, "No results found."); + }); + + it("should return the result of getValueFromArray if the results are an array with a single object with a 'Value' property", () => { + const ab = new ArrayBuffer(128); + const expectedOutput = "10"; + const uriTest: vscode.Uri = vscode.Uri.parse("test"); + ext.resultsViewProvider = new KdbResultsViewProvider(uriTest); + const qtableStub = sinon.stub(QTable.default, "toLegacy").returns({ + class: "203", + columns: ["Value"], + meta: { Value: 7 }, + rows: [{ Value: "10" }], + }); + const isVisibleStub = sinon + .stub(ext.resultsViewProvider, "isVisible") + .returns(true); + const convertRowsSpy = sinon.spy(queryUtils, "convertRows"); + const result = queryUtils.handleWSResults(ab); + sinon.assert.notCalled(convertRowsSpy); + assert.strictEqual(result, expectedOutput); + }); }); it("convertRows", () => { @@ -579,14 +626,14 @@ describe("Utils", () => { b: 4, }, ]; - const expectedRes = ["a,b", "1,2", "3,4"].toString(); + const expectedRes = ["a#$#;#$#b", "1#$#;#$#2", "3#$#;#$#4"].toString(); const result = queryUtils.convertRows(rows); assert.equal(result, expectedRes); }); it("convertRowsToConsole", () => { const rows = ["a,b", "1,2", "3,4"]; - const expectedRes = ["a b ", "------", "1 2 ", "3 4 "].toString(); + const expectedRes = ["a,b ", "-----", "1,2 ", "3,4 "].toString(); const result = queryUtils.convertRowsToConsole(rows); assert.equal(result, expectedRes); });