diff --git a/package.json b/package.json index 397ad714..edac6c62 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Materials Design in Javascript (made.js)", "scripts": { "postinstall": "mkdir -p lib; babel src --out-dir lib", - "test": "mocha $NODE_DEBUG_OPTION --recursive --bail --require @babel/register/lib tests" + "test": "cucumber-js --require-module @babel/register --format node_modules/cucumber-pretty --require tests/cucumber/support tests/cucumber/features" }, "repository": { "type": "git", @@ -17,17 +17,20 @@ }, "homepage": "https://github.com/Exabyte-io/made-js", "devDependencies": { - "mocha": "^2.5.3", "chai": "^3.5.0", - "fs-extra": "^4.0.0" + "cucumber": "^5.1.0", + "cucumber-pretty": "^1.5.0", + "fs-extra": "^4.0.0", + "mocha": "^2.5.3", + "random-seed": "^0.3.0" }, "dependencies": { "@babel/cli": "7.0.0", "@babel/core": "7.0.0", - "@babel/register": "7.0.0", + "@babel/plugin-proposal-class-properties": "7.0.0", "@babel/polyfill": "7.0.0", "@babel/preset-env": "7.0.0", - "@babel/plugin-proposal-class-properties": "7.0.0", + "@babel/register": "7.0.0", "array-almost-equal": "^1.0.0", "crypto-js": "^3.1.9-1", "lodash": "^4.17.4", diff --git a/tests/cucumber/features/formula.feature b/tests/cucumber/features/formula.feature new file mode 100644 index 00000000..18f7a52b --- /dev/null +++ b/tests/cucumber/features/formula.feature @@ -0,0 +1,8 @@ +Feature: formula is extracted properly from Basis class. + + Scenario: + When Basis is created with the following data: + | basis | cacheKey | + | $BASIS{Si 0 0 0, Ge 0.25 0.25 0.25} | basis | + Then Basis stored under "basis" key returns "SiGe" as formula + diff --git a/tests/cucumber/support/basis/step_definitions/Then Basis stored under # key returns # as formula.js b/tests/cucumber/support/basis/step_definitions/Then Basis stored under # key returns # as formula.js new file mode 100644 index 00000000..ee5f26ae --- /dev/null +++ b/tests/cucumber/support/basis/step_definitions/Then Basis stored under # key returns # as formula.js @@ -0,0 +1,6 @@ +import assert from "assert"; +import {Then} from "cucumber"; + +Then(/^Basis stored under "([^"]*)" key returns "([^"]*)" as formula$/, function (cacheKey, formula) { + assert.equal(this["cacheKey"].formula, formula); +}); diff --git a/tests/cucumber/support/basis/step_definitions/When Basis is created with the following data.js b/tests/cucumber/support/basis/step_definitions/When Basis is created with the following data.js new file mode 100644 index 00000000..65e55252 --- /dev/null +++ b/tests/cucumber/support/basis/step_definitions/When Basis is created with the following data.js @@ -0,0 +1,9 @@ +import {When} from "cucumber"; + +import {parseTable} from "../../table"; +import {Basis} from "../../../../../src/basis/basis"; + +When(/^Basis is created with the following data:$/, function (table) { + const config = parseTable(table, this)[0]; + this["cacheKey"] = new Basis(config.basis); +}); diff --git a/tests/cucumber/support/logger.js b/tests/cucumber/support/logger.js new file mode 100644 index 00000000..89308a92 --- /dev/null +++ b/tests/cucumber/support/logger.js @@ -0,0 +1,43 @@ +import debug from "debug"; + +const LEVELS = { + debug: 3, + warn: 2, + info: 1, + error: 0 +}; + +const DEBUG_LEVEL = process.env.DEBUG_LEVEL || LEVELS.warn; + +/** + * @summary Returns a logger with specified name. + * @param name {String} E.g. "exachimp:widget" + */ +export function getLogger(name) { + const log = debug(name); + + return { + debug(message) { + if (DEBUG_LEVEL >= LEVELS.debug) { + log(message); + } + }, + warn(message) { + if (DEBUG_LEVEL >= LEVELS.warn) { + log(message); + } + }, + info(message) { + if (DEBUG_LEVEL >= LEVELS.info) { + log(message); + } + }, + error(message) { + if (DEBUG_LEVEL >= LEVELS.error) { + log(message); + } + } + } +} + +export const logger = getLogger("exachimp:default"); diff --git a/tests/cucumber/support/table.js b/tests/cucumber/support/table.js new file mode 100644 index 00000000..21eb7639 --- /dev/null +++ b/tests/cucumber/support/table.js @@ -0,0 +1,147 @@ +import _ from "underscore"; +import lodash from "lodash"; +import random from "random-seed"; + +import {getCacheValue} from "./utils/cache"; + +const REGEXES = [ + { + name: "DATE_REGEX", + regex: /^\$DATE\{(.*)}/, + func: (str, regex) => new Date(str.match(regex)[1]) + }, + { + name: "BOOLEAN_REGEX", + regex: /^\$BOOLEAN\{(.*)}/, + func: (str, regex) => JSON.parse(str.match(regex)[1]) + }, + { + name: "ARRAY_REGEX", + regex: /^\$ARRAY\{(.*)}/, + func: (str, regex) => str.match(regex)[1].split(",") + }, + { + name: "INT_REGEX", + regex: /^\$INT\{(.*)}/, + func: (str, regex) => parseInt(str.match(regex)[1]) + }, + { + name: "FLOAT_REGEX", + regex: /^\$FLOAT\{(.*)}/, + func: (str, regex) => parseFloat(str.match(regex)[1]) + }, + { + name: "BASIS_REGEX", + regex: /^\$BASIS\{(.*)}/, + func: (str, regex) => parseBasisStr(str.match(regex)[1]) + }, + { + name: "EXPR_REGEX", + regex: /^\$\{(.*)}/, + func: (str, regex, context) => { + const value = str.match(regex)[1]; + return value.split(',').map(evalExpression).reduceRight((mem, part) => part + mem, ''); + } + }, + { + name: "CACHE_REGEX", + regex: /\$CACHE\{([^\{^}]*)}/, + func: (str, regex, context) => { + const value = str.match(regex)[1]; + const [contextKey, property] = value.split(':'); + return parseValue(str.replace(`$CACHE{${value}}`, lodash.get(getCacheValue(context, contextKey), property)), context); + } + } +]; + +/** + * Checks whether passed string is integer number. + */ +function isInteger(str) { + return /^\d+$/.test(str); +} + +/** + * Parses basis string in format "Si 0 0 0, Li 0.5 0.5 0.5" and returns it as an object in exabyte internal format. + */ +function parseBasisStr(str) { + const lines = str.split(/[,;]/).map(x => x.trim()); + const basis = { + elements: [], + coordinates: [], + units: 'crystal' + }; + for (var i = 0; i < lines.length; i++) { + var items = lines[i].split(' '); + basis.elements.push({ + id: i + 1, + value: items[0] + }); + basis.coordinates.push({ + id: i + 1, + value: [items[1], items[2], items[3]].map(parseFloat) + }); + } + return basis; +} + +/** + * Evaluates expression from passed string. + * See https://www.jayway.com/2012/04/03/cucumber-data-driven-testing-tips/ for more information. + * The rules are: + * - ${} – everything inside will be parsed, strings are comma separated + * - numerical value – create random alphanumeric string + * - ! – use the string as it is + * - N – random numbers + * - d - date in + * Math.random().toString(36) is used to generate random string. + */ +function evalExpression(str) { + if (isInteger(str)) { + /** + * Seed is generated by timestamp + random string. + * Additional random string is required, because using only seed for generating string in loop + * will cause random string duplication. + */ + var seed = new Date().getTime().toString() + Math.random().toString(36); + var rand = random.create(seed); + var alphabet = "abcdefghijklmnopqrstuvwxyz"; + var randomLetter = alphabet[Math.floor(rand.random() * alphabet.length)]; + // !!!IMPORTANT Random letter is required in generated string because of + // issue https://exabyte.atlassian.net/browse/SOF-1719 + // Generated string is used for username generation. In case of random string contains only numbers + // slug for default issue will be inappropriate (e.g., "user-1232" has "user" slug). + return randomLetter + rand.random().toString(36).substring(2, 2 + parseInt(str) - 1); + } else if (str.indexOf('!') === 0) { + // ! – use the string as it is + return str.substring(1); + } else if (str.indexOf('N') === 0) { + // random numbers + var result = '', max = 9, min = 0; + var count = parseInt(str.substring(1)); + var i = 0; + for (; i < count; i++) { + result += Math.floor(Math.random() * (max - min + 1)) + min; + } + return result; + } +} + +/** + * Parses passed string and returns evaluated value. + */ +export function parseValue(str, context, key) { + if (!_.isString(str)) throw new Error('Argument should be string'); + const config = REGEXES.find(config => str.match(config.regex)); + return config ? config.func(str, config.regex, context) : str; +} + +/** + * @summary Parses values from table rows. Each column's value for each row are parsed by parseValue. + * @param table {Object} Table passed from Cucumber step definition. + * @param context {Any} Context for extracting cached values. + * @return {Object} + */ +export function parseTable(table, context) { + return table.hashes().map(hash => _.mapObject(hash, (value, key) => parseValue(value, context, key))); +} diff --git a/tests/cucumber/support/utils/cache.js b/tests/cucumber/support/utils/cache.js new file mode 100644 index 00000000..973cc360 --- /dev/null +++ b/tests/cucumber/support/utils/cache.js @@ -0,0 +1,19 @@ +import {logger} from "../logger"; + +export function getCacheValue(context, key) { + return context[key] +} + +export function setCacheValue(context, key, value) { + context[key] = value; +} + +/** + * @summary Stores a given entity config in the context. + * when useCollectionItem is passed collection item is stored instead of config. + */ +export function cacheEntityData(config, context, entityId, entityTAO) { + if (!config.cacheKey || !context) return; + logger.debug(`Entity with name "${config.name || config.username}" is stored under cacheKey "${config.cacheKey}"`); + setCacheValue(context, config.cacheKey, config.useCollectionItem ? entityTAO.findById(entityId) : config); +}