"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const util_1 = __importDefault(require("util")); const async_1 = __importDefault(require("async")); const strip_bom_1 = __importDefault(require("strip-bom")); const pinkie_1 = __importDefault(require("pinkie")); const uglify_js_1 = require("../tools/uglify-js/uglify-js"); const uglify_js_2 = require("../tools/uglify-js/uglify-js"); const Common = __importStar(require("./common")); const Ast = __importStar(require("./ast")); const CallAnalyzer = __importStar(require("./analysis/call_analyzer")); const steps_analyzer_1 = __importDefault(require("./analysis/steps_analyzer")); const ErrCodes = __importStar(require("./err_codes")); const promisify_1 = __importDefault(require("../../utils/promisify")); var readFile = (0, promisify_1.default)(fs_1.default.readFile); //Util //NOTE: this is a version of splice which can operate with array of the injectable items function multySplice(arr, index, deleteCount, itemsToInsert) { var args = [index, deleteCount].concat(itemsToInsert); arr.splice.apply(arr, args); } //Compiler function Compiler(src, filename, modules, requireReader, sourceIndex, hammerheadProcessScript) { this.walker = uglify_js_1.uglify.ast_walker(); this.hammerheadProcessScript = hammerheadProcessScript; this.filename = filename; this.src = src; this.workingDir = path_1.default.dirname(this.filename); this.modules = modules; this.requireReader = requireReader; this.sourceIndex = sourceIndex || []; this.requires = []; this.line = 0; this.errs = []; this.okFlag = true; this.rawTestsStepData = {}; this.rawMixinsStepData = {}; this.out = { fixture: '', page: '', authCredentials: null, requireJs: '', remainderJs: '', testsStepData: {}, workingDir: this.workingDir, testGroupMap: {} }; this.testAnalyzer = new steps_analyzer_1.default(false, this.rawTestsStepData, this.errs, this.sourceIndex); this.mixinAnalyzer = new steps_analyzer_1.default(true, this.rawMixinsStepData, this.errs, this.sourceIndex); } exports.default = Compiler; ; Object.defineProperties(Compiler.prototype, { ok: { get: function () { return !this.errs.length && this.okFlag; }, set: function (value) { this.okFlag = value; } } }); Compiler.prototype._err = function (type, filename, line, additionalFields) { this.errs.push(Common.createErrorObj(type, filename, line, additionalFields)); }; Compiler.prototype._fixtureErr = function (type, line, additionalFields) { this._err(type, this.filename, line, additionalFields); }; Compiler.prototype._addRequire = function (require) { if (this.requires.indexOf(require) > -1) this._fixtureErr(ErrCodes.REQUIRED_FILE_ALREADY_INCLUDED, this.line, { req: require }); else this.requires.push(require); }; Compiler.prototype._compileDirective = function (match) { switch (match[1]) { case Common.AUTH_DIRECTIVE_LVALUE: if (this.out.authCredentials) { this._fixtureErr(ErrCodes.AUTH_DIRECTIVE_REDEFINITION, this.line); break; } var credentials = Common.AUTH_CREDENTIALS_REGEXP.exec(match[2]); if (!credentials || credentials.length < 3) { this._fixtureErr(ErrCodes.INVALID_NETWORK_AUTHENTICATION_CREDENTIALS_FORMAT, this.line); break; } this.out.authCredentials = { username: credentials[1], password: credentials[2] }; break; case Common.FIXTURE_DIRECTIVE_LVALUE: if (this.out.fixture) { this._fixtureErr(ErrCodes.FIXTURE_DIRECTIVE_REDEFINITION, this.line); break; } this.out.fixture = match[2]; break; case Common.PAGE_DIRECTIVE_LVALUE: if (this.out.page) { this._fixtureErr(ErrCodes.PAGE_DIRECTIVE_REDEFINITION, this.line); break; } this.out.page = match[2]; break; case Common.REQUIRE_DIRECTIVE_LVALUE: if (match[2].indexOf(Common.MODULE_PREFIX) === 0) { var moduleName = match[2].slice(1), moduleFiles = this.modules && this.modules[moduleName]; if (!moduleFiles) { this._fixtureErr(ErrCodes.MODULE_NOT_FOUND, this.line, { moduleName: moduleName }); break; } var compiler = this; moduleFiles.forEach(function (moduleFile) { compiler._addRequire(moduleFile); }); break; } var require = path_1.default.join(this.workingDir, match[2]); this._addRequire(require); break; default: return false; } //NOTE: valid directive expression must match following AST path: //'toplevel' -> 'stat' -> [string, DIRECTIVE_EXPRESSION]. //Also it's important to perform this test here, when we sure that this is a directive expression //and not just a string that starts with @ if (!Ast.isPathMatch(Common.DIRECTIVE_EXPRESSION_AST_PATH, this.walker.stack())) this._fixtureErr(ErrCodes.MISPLACED_DIRECTIVE, this.line); return true; }; Compiler.prototype._getRemainderCode = function (ast) { var remainderAst = Ast.getRemainderAst(ast); if (remainderAst) { CallAnalyzer.run(remainderAst, this.filename, this.errs, true, this.sourceIndex, this.src); if (this.ok) { var remainderCode = uglify_js_1.uglify.gen_code(remainderAst, { beautify: true }); return this.hammerheadProcessScript(remainderCode, false); } } return ''; }; Compiler.prototype._analyzeAst = function (ast) { var compiler = this; this.walker.with_walkers({ 'string': function () { var astPath = compiler.walker.stack(), topStatement = astPath[1][0]; compiler.line = Ast.getCurrentSrcLineNum(astPath); var isMixinDeclaration = this[1] === Common.MIXIN_DECLARATION_MARKER, isTestDeclaration = this[1] === Common.TEST_DECLARATION_MARKER; if (isMixinDeclaration || isTestDeclaration) { topStatement.remove = true; var analyzer = isMixinDeclaration ? compiler.mixinAnalyzer : compiler.testAnalyzer; analyzer.run(compiler.walker.stack(), compiler.filename, compiler.src); return; } // NOTE: Try to find directive expression. // It's a string statement that match DIRECTIVE_EXPRESSION_PATTERN var match = Common.DIRECTIVE_EXPRESSION_PATTERN.exec(this[1]); if (match) { //NOTE: do not unset 'remove' flag if it was already set by test facility parsing algorithm var success = compiler._compileDirective(match); topStatement.remove = topStatement.remove || success; } } }, function () { compiler.walker.walk(ast); }); }; //Requires Compiler.prototype._mergeRequireMixins = function (requireDescriptor) { var compiler = this; Object.keys(requireDescriptor.rawMixinsStepData).forEach(function (name) { if (compiler.rawMixinsStepData[name]) { compiler._fixtureErr(ErrCodes.DUPLICATE_MIXIN_NAME_IN_REQUIRE, null, { name: name, defFilename1: requireDescriptor.filename, defFilename2: compiler.rawMixinsStepData[name].reqFilename || compiler.filename }); } else { compiler.rawMixinsStepData[name] = requireDescriptor.rawMixinsStepData[name]; compiler.rawMixinsStepData[name].reqFilename = requireDescriptor.filename; } }); }; Compiler.prototype._analyzeRequires = function (callback) { var requireReaderPromises = this.requires.map(require => { return this.requireReader .read(require, this.filename, this.sourceIndex) .then(res => { var descriptor = res.descriptor; if (res.fromCache) this.ok = this.ok && !descriptor.hasErrs; else this.errs = this.errs.concat(res.errs); return descriptor; }); }); pinkie_1.default.all(requireReaderPromises) .then(descriptors => { descriptors.forEach(descriptor => { this._mergeRequireMixins(descriptor); if (this.ok) this.out.requireJs += descriptor.jsCode; }); callback(); }); }; //Test steps data compilation Compiler.prototype._insertMixins = function (testStepData) { var stepCount = testStepData.names.length; for (var i = 0; i < stepCount; i++) { var ast = testStepData.asts[i]; if (ast.isMixinInsertionPoint) { var mixinStepData = this.rawMixinsStepData[ast.mixinName]; if (!mixinStepData) { this._fixtureErr(ErrCodes.UNDEFINED_MIXIN_USED, ast.line, { mixinName: ast.mixinName }); continue; } var mixinStepNames = [], stepName = testStepData.names[i]; for (var j = 0; j < mixinStepData.names.length; j++) mixinStepNames.push(stepName + Common.TEST_MIXIN_STEP_NAME_SEPARATOR + mixinStepData.names[j]); multySplice(testStepData.names, i, 1, mixinStepNames); multySplice(testStepData.asts, i, 1, mixinStepData.asts); i += mixinStepNames.length - 1; stepCount = testStepData.names.length; } } }; Compiler.prototype._populateTestCases = function (testName, testStepData) { var compiler = this, cases = testStepData.testCasesDirectiveAst[1]; cases.forEach(function (testCase, index) { var fields = testCase[1], caseName = null, initStats = []; fields.forEach(function (field) { var fieldName = field[0]; if (fieldName === Common.TEST_CASE_NAME_FIELD) caseName = field[1][1]; else initStats.push(['stat', ['assign', true, ['sub', ['name', 'this'], ['string', fieldName]], field[1]]]); }); var initStepAst = ['function', null, [], initStats], genStepData = { names: [Common.TEST_CASE_INIT_STEP_NAME].concat(testStepData.names), asts: [initStepAst].concat(testStepData.asts) }, genTestName = testName + Common.TEST_CASE_NAME_SEPARATOR + (caseName || util_1.default.format(Common.TEST_CASE_DEFAULT_NAME_PATTERN, index)); compiler.out.testGroupMap[genTestName] = testName; compiler._addOutputTestStepData(genTestName, genStepData); }); }; Compiler.prototype._addOutputTestStepData = function (testName, testStepData) { var js = uglify_js_1.uglify.gen_code(['array', testStepData.asts], { beautify: true }); this.out.testsStepData[testName] = { names: testStepData.names, js: this.hammerheadProcessScript(js, false) }; }; Compiler.prototype._compileTestsStepData = function () { var compiler = this, testNames = Object.keys(this.rawTestsStepData); testNames.forEach(function (testName) { var testStepData = compiler.rawTestsStepData[testName]; compiler._insertMixins(testStepData); if (compiler.ok) { if (testStepData.testCasesDirectiveAst) compiler._populateTestCases(testName, testStepData); else compiler._addOutputTestStepData(testName, testStepData); } }); }; //Test cases preparation Compiler.prototype._parseExternalTestCases = function (data) { var ast = null; try { ast = uglify_js_2.parser.parse(data.toString().trim(), false, true); } catch (parserErr) { return null; } //NOTE: extract testCases array from first statement [toplevel]->[stat]->[array] return ast && ast[1] && ast[1][0] && ast[1][0][1]; }; Compiler.prototype._validateTestCase = function (caseAst, filename, namesMap) { if (caseAst[0].name !== 'object') { this._err(ErrCodes.TEST_CASE_IS_NOT_AN_OBJECT, filename, Ast.getCurrentSrcLineNum(caseAst)); return; } var fields = caseAst[1]; if (!fields.length) { this._err(ErrCodes.TEST_CASE_DOESNT_CONTAIN_ANY_FIELDS, filename, Ast.getCurrentSrcLineNum(caseAst)); return; } var nameField = fields.filter(function (field) { return field[0] === Common.TEST_CASE_NAME_FIELD; })[0]; if (nameField) { var nameFieldValue = nameField[1]; if (nameFieldValue[0].name !== 'string') { this._err(ErrCodes.TEST_CASE_NAME_IS_NOT_A_STRING, filename, Ast.getCurrentSrcLineNum(nameField)); return; } var testCaseName = nameFieldValue[1]; if (namesMap[testCaseName]) this._err(ErrCodes.DUPLICATE_TEST_CASE_NAME, filename, Ast.getCurrentSrcLineNum(nameField), { testCaseName: testCaseName }); else namesMap[testCaseName] = true; } }; Compiler.prototype._validateTestCaseListAst = function (ast, filename) { if (!ast || !ast[0] || ast[0].name !== 'array') { this._err(ErrCodes.TEST_CASES_LIST_IS_NOT_ARRAY, filename, ast ? Ast.getCurrentSrcLineNum(ast) : 1); return; } var cases = ast[1]; if (!cases.length) { this._err(ErrCodes.TEST_CASES_LIST_IS_EMPTY, filename, Ast.getCurrentSrcLineNum(ast)); return; } var compiler = this, namesMap = []; cases.forEach(function (caseAst) { compiler._validateTestCase(caseAst, filename, namesMap); }); }; Compiler.prototype._createExternalTestCasesAnalyzer = function (testCasesPath, refTestsStepData) { var compiler = this; return function (readerCallback) { readFile(testCasesPath) .then(data => { data = (0, strip_bom_1.default)(data); var ast = compiler._parseExternalTestCases(data); compiler._validateTestCaseListAst(ast, testCasesPath); refTestsStepData.forEach(function (testStepData) { testStepData.testCasesDirectiveAst = ast; }); readerCallback(); }) .catch(() => { compiler._fixtureErr(ErrCodes.FAILED_TO_READ_EXTERNAL_TEST_CASES, 0, { testCasesPath: testCasesPath }); readerCallback(); }); }; }; Compiler.prototype._prepareTestCases = function (callback) { var compiler = this, externalTestCases = {}, externalTestCasesAnalyzers = []; Object.keys(this.rawTestsStepData).forEach(function (testName) { var testStepData = compiler.rawTestsStepData[testName]; if (testStepData.testCasesDirectiveAst) { //NOTE: we have a test cases from external file. Add it to the external test cases list, then read them, //validate and attach to the appropriate testStepData's if (testStepData.testCasesDirectiveAst[0].name === 'string') { var testCasesPath = path_1.default.join(compiler.workingDir, testStepData.testCasesDirectiveAst[1]); //NOTE: assign absolute path as a directive value testStepData.testCasesDirectiveAst = testCasesPath; //NOTE: create array which will contain all testStepData's which uses this external test case. if (!externalTestCases[testCasesPath]) externalTestCases[testCasesPath] = []; externalTestCases[testCasesPath].push(testStepData); } else compiler._validateTestCaseListAst(testStepData.testCasesDirectiveAst, compiler.filename); } }); //NOTE: run external test cases analyzers Object.keys(externalTestCases).forEach(function (testCasesPath) { externalTestCasesAnalyzers.push(compiler._createExternalTestCasesAnalyzer(testCasesPath, externalTestCases[testCasesPath])); }); async_1.default.parallel(externalTestCasesAnalyzers, callback); }; //Compile Compiler.prototype.compile = function (callback) { var compiler = this; var constructed = null; try { constructed = Ast.constructFromCode(this.src, this.filename); } catch (parserErr) { callback([parserErr]); return; } var ast = constructed.ast; compiler.src = constructed.preprocessedCode; compiler._analyzeAst(ast); if (!compiler.out.fixture) compiler._fixtureErr(ErrCodes.FIXTURE_DIRECTIVE_IS_UNDEFINED); if (!compiler.out.page) compiler._fixtureErr(ErrCodes.PAGE_DIRECTIVE_IS_UNDEFINED); compiler.out.remainderJs = compiler._getRemainderCode(ast); compiler._analyzeRequires(function () { compiler._prepareTestCases(function () { compiler._compileTestsStepData(); if (compiler.ok) callback(null, compiler.out); else callback(compiler.errs); }); }); };