UNPKG

@itwin/core-backend

Version:
420 lines • 18.5 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import * as path from "path"; import * as fs from "fs"; import { marked } from "marked"; export const columnInfoPropsKeys = new Set([ "name", "className", "accessString", "generated", "index", "jsonName", "extendedType", "type", "typeName", "originPropertyName", ]); /* interface QueryPropertyMetaData className: string; accessString?: string; generated: boolean; index: number; jsonName: string; name: string; extendType: string; typeName: string; */ function isColumnInfoProps(obj) { const numberOfKeys = typeof obj === "object" ? Object.keys(obj).length : 0; const isValid = typeof obj === "object" && typeof obj.name === "string" && (obj.className === undefined || typeof obj.className === "string") && (obj.accessString === undefined || typeof obj.accessString === "string") && (obj.generated === undefined || typeof obj.generated === "boolean") && (obj.index === undefined || typeof obj.index === "number") && (obj.jsonName === undefined || typeof obj.jsonName === "string") && (obj.extendedType === undefined || typeof obj.extendedType === "string") && (obj.type === undefined || typeof obj.type === "string") && (obj.typeName === undefined || typeof obj.typeName === "string"); if (!isValid) { const errors = []; if (typeof obj !== "object") errors.push("Object is not of type 'object'"); if (numberOfKeys < 1 || numberOfKeys > 7) errors.push("Number of keys is not between 1 and 7"); if (typeof obj.name !== "string") errors.push("Property 'name' is not of type 'string'"); if (obj.className !== undefined && typeof obj.className !== "string") errors.push("Property 'className' is not of type 'string'"); if (obj.accessString !== undefined && typeof obj.accessString !== "string") errors.push("Property 'accessString' is not of type 'string'"); if (obj.generated !== undefined && typeof obj.generated !== "boolean") errors.push("Property 'generated' is not of type 'boolean'"); if (obj.index !== undefined && typeof obj.index !== "number") errors.push("Property 'index' is not of type 'number'"); if (obj.jsonName !== undefined && typeof obj.jsonName !== "string") errors.push("Property 'jsonName' is not of type 'string'"); if (obj.extendedType !== undefined && typeof obj.extendedType !== "string") errors.push("Property 'extendedType' is not of type 'string'"); if (obj.type !== undefined && typeof obj.type !== "string") errors.push("Property 'type' is not of type 'string'"); if (obj.typeName !== undefined && typeof obj.typeName !== "string") errors.push("Property 'typeName' is not of type 'string'"); logWarning(`Validation failed for ColumnInfoProps. Object: ${JSON.stringify(obj)}. Errors: ${errors.join(", ")}`); } return isValid; } export var ECDbTestMode; (function (ECDbTestMode) { ECDbTestMode["Both"] = "Both"; ECDbTestMode["Statement"] = "Statement"; ECDbTestMode["ConcurrentQuery"] = "ConcurrentQuery"; })(ECDbTestMode || (ECDbTestMode = {})); ; export var ECDbTestRowFormat; (function (ECDbTestRowFormat) { ECDbTestRowFormat["ECSqlNames"] = "ECSqlNames"; ECDbTestRowFormat["ECSqlIndexes"] = "ECSqlIndexes"; ECDbTestRowFormat["JsNames"] = "JsNames"; })(ECDbTestRowFormat || (ECDbTestRowFormat = {})); function tableTextToValue(text) { if (text.startsWith("\"") && text.endsWith("\"")) return text.slice(1, text.length - 1); if (text === "null") return null; if (text === "undefined") return undefined; if (text.startsWith("{") || text.startsWith("[")) return JSON.parse(text); if (text === "true" || text === "false") return text === "true"; if (text.startsWith("0x")) return text; // we use this for IDs and they are handled as strings, the parseInt below would attempt to convert them to numbers if (/^-?\d+(\.\d+)?$/.test(text)) { const flt = parseFloat(text); if (!Number.isNaN(flt)) return flt; } if (/^-?\d+$/.test(text)) { // eslint-disable-next-line radix const asInt = parseInt(text); if (!Number.isNaN(asInt)) return asInt; } return text; } export function buildBinaryData(obj) { for (const key in obj) { if (typeof obj[key] === "string" && obj[key].startsWith("BIN(") && obj[key].endsWith(")")) obj[key] = understandAndReplaceBinaryData(obj[key]); else if (typeof obj[key] === "object" || Array.isArray(obj[key])) obj[key] = buildBinaryData(obj[key]); } return obj; } function understandAndReplaceBinaryData(str) { const startInd = str.indexOf("(") + 1; const endInd = str.indexOf(")"); str = str.slice(startInd, endInd); const ans = []; const numbers = str.split(","); numbers.forEach((value) => { value = value.trim(); // eslint-disable-next-line radix ans.push(parseInt(value)); }); return Uint8Array.of(...ans); } export class ECDbMarkdownTestParser { static parse() { const testAssetsDir = path.join(__dirname, "..", "queries"); const testFiles = fs.readdirSync(testAssetsDir, "utf-8").filter((fileName) => fileName.toLowerCase().endsWith("ecsql.md")); const out = []; for (const fileName of testFiles) { try { const tests = this.parseFile(testAssetsDir, fileName); out.push(...tests); } catch (error) { logWarning(`Failed to parse file ${fileName}. Error: ${error instanceof Error ? error.message : "Unknown error"}`); } } return out; } static parseFile(testAssetsDir, fileName) { const markdownFilePath = path.join(testAssetsDir, fileName); const baseFileName = fileName.replace(/\.ecdbtest\.md$/i, ""); const markdownContent = fs.readFileSync(markdownFilePath, "utf-8"); const tokens = marked.lexer(markdownContent); const out = []; let currentTest; for (const token of tokens) { switch (token.type) { case "space": case "html": case "paragraph": case "hr": continue; case "heading": if (currentTest !== undefined) { out.push(currentTest); } currentTest = { title: token.text, mode: ECDbTestMode.Both, fileName: baseFileName, rowFormat: ECDbTestRowFormat.ECSqlNames, abbreviateBlobs: false, convertClassIdsToClassNames: false }; break; case "list": this.handleListToken(token, currentTest, markdownFilePath); break; case "code": this.handleCodeToken(token, currentTest, markdownFilePath); break; case "table": this.handleTableToken(token, currentTest, markdownFilePath); break; default: logWarning(`Unknown token type ${token.type} found in file ${markdownFilePath}. Skipping.`); break; } } if (currentTest !== undefined) { out.push(currentTest); } return out; } static handleListToken(token, currentTest, markdownFilePath) { if (currentTest === undefined) { logWarning(`List token found without a test title in file ${markdownFilePath}. Skipping.`); return; } const variableRegex = /^(\w+):\s*(.+)$/; const bindRegex = /^bind(\w+)\s([^,\s]+),\s?(.+)$/; for (const item of token.items) { const match = item.text.match(variableRegex); if (match) { const key = match[1]; const value = match[2]; switch (key.toLowerCase()) { case "dataset": currentTest.dataset = value; continue; case "errorduringprepare": currentTest.errorDuringPrepare = value.toLowerCase() === "true"; continue; case "stepstatus": currentTest.stepStatus = value; continue; case "only": currentTest.only = value.toLowerCase() === "true"; continue; case "skip": currentTest.skip = value; continue; case "mode": this.handleMode(value, currentTest, markdownFilePath); continue; case "rowformat": this.handleRowFormat(value, currentTest, markdownFilePath); continue; case "abbreviateblobs": currentTest.abbreviateBlobs = value.toLowerCase() === "true"; continue; case "convertclassidstoclassnames": currentTest.convertClassIdsToClassNames = value.toLowerCase() === "true"; continue; case "indexestoinclude": currentTest.indexesToIncludeInResults = this.handleValidIndexList(value); continue; } } const bindMatch = item.text.match(bindRegex); if (bindMatch) { currentTest.binders = currentTest.binders || []; currentTest.binders.push({ indexOrName: bindMatch[2], type: bindMatch[1], value: bindMatch[3] }); continue; } } } static handleMode(value, currentTest, markdownFilePath) { switch (value.toLowerCase()) { case "statement": currentTest.mode = ECDbTestMode.Statement; break; case "concurrentquery": currentTest.mode = ECDbTestMode.ConcurrentQuery; break; case "both": currentTest.mode = ECDbTestMode.Both; break; default: logWarning(`Mode value (${value}) is not recognized in file ${markdownFilePath} and test ${currentTest.title}. Skipping.`); } } static handleRowFormat(value, currentTest, markdownFilePath) { switch (value.toLowerCase()) { case "ecsqlnames": currentTest.rowFormat = ECDbTestRowFormat.ECSqlNames; break; case "ecsqlindexes": currentTest.rowFormat = ECDbTestRowFormat.ECSqlIndexes; break; case "jsnames": currentTest.rowFormat = ECDbTestRowFormat.JsNames; break; default: logWarning(`Row Format value (${value}) is not recognized in file ${markdownFilePath} and test ${currentTest.title}. Skipping.`); } } static handleValidIndexList(obj) { try { const numsArr = JSON.parse(obj); if (Array.isArray(numsArr) && numsArr.every((val) => typeof val === "number")) return numsArr; logWarning("The given value is not valid for the property indexestoinclude"); return undefined; } catch { logWarning("The given value is not valid for the property indexestoinclude"); return undefined; } } static handleCodeToken(token, currentTest, markdownFilePath) { if (currentTest === undefined) { logWarning(`Code token found without a test title in file ${markdownFilePath}. Skipping.`); return; } if (token.lang === "sql") { currentTest.sql = token.text; } else if (token.lang === "json") { let json; try { json = JSON.parse(token.text); } catch (error) { if (error instanceof Error) { logWarning(`Failed to parse JSON ${token.text} in file ${markdownFilePath}. ${error.message} Skipping.`); } else { logWarning(`Failed to parse SON ${token.text} in file ${markdownFilePath}. Unknown error. Skipping.`); } } if (typeof json === "object" && Array.isArray(json.columns)) { this.handleJSONColumnMetadata(json, currentTest, markdownFilePath); return; } this.handleJSONExpectedResults(json, currentTest); // TODO: validate the expected results } else { logWarning(`Unknown code language ${token.lang} found in file ${markdownFilePath}. Skipping.`); } } static handleJSONColumnMetadata(json, currentTest, markdownFilePath) { const extraProps = new Set(); if (json.columns.every(isColumnInfoProps)) { currentTest.columnInfo = json.columns; for (const column of json.columns) { for (const key in column) { if (!columnInfoPropsKeys.has(key)) { extraProps.add(key); } } } if (extraProps.size > 0) { logWarning(`Found extra properties in column infos: ${Array.from(extraProps).join(", ")} in file '${markdownFilePath}' test '${currentTest.title}'.`); } } else { logWarning(`Columns format in file '${markdownFilePath}' test '${currentTest.title}' failed type guard. Skipping.`); } } static handleJSONExpectedResults(json, currentTest) { currentTest.expectedResults = json; } static handleTableToken(token, currentTest, markdownFilePath) { if (currentTest === undefined) { logWarning(`Table token found without a test title in file ${markdownFilePath}. Skipping.`); return; } this.handleTable(token, currentTest, markdownFilePath); } static handleTable(token, currentTest, markdownFilePath) { if (token.header.length > 0 && currentTest.columnInfo === undefined && columnInfoPropsKeys.has(token.header[0].text)) { this.handleColumnTable(token, currentTest, markdownFilePath); return; } else if (token.header.length > 0 && token.header[0].text === "") { this.handleExpectedResultsTableForECSqlPropertyIndexesOption(token, currentTest, markdownFilePath); } else { this.handleExpectedResultsTable(token, currentTest, markdownFilePath); } } static handleColumnTable(token, currentTest, markdownFilePath) { const columnInfos = []; for (const row of token.rows) { if (row.length < 1 || row.length !== token.header.length) { logWarning(`Rows in a expected result table must have a minimum of 1 cell, and as many cells as there are headers. ${markdownFilePath}. Skipping.`); continue; } const columnInfo = {}; for (let i = 0; i < token.header.length; i++) { const header = token.header[i].text; const cell = row[i].text; columnInfo[header] = tableTextToValue(cell); } columnInfos.push(columnInfo); } this.handleJSONColumnMetadata({ columns: columnInfos }, currentTest, markdownFilePath); } static handleExpectedResultsTable(token, currentTest, markdownFilePath) { if (currentTest.expectedResults !== undefined) { logWarning(`Expected results already set for test ${currentTest.title} in file ${markdownFilePath}. Skipping.`); return; } currentTest.expectedResults = []; for (const row of token.rows) { if (row.length < 1 || row.length !== token.header.length) { logWarning(`Rows in a expected result table must have a minimum of 1 cell, and as many cells as there are headers. ${markdownFilePath}. Skipping.`); continue; } const expectedResult = {}; for (let i = 0; i < token.header.length; i++) { const header = token.header[i].text; const cell = row[i].text; const value = tableTextToValue(cell); if (value !== undefined) expectedResult[header] = value; } currentTest.expectedResults.push(expectedResult); } } static handleExpectedResultsTableForECSqlPropertyIndexesOption(token, currentTest, markdownFilePath) { if (currentTest.expectedResults !== undefined) { logWarning(`Expected results already set for test ${currentTest.title} in file ${markdownFilePath}. Skipping.`); return; } currentTest.expectedResults = []; for (const row of token.rows) { if (row.length < 1 || row.length !== token.header.length) { logWarning(`Rows in a expected result table must have a minimum of 1 cell, and as many cells as there are headers. ${markdownFilePath}. Skipping.`); continue; } const expectedResult = []; for (let i = 0; i < token.header.length; i++) { const cell = row[i].text; const value = tableTextToValue(cell); if (value !== undefined) expectedResult.push(value); } currentTest.expectedResults.push(expectedResult); } } } function logWarning(message) { // eslint-disable-next-line no-console console.log(`\x1b[33m${message}\x1b[0m`); } //# sourceMappingURL=ECSqlTestParser.js.map