UNPKG

slicerjs

Version:

Official JavaScript client for SlicingDice, Data Warehouse and Analytics Database as a Service.

573 lines (488 loc) 18.3 kB
/* Tests SlicingDice endpoints. This script tests SlicingDice by running tests suites, each composed by: - Creating columns - Inserting data - Querying - Comparing results All tests are stored in JSON files at ./examples named as the query being tested: - count_entity.json - count_event.json In order to execute the tests, simply replace API_KEY by the demo API key and run the script with: $ node run_tests.js */ "use strict"; var SlicingDice = require('../src/slicer.js'); var errors = require('../src/errors.js'); var fs = require('fs'); var async = require('async'); var https = require('https'); var sleep = require('sleep'); var HAS_FAILED_TESTS = false; var perTestInsertion; class SlicingDiceTester { constructor(api_key, verbose=false) { this.client = new SlicingDice({masterKey: api_key}); // Translation table for columns with timestamp this.columnTranslation = {} // Sleep Time in seconds this.sleepTime = 20; // Directory containing examples to test this.path = 'examples/'; // Examples file format this.extension = '.json'; this.numSuccesses = 0; this.numFails = 0; this.failedTests = []; this.verbose = verbose; this.perTestInsertion; this.insertSqlData = false; String.prototype.replaceAll = function(search, replacement) { var target = this; return target.replace(new RegExp(search, 'g'), replacement); }; String.prototype.format = function() { var args = arguments; return this.replace(/{(\d+)}/g, function(match, number) { return typeof args[number] != 'undefined' ? args[number] : match ; }); }; } /* Run all tests for a determined query type * * @param (string) queryType - the type of the query to test */ runTests(queryType, callback) { let testData = this.loadTestData(queryType); let numTests = testData.length; let result; this.perTestInsertion = testData[0].hasOwnProperty("insert"); if (!this.perTestInsertion && this.insertSqlData) { let insertData = this.loadTestData(queryType, "_insert"); for (let i = 0; i < insertData.length; i++) { async.series([ (callback) => { this.client.insert(insertData[i], false).then(() => { // Wait a few seconds so the data can be inserted by SlicingDice callback(); }, (err) => { callback(); }); } ]); } sleep.sleep(this.sleepTime); } let tasks = []; for(let i = 0; i < numTests; i++){ let _queryType = queryType; tasks.push(((i) => (callback) => { try { let test = testData[i]; this._emptyColumnTranslation(); console.log("({0}/{1}) Executing test \"{2}\"".format(i + 1, numTests, test['name'])); if("description" in test){ console.log(" Description: {0}".format(test['description'])); } console.log(" Query type: {0}".format(queryType)); if (this.perTestInsertion) { async.series([ (callback) => { this.createColumns(test, callback); }, (callback) => { this.insertData(test, callback); }, (callback) => { this._runAdditionalOperations(queryType, test, callback); } ], callback); } else { async.series([ (callback) => { this.executeQuery(queryType, test, callback); } ], callback); } } catch (err) { callback(); } })(i)); } async.series(tasks, callback); } // Method used to run delete and update operations, this operations // are executed before the query and the result comparison _runAdditionalOperations(queryType, test, callback) { if (queryType === "delete" || queryType === "update") { let queryData = this._translateColumnNames(test['additional_operation']); if (queryType == "delete") { console.log(" Deleting"); } else { console.log(" Updating"); } if (this.verbose){ console.log(" - {0}".format(queryData)); } if (queryType == "delete") { async.series([(callback) => { this.client.delete(queryData).then((resp) => { this.compareResultAndMakeQuery('count_entity', test, callback, resp).then((resp) => { callback(); }, (err) => { throw "An error occurred"; }); }, (err) => { throw "An error occurred"; }) }], callback); } else if (queryType == "update") { async.series([(callback) => { this.client.update(queryData).then((resp) => { this.compareResultAndMakeQuery('count_entity', test, callback, resp).then((resp) => { callback(); }, (err) => { throw "An error occurred"; }); }, (err) => { throw "An error occurred"; }) }], callback); } } else { async.series([(callback) => { this.executeQuery(queryType, test, callback); }], callback); } } compareResultAndMakeQuery(queryType, test, callback, result) { async.series([(callback) => { let expected = this._translateColumnNames(test['result_additional']); for(var key in expected) { let value = expected[key]; if (value === 'ignore') { continue; } if (!this.compareJson(expected[key], result[key])){ this.numFails += 1; this.failedTests.push(test['name']); console.log(" Expected: \"{0}\": {1}".format(key, JSON.stringify(expected[key]))); console.log(" Result: \"{0}\": {1}".format(key, JSON.stringify(result[key]))); console.log(" Status: Failed\n"); this.updateResult(); return; } this.numSuccesses += 1; console.log(' Status: Passed\n'); this.updateResult(); } this.executeQuery(queryType, test, callback); }], callback); } // Erase columnTranslation object _emptyColumnTranslation(){ this.columnTranslation = {}; } // Load test data from examples files loadTestData(queryType, suffix = "") { let filename = this.path + queryType + suffix + this.extension; return JSON.parse(fs.readFileSync(filename)); } /* Create columns on Slicing Dice API * * @param (array) test - the test data containing the column to create */ createColumns(test, callback){ let isSingular = test['columns'].length == 1; let column_or_columns; if (isSingular){ column_or_columns = 'column'; } else{ column_or_columns = 'columns'; } console.log(" Creating {0} {1}".format(test['columns'].length, column_or_columns)); let tasks = []; for(var data in test['columns']) { let column = test['columns'][data]; this._appendTimestampToColumnName(column); tasks.push((callback) => { this.client.createColumn(column).then((resp) => { callback(); }, (err) => { this.compareResult(test, err); callback(); }); }); if (this.verbose){ console.log(" - {0}".format(column['api-name'])); } } async.series(tasks, callback); } /* Append integer timestamp to column name * * This technique allows the same test suite to be executed over and over * again, since each execution will use different column names. * * @param (array) column - array containing column name */ _appendTimestampToColumnName(column){ let oldName = '"{0}"'.format(column['api-name']); let timestamp = this._getTimestamp(); column['name'] += timestamp column['api-name'] += timestamp let newName = '"{0}"'.format(column['api-name']) this.columnTranslation[oldName] = newName } // Get actual timestamp in string format _getTimestamp(){ return new Date().getTime().toString(); } /* Insert data on Slicing Dice API * * @param (array) test - the test data containing the data to insert on Slicing Dice API */ insertData(test, callback) { let isSingular = test['insert'].length == 1; let column_or_columns; if (isSingular){ column_or_columns = 'entity'; } else{ column_or_columns = 'entities'; } console.log(" Inserting {0} {1}".format(Object.keys(test['insert']).length, column_or_columns)); let insertData = this._translateColumnNames(test['insert']); if (this.verbose){ console.log(insertData); } this.client.insert(insertData, false).then(() => { // Wait a few seconds so the data can be inserted by SlicingDice sleep.sleep(this.sleepTime); callback(); }, (err) => { this.compareResult(test, err); callback(); }); } /* Execute query on Slicing Dice API * * @param (string) queryType - the type of the query to send * @param (string) test - the test data containing the data to query on Slicing Dice API */ executeQuery(queryType, test, callback) { let result; let queryData; if (this.perTestInsertion) { queryData = this._translateColumnNames(test['query']); } else { queryData = test['query']; } console.log(' Querying'); if (this.verbose){ console.log(' - ' + JSON.stringify(queryData)); } var queryTypeMethodMap = { 'count_entity': 'countEntity', 'count_event': 'countEvent', 'top_values': 'topValues', 'aggregation': 'aggregation', 'result': 'result', 'score': 'score', 'sql': 'sql' }; this.client[queryTypeMethodMap[queryType]](queryData).then((resp) =>{ this.compareResult(test, resp); callback(); }, (err) =>{ this.compareResult(test, err); callback(err); }) } /* Translate column name to match column name with timestamp * * @param (array) jsonData - the json to translate the column name */ _translateColumnNames(jsonData){ let dataString = JSON.stringify(jsonData); for(var oldName in this.columnTranslation){ let newName = this.columnTranslation[oldName]; dataString = dataString.replaceAll(oldName, newName); } return JSON.parse(dataString); } /* Compare the result received from Slicing Dice API and the expected * * @param (array) test - the data expected * @param (array) result - the data received from Slicing Dice API */ compareResult(test, result) { let expected; if (this.perTestInsertion) { expected = this._translateColumnNames(test['expected']); } else { expected = test['expected']; } let dataExpected = test['expected']; for(var key in dataExpected) { let value = dataExpected[key]; if (value === 'ignore') { continue; } if (!this.compareJson(expected[key], result[key])){ this.numFails += 1; this.failedTests.push(test['name']); console.log(" Expected: \"{0}\": {1}".format(key, JSON.stringify(expected[key]))); console.log(" Result: \"{0}\": {1}".format(key, JSON.stringify(result[key]))); console.log(" Status: Failed\n"); this.updateResult(); return; } this.numSuccesses += 1; console.log(' Status: Passed\n'); this.updateResult(); } } /* Compare two JSON's * * @param (object) expected - the data expected * @param (object) result - the data received from Slicing Dice API */ compareJson(expected, result) { if (typeof expected !== typeof result) return false; if (expected.constructor !== result.constructor) return false; if (expected instanceof Array) { return this.arrayEqual(expected, result); } if (typeof expected === "object") { return this.compareJsonValue(expected, result); } if (isNaN(expected)) { return expected === result; } return this.numberIsClose(expected, result); } numberIsClose(a, b, rel_tol=1e-09, abs_tol=0.0) { return Math.abs(a - b) <= Math.max(rel_tol * Math.max(Math.abs(a), Math.abs(b)), abs_tol); } /* Compare two JSON's values * * @param (object) expected - the data expected * @param (object) result - the data received from Slicing Dice API */ compareJsonValue(expected, result) { for (let key in expected) { if (expected.hasOwnProperty(key)) { if (!result.hasOwnProperty(key)) { return false; } if (!this.compareJson(expected[key], result[key])) { return false; } } } return true; } /* Compare two JSON's arrays * * @param (array) expected - the data expected * @param (array) result - the data received from Slicing Dice API */ arrayEqual(expected, result) { if (expected.length !== result.length) { return false; } let i = expected.length; while (i--) { let j = result.length; let found = false; while (!found && j--) { if (this.compareJson(expected[i], result[j])) { found = true; } } if (!found) { return false; } } return true; } // Write a file testResult.tmp with the result of the tests updateResult() { if (process.platform == "win32") { return; } let finalMessage; let failedTestsStr = ""; if (this.failedTests.length > 0){ for(let item in this.failedTests) { failedTestsStr += " - {0}\n".format(this.failedTests[item]); } } if (this.numFails > 0){ HAS_FAILED_TESTS = true; let isSingular = this.numFails == 1; let testOrTests = null; if (isSingular){ testOrTests = "test has"; } else { testOrTests = "tests have"; } finalMessage = "FAIL: {0} {1} failed\n".format(this.numFails, testOrTests); } else { finalMessage = "SUCCESS: All tests passed\n"; } let content = "\nResults:\n Successes: {0}\n Fails: {1} \n{2}\n{3}".format(this.numSuccesses, this.numFails, failedTestsStr, finalMessage); fs.writeFile('testResult.tmp', content, function (err) { if (err) return console.log(err); }); } } // Show results of the test saved on testResult.tmp file function showResults(){ console.log(fs.readFileSync('testResult.tmp', 'utf8')); fs.unlink('testResult.tmp', (err) => { if (err) throw err; }); if (HAS_FAILED_TESTS) process.exit(1); process.exit(0); } process.on('SIGINT', function() { showResults(); }); function main(){ // SlicingDice queries to be tested. Must match the JSON file name. let queryTypes = [ 'count_entity', 'count_event', 'top_values', 'aggregation', 'result', 'score', 'sql', 'delete', 'update' ]; let apiKey = process.env.SD_API_KEY; if (apiKey === undefined){ apiKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfX3NhbHQiOiIxNTI0MjQ4NzIwNzg2IiwicGVybWlzc2lvbl9sZXZlbCI6MywicHJvamVjdF9pZCI6MzAwMjksImNsaWVudF9pZCI6MTF9.SaRJX3Li_0AcrfDFaMOPHJNS9oGxYPjkYmpEza00IMk"; } // Testing class with demo API key // To get a demo api key visit: http://panel.slicingdice.com/docs/#api-details-api-connection-api-keys-demo-key let sdTester = new SlicingDiceTester(apiKey, false); let tests = []; for(let i = 0; i < queryTypes.length; i++) { tests.push((callback) => sdTester.runTests(queryTypes[i], callback)); } async.series(tests, () => { showResults(); }); } if (require.main === module) { main(); }