@itwin/core-backend
Version:
iTwin.js backend components
420 lines • 18.5 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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