├── images ├── logo1.png └── logo2.png ├── test ├── index.js └── config.js ├── yuidoc.json ├── index.js ├── .npmignore ├── lib ├── defaults │ ├── defaultShared.js │ ├── defaultTaskDecorator.js │ ├── defaultClient.js │ ├── defaultClientDecorator.js │ ├── defaultCoverage.js │ ├── defaultConfig.js │ └── defaultTask.js ├── task │ ├── client │ │ ├── resources │ │ │ └── empty.js │ │ ├── node.js │ │ ├── kobold.js │ │ ├── cucumber.js │ │ └── mocha.js │ ├── node.js │ ├── kobold.js │ ├── loader.js │ ├── shell.js │ ├── cucumber.js │ ├── group.js │ └── mocha.js ├── taskDecorator │ ├── decorator.js │ ├── identifier.js │ └── group.js ├── abstractTaskDecorator.js ├── coverage.js ├── abstractClientDecorator.js ├── client.js ├── config.js ├── clientDecorator │ └── plain.js ├── abstractForkTask.js ├── abstractClient.js ├── abstractTask.js └── manager.js ├── .gitignore ├── .travis.yml ├── .editorconfig ├── examples └── simple │ ├── mocha │ ├── test3.js │ ├── test2.js │ ├── test1.js │ └── index.js │ ├── lib.js │ └── config.js ├── CHANGELOG.md ├── LICENSE ├── package.json ├── bin └── preceptor └── README.md /images/logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/preceptor/HEAD/images/logo1.png -------------------------------------------------------------------------------- /images/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/preceptor/HEAD/images/logo2.png -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | describe('Test', function () { 2 | it('should', function () { 3 | }); 4 | }); 5 | -------------------------------------------------------------------------------- /yuidoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": { 3 | "outdir": "docs", 4 | "linkNatives": true, 5 | "exclude": "test,bin" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | module.exports = require('./lib/manager'); 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | artifacts/ 3 | **/npm-debug.log 4 | .DS_Store 5 | *~ 6 | test/ 7 | tests/ 8 | docs/ 9 | examples/ 10 | Makefile 11 | .idea/ 12 | junit.xml 13 | tap.txt 14 | images/ 15 | -------------------------------------------------------------------------------- /lib/defaults/defaultShared.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var shared = {}; 5 | 6 | module.exports = shared; 7 | -------------------------------------------------------------------------------- /lib/defaults/defaultTaskDecorator.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var options = {}; 5 | 6 | module.exports = options; 7 | -------------------------------------------------------------------------------- /lib/defaults/defaultClient.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var options = { 5 | config: {} 6 | }; 7 | 8 | module.exports = options; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | artifacts/ 3 | node_modules/ 4 | **/npm-debug.log 5 | tmp/ 6 | CVS/ 7 | .DS_Store 8 | .*.swp 9 | .svn 10 | *~ 11 | .com.apple.timemachine.supported 12 | .idea/ 13 | xunit.xml 14 | docs/ 15 | junit.xml 16 | tap.txt 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8.4.0" 5 | 6 | branches: 7 | except: 8 | - gh-pages 9 | 10 | script: 11 | - npm test 12 | 13 | after_script: 14 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 15 | 16 | 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /lib/defaults/defaultClientDecorator.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var options = { 5 | "type": null, 6 | "configuration": {} 7 | }; 8 | 9 | module.exports = options; 10 | -------------------------------------------------------------------------------- /examples/simple/mocha/test3.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var lib = require('../lib'); 3 | 4 | it('should not panic', function () { 5 | assert.ok(lib.whatToDo("don't panic")); 6 | }); 7 | 8 | it('should panic', function () { 9 | assert.ok(!lib.whatToDo("do not panic")); 10 | }); 11 | -------------------------------------------------------------------------------- /lib/task/client/resources/empty.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | // This file is used as a placeholder for file loading since some testing frameworks need at least one file to load. 5 | module.exports = function () { 6 | // Nothing 7 | }; 8 | -------------------------------------------------------------------------------- /examples/simple/mocha/test2.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var lib = require('../lib'); 3 | 4 | it('should find all there is', function () { 5 | assert.equal(lib.allThereIs(), 42); 6 | }); 7 | 8 | it('should calculate the worst first million years for marvin', function () { 9 | assert.equal(lib.worstFirstMillionYears(), 30); 10 | }); 11 | -------------------------------------------------------------------------------- /examples/simple/mocha/test1.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var lib = require('../lib'); 3 | 4 | it('should know the answer to life, the universe, and everything', function () { 5 | assert.equal(lib.answerToLifeTheUniverseAndEverything(), 42); 6 | }); 7 | 8 | describe('The End', function () { 9 | 10 | it('should print something', function () { 11 | lib.message(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /lib/defaults/defaultCoverage.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var options = { 5 | "active": false, 6 | 7 | "root": "/", 8 | "path": "./coverage", 9 | 10 | "reports": ['file', 'lcov', 'text-summary'], 11 | 12 | "includes": null, 13 | "excludes": null 14 | }; 15 | 16 | module.exports = options; 17 | -------------------------------------------------------------------------------- /lib/defaults/defaultConfig.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var config = { 5 | "verbose": false, 6 | "debug": false, 7 | "ignoreErrors": false, 8 | 9 | "reportManager": { 10 | "reporter": [], 11 | "listener": [] 12 | }, 13 | 14 | "coverage": {}, 15 | 16 | "plugins": [] 17 | }; 18 | 19 | module.exports = config; 20 | -------------------------------------------------------------------------------- /lib/defaults/defaultTask.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var options = { 5 | "taskId": null, 6 | "type": null, 7 | "name": null, 8 | "title": null, 9 | 10 | "configuration": {}, 11 | "decorators": [], 12 | 13 | "active": true, 14 | "suite": false, 15 | "debug": false, 16 | "verbose": false, 17 | "report": true, 18 | "coverage": false, 19 | 20 | "failOnError": false, 21 | "echoStdOut": false, 22 | "echoStdErr": false 23 | }; 24 | 25 | module.exports = options; 26 | -------------------------------------------------------------------------------- /examples/simple/lib.js: -------------------------------------------------------------------------------- 1 | console.log('Mostly Harmless'); 2 | 3 | module.exports = { 4 | 5 | answerToLifeTheUniverseAndEverything: function () { 6 | return 42; 7 | }, 8 | 9 | whatToDo: function (phrase) { 10 | return (phrase == "don't panic"); 11 | }, 12 | 13 | allThereIs: function () { 14 | return 6 * 9; 15 | }, 16 | 17 | worstFirstMillionYears: function () { 18 | return 10 + 10 + 10; 19 | }, 20 | 21 | message: function () { 22 | console.log("So Long, and Thanks for All the Fish."); 23 | }, 24 | 25 | startInfiniteImprobabilityDriver: function () { 26 | throw new Error("infinitely improbable"); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /examples/simple/mocha/index.js: -------------------------------------------------------------------------------- 1 | var assert = 2 | 3 | describe('test suite', function () { 4 | it('should just work', function () { 5 | 6 | }); 7 | 8 | describe('sub tests', function () { 9 | it('should also be successful', function () { 10 | require('../lib'); 11 | }); 12 | }); 13 | 14 | it('should be incomplete'); 15 | 16 | it.skip('should be skipped', function () { 17 | 18 | }); 19 | 20 | var test = it('should be incomplete using the pending property', function () { 21 | 22 | }); 23 | test.pending = true; 24 | 25 | it('should fail', function () { 26 | throw new Error('An error happened here.'); 27 | }); 28 | }); 29 | 30 | describe('another test suite', function () { 31 | it('should also be successful in second suite', function () { 32 | 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /lib/taskDecorator/decorator.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var AbstractTaskDecorator = require('../abstractTaskDecorator'); 5 | 6 | /** 7 | * @class DecoratorTaskDecorator 8 | * @extends AbstractTaskDecorator 9 | * @constructor 10 | * 11 | * @property {Driver} _instance 12 | */ 13 | var DecoratorTaskDecorator = AbstractTaskDecorator.extend( 14 | 15 | { 16 | /** 17 | * Run the decorator 18 | * 19 | * @method run 20 | * @param {object} taskOptions 21 | * @param {int} taskIndex 22 | */ 23 | run: function (taskOptions, taskIndex) { 24 | 25 | if (taskOptions.decorator) { 26 | taskOptions.decorators = taskOptions.decorator; 27 | delete taskOptions.decorator; 28 | } 29 | } 30 | }); 31 | 32 | module.exports = DecoratorTaskDecorator; 33 | -------------------------------------------------------------------------------- /lib/taskDecorator/identifier.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var AbstractTaskDecorator = require('../abstractTaskDecorator'); 5 | 6 | /** 7 | * @class IdentifierTaskDecorator 8 | * @extends AbstractTaskDecorator 9 | * @constructor 10 | * 11 | * @property {Driver} _instance 12 | */ 13 | var IdentifierTaskDecorator = AbstractTaskDecorator.extend( 14 | 15 | { 16 | /** 17 | * Run the decorator 18 | * 19 | * @method run 20 | * @param {object} taskOptions 21 | * @param {int} taskIndex 22 | */ 23 | run: function (taskOptions, taskIndex) { 24 | 25 | taskOptions.taskId = "task_" + taskIndex; 26 | if (!taskOptions.name) { 27 | taskOptions.name = taskOptions.taskId; 28 | } 29 | if (!taskOptions.title) { 30 | taskOptions.title = taskOptions.name; 31 | } 32 | } 33 | }); 34 | 35 | module.exports = IdentifierTaskDecorator; 36 | -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "configuration": { 3 | "reportManager": { 4 | "reporter": [ 5 | { "type": "Spec", "progress": false }, 6 | { "type": "List", "progress": false } 7 | ] 8 | }, 9 | "plugins": ["preceptor-webdriver"] 10 | }, 11 | 12 | "tasks": [ 13 | { 14 | "type": "mocha", 15 | "name": "First Mocha Task", 16 | "suite": true, 17 | "decorators":[{ 18 | "type": "webDriver", 19 | "configuration": { 20 | "isolation": false, 21 | "client": { 22 | "type": "cabbie", 23 | "configuration": { 24 | "mode": "sync", 25 | "debug": true, 26 | "httpDebug": true 27 | }, 28 | "capabilities": { 29 | "browserName": "firefox", 30 | "version": "latest" 31 | } 32 | }, 33 | "server": { 34 | "type": "selenium" 35 | } 36 | } 37 | }], 38 | "configuration": { 39 | "coverage": true, 40 | "paths": [__dirname + "/index.js"], 41 | "timeOut": 60000 42 | } 43 | } 44 | ] 45 | }; 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | v0.9.9 - 09/22/15 5 | * Add ignoreErrors flag on global config to avoid tests error propagation 6 | 7 | v0.9.8 - 04/08/15 8 | * Update versions 9 | * Remove Node 0.8 support 10 | * Add support for Node 0.12 and IO.js 11 | 12 | v0.9.7 - 12/02/14 13 | * Updated dependencies 14 | 15 | v0.9.6 - 12/02/14 16 | * Updated dependencies 17 | 18 | v0.9.5 - 12/02/14 19 | * Added Coverage loader support 20 | 21 | v0.9.4 - 11/30/14 22 | * Update of reporter version adding custom loader configuration option (topLevel) 23 | 24 | v0.9.3 - 11/29/14 25 | * Added loader task 26 | * Changed 'rule-book.js' and 'rule-book.json' as default values for a configuration file 27 | * Renamed "decorator" to "decorators" in the task-options 28 | * Added error logging to cucumber and mocha hooks 29 | * Added Profile and Sub-profile 30 | 31 | v0.9.2 - 11/10/14 32 | * Added "report" property for task to turn on/off reporting for the task 33 | 34 | v0.9.1 - 11/05/14 35 | * Gave access to global configuration in client 36 | 37 | v0.9.0 - Initial release 11/04/14 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Yahoo! Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/task/node.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var AbstractForkTask = require('../abstractForkTask'); 5 | var _ = require('underscore'); 6 | var path = require('path'); 7 | 8 | /** 9 | * @class NodeTask 10 | * @extends AbstractForkTask 11 | * @constructor 12 | */ 13 | var NodeTask = AbstractForkTask.extend( 14 | 15 | { 16 | /** 17 | * Validates the given data 18 | * 19 | * @method validate 20 | */ 21 | validate: function () { 22 | this.__super(); 23 | 24 | if (!_.isString(this.getPath())) { 25 | throw new Error('The "path" parameter is not a string.'); 26 | } 27 | }, 28 | 29 | 30 | /** 31 | * Gets the path of the file to require 32 | * 33 | * @method getPath 34 | * @return {string} 35 | */ 36 | getPath: function () { 37 | return this.getConfiguration().path; 38 | }, 39 | 40 | 41 | /** 42 | * Run the client 43 | * 44 | * @method _run 45 | * @param {string} parentId 46 | * @return {Promise} 47 | * @private 48 | */ 49 | _run: function (parentId) { 50 | return this.runClient(parentId, path.join(__dirname, 'client', 'node')); 51 | } 52 | }); 53 | 54 | module.exports = NodeTask; 55 | -------------------------------------------------------------------------------- /lib/task/client/node.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var AbstractClient = require('../../abstractClient'); 5 | var Promise = require('promise'); 6 | var _ = require('underscore'); 7 | 8 | /** 9 | * @class NodeClient 10 | * @extends AbstractClient 11 | * @constructor 12 | */ 13 | var NodeClient = AbstractClient.extend( 14 | 15 | { 16 | /** 17 | * Gets the path of the file to require 18 | * 19 | * @method getPath 20 | * @return {string} 21 | */ 22 | getPath: function () { 23 | return this.getOptions().path; 24 | }, 25 | 26 | 27 | /** 28 | * Execute client 29 | * 30 | * @method run 31 | * @param {string} parentId 32 | * @return {Promise} 33 | */ 34 | run: function (parentId) { 35 | 36 | return new Promise(function (resolve, reject) { 37 | 38 | var message = this.getReportManager().message(); 39 | 40 | message.start(); 41 | 42 | try { 43 | require(this.getPath())(parentId, this.getReportManager()); 44 | 45 | message.stop(); 46 | message.complete(); 47 | 48 | resolve(true); 49 | 50 | } catch (err) { 51 | 52 | message.stop(); 53 | message.complete(); 54 | 55 | reject(err); 56 | } 57 | 58 | }.bind(this)); 59 | } 60 | }); 61 | 62 | module.exports = NodeClient; 63 | -------------------------------------------------------------------------------- /examples/simple/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "configuration": { 3 | "reportManager": { 4 | "reporter": [ 5 | { "type": "Spec", "progress": false }, 6 | { "type": "List", "progress": false }, 7 | { "type": "Duration" }, 8 | { "type": "Junit", path: "junit.xml" }, 9 | { "type": "Tap", path: "tap.txt" } 10 | ] 11 | }, 12 | "coverage": { 13 | "active": true, // Activate coverage 14 | 15 | "path": "./coverage" // This is the default path 16 | // Add here any other coverage configuration. 17 | // See below for more information 18 | } 19 | }, 20 | 21 | "shared": { 22 | "echoStdErr": true 23 | }, 24 | 25 | "tasks": [ 26 | [{ 27 | "type": "mocha", 28 | "title": "Everything", 29 | "suite": true, 30 | "configuration": { 31 | "paths": [__dirname + "/mocha/test1.js"] 32 | } 33 | }, 34 | { 35 | "type": "mocha", 36 | "title": "Calculations", 37 | "suite": true, 38 | "configuration": { 39 | "paths": [__dirname + "/mocha/test2.js"] 40 | } 41 | }], 42 | { 43 | "type": "mocha", 44 | "title": "Phrasing", 45 | "suite": true, 46 | "configuration": { 47 | "paths": [__dirname + "/mocha/test3.js"] 48 | } 49 | }, 50 | { 51 | "type": "loader", 52 | "title": "JUnit Test Import", 53 | "suite": true, 54 | "configuration": { 55 | "path": __dirname + "/*.xml" 56 | } 57 | } 58 | ] 59 | }; 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preceptor", 3 | "version": "0.9.11", 4 | "description": "Preceptor testrunner and aggregator", 5 | "license": "MIT", 6 | "main": "index.js", 7 | "bin": { 8 | "preceptor": "bin/preceptor" 9 | }, 10 | "scripts": { 11 | "test": "istanbul cover -- _mocha --reporter spec", 12 | "docs": "yuidoc ." 13 | }, 14 | "homepage": "https://github.com/yahoo/preceptor", 15 | "bugs": "https://github.com/yahoo/preceptor/issues", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/yahoo/preceptor.git" 19 | }, 20 | "keywords": [ 21 | "testing", 22 | "preceptor", 23 | "testrunner", 24 | "aggregator", 25 | "mocha", 26 | "cucumber", 27 | "kobold" 28 | ], 29 | "author": { 30 | "name": "Marcel Erz", 31 | "email": "erz@yahoo-inc.com" 32 | }, 33 | "dependencies": { 34 | "glob": "7.1.2", 35 | "istanbul": "0.4.5", 36 | "log4js": "2.5.2", 37 | "minimatch": "3.0.4", 38 | "mkdirp": "0.5.0", 39 | "preceptor-core": "0.9.3", 40 | "preceptor-reporter": "0.10.1", 41 | "promise": "6.0.0", 42 | "underscore": "1.7.0", 43 | "uuid": "2.0.1" 44 | }, 45 | "devDependencies": { 46 | "chai": "1.9.1", 47 | "cucumber": "0.4.2", 48 | "coveralls": "2.11.2", 49 | "mocha": "5.0.0", 50 | "cabbie-alpha": "~1.0.0", 51 | "kobold": "~0.9.0", 52 | "preceptor-webdriver": "~0.9.0", 53 | "yuidocjs": "0.3.50" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/task/client/kobold.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var MochaClient = require('./mocha'); 5 | var path = require('path'); 6 | 7 | /** 8 | * @class KoboldClient 9 | * @extends MochaClient 10 | * @constructor 11 | */ 12 | var KoboldClient = MochaClient.extend( 13 | 14 | { 15 | /** 16 | * Initializes the instance 17 | * 18 | * @method initialize 19 | */ 20 | initialize: function () { 21 | var options; 22 | 23 | this.__super(); 24 | 25 | options = this.getOptions(); 26 | this.getFunctions().push(function () { 27 | require('kobold')(options); 28 | }); 29 | this.getPaths().push(path.join(__dirname, 'resources', 'empty.js')); 30 | }, 31 | 32 | 33 | /** 34 | * Gets the mocha configuration 35 | * Overwrite this function if the mocha configuration is found somewhere else. 36 | * 37 | * @method getMochaConfiguration 38 | * @return {object} 39 | */ 40 | getMochaConfiguration: function () { 41 | return this.getOptions().mocha; 42 | }, 43 | 44 | /** 45 | * Sets the mocha configuration 46 | * Overwrite this function if the mocha configuration is found somewhere else. 47 | * 48 | * @method setMochaConfiguration 49 | * @param {object} options 50 | */ 51 | setMochaConfiguration: function (options) { 52 | this.getOptions().mocha = options; 53 | } 54 | }); 55 | 56 | module.exports = KoboldClient; 57 | -------------------------------------------------------------------------------- /lib/abstractTaskDecorator.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('preceptor-core').Base; 5 | var utils = require('preceptor-core').utils; 6 | var logger = require('log4js').getLogger(__filename); 7 | 8 | var defaultTaskDecorator = require('./defaults/defaultTaskDecorator'); 9 | 10 | /** 11 | * @class AbstractTaskDecorator 12 | * @extends Base 13 | * 14 | * @property {object} _options 15 | */ 16 | var AbstractTaskDecorator = Base.extend( 17 | 18 | /** 19 | * Abstract task-decorator constructor 20 | * 21 | * @param {object} options 22 | * @constructor 23 | */ 24 | function (options) { 25 | this.__super(); 26 | 27 | this._options = utils.deepExtend({}, [defaultTaskDecorator, options || {}]); 28 | logger.debug('Construct options', this._options); 29 | 30 | this.initialize(); 31 | }, 32 | 33 | { 34 | /** 35 | * Initializes the instance 36 | */ 37 | initialize: function () { 38 | // Nothing by default 39 | }, 40 | 41 | 42 | /** 43 | * Gets the options 44 | * 45 | * @method getOptions 46 | * @return {object} 47 | */ 48 | getOptions: function () { 49 | return this._options; 50 | }, 51 | 52 | 53 | /** 54 | * Run the decorator 55 | * 56 | * @method run 57 | * @param {object} taskOptions 58 | * @param {int} taskIndex 59 | * @return {void|object|object[]} 60 | */ 61 | run: function (taskOptions, taskIndex) { 62 | throw new Error('Unimplemented decorator function "run".'); 63 | } 64 | }, 65 | 66 | { 67 | /** 68 | * @property TYPE 69 | * @type {string} 70 | * @static 71 | */ 72 | TYPE: 'AbstractTaskDecorator' 73 | }); 74 | 75 | module.exports = AbstractTaskDecorator; 76 | -------------------------------------------------------------------------------- /lib/taskDecorator/group.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var AbstractTaskDecorator = require('../abstractTaskDecorator'); 5 | var _ = require('underscore'); 6 | 7 | /** 8 | * @class GroupTaskDecorator 9 | * @extends AbstractTaskDecorator 10 | * @constructor 11 | * 12 | * @property {Driver} _instance 13 | */ 14 | var GroupTaskDecorator = AbstractTaskDecorator.extend( 15 | 16 | { 17 | /** 18 | * Run the decorator 19 | * 20 | * @method run 21 | * @param {object} taskOptions 22 | * @param {int} taskIndex 23 | */ 24 | run: function (taskOptions, taskIndex) { 25 | 26 | taskOptions = this._processWhenArray(taskOptions); 27 | taskOptions = this._processWhenObject(taskOptions); 28 | 29 | return taskOptions; 30 | }, 31 | 32 | 33 | /** 34 | * Processes an object without type when one is given 35 | * 36 | * @method _processWhenObject 37 | * @param {object} taskOptions 38 | * @return {object} 39 | * @private 40 | */ 41 | _processWhenObject: function (taskOptions) { 42 | 43 | var newList; 44 | 45 | if (_.isObject(taskOptions) && !taskOptions.type) { 46 | 47 | newList = []; 48 | 49 | _.each(_.keys(taskOptions), function (key) { 50 | newList.push({ 51 | "type": "group", 52 | "name": key, 53 | "suite": true, 54 | "configuration": { 55 | "tasks": taskOptions[key] 56 | } 57 | }) 58 | }); 59 | 60 | taskOptions = newList; 61 | } 62 | 63 | return taskOptions; 64 | }, 65 | 66 | /** 67 | * Processes an array when one is given 68 | * 69 | * @method _processWhenArray 70 | * @param {object|array} taskOptions 71 | * @return {object} 72 | * @private 73 | */ 74 | _processWhenArray: function (taskOptions) { 75 | 76 | if (_.isArray(taskOptions)) { 77 | taskOptions = { 78 | "type": "group", 79 | "configuration": { 80 | "parallel": true, 81 | "tasks": taskOptions 82 | } 83 | } 84 | } 85 | 86 | return taskOptions; 87 | } 88 | }); 89 | 90 | module.exports = GroupTaskDecorator; 91 | -------------------------------------------------------------------------------- /lib/task/kobold.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var MochaTask = require('./mocha'); 5 | var path = require('path'); 6 | var utils = require('preceptor-core').utils; 7 | var _ = require('underscore'); 8 | 9 | /** 10 | * @class KoboldTask 11 | * @extends MochaTask 12 | * @constructor 13 | */ 14 | var KoboldTask = MochaTask.extend( 15 | 16 | { 17 | /** 18 | * Initializes the instance 19 | * 20 | * @method initialize 21 | */ 22 | initialize: function () { 23 | this.getOptions().configuration = utils.deepExtend({}, [ 24 | { 25 | "verbose": false, 26 | 27 | "failForOrphans": true, 28 | "failOnAdditions": true, 29 | 30 | "build": process.env.BUILD_NUMBER || (process.env.USER + '_' + (+(new Date()))), 31 | 32 | "blinkDiff": {}, 33 | 34 | "mocha": { 35 | "slow": 30000, 36 | "timeOut": 60000 37 | }, 38 | 39 | "storage": { 40 | "type": 'File', 41 | 42 | "options": { 43 | "approvedFolderName": 'approved', 44 | "buildFolderName": 'build', 45 | "highlightFolderName": 'highlight' 46 | } 47 | } 48 | }, 49 | this.getConfiguration() 50 | ]); 51 | 52 | this.__super(); 53 | }, 54 | 55 | 56 | /** 57 | * Validates the given data 58 | * 59 | * @method validate 60 | */ 61 | validate: function () { 62 | this.__super(); 63 | 64 | if (!_.isObject(this.getMochaConfiguration())) { 65 | throw new Error('The "mocha" parameter is not an object.'); 66 | } 67 | }, 68 | 69 | 70 | /** 71 | * Gets the mocha configuration 72 | * Overwrite this function if the mocha configuration is found somewhere else. 73 | * 74 | * @method getMochaConfiguration 75 | * @return {object} 76 | */ 77 | getMochaConfiguration: function () { 78 | return this.getConfiguration().mocha; 79 | }, 80 | 81 | /** 82 | * Sets the mocha configuration 83 | * Overwrite this function if the mocha configuration is found somewhere else. 84 | * 85 | * @method setMochaConfiguration 86 | * @param {object} options 87 | */ 88 | setMochaConfiguration: function (options) { 89 | this.getConfiguration().mocha = options; 90 | }, 91 | 92 | 93 | /** 94 | * Run the client 95 | * 96 | * @method _run 97 | * @param {string} parentId 98 | * @return {Promise} 99 | * @private 100 | */ 101 | _run: function (parentId) { 102 | return this.runClient(parentId, path.join(__dirname, 'client', 'kobold')); 103 | } 104 | }); 105 | 106 | module.exports = KoboldTask; 107 | -------------------------------------------------------------------------------- /lib/task/loader.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var AbstractTask = require('../abstractTask'); 5 | var Promise = require('promise'); 6 | var utils = require('preceptor-core').utils; 7 | var _ = require('underscore'); 8 | var ReportManager = require('preceptor-reporter'); 9 | 10 | /** 11 | * @class JUnitTask 12 | * @extends AbstractTask 13 | * @constructor 14 | */ 15 | var JUnitTask = AbstractTask.extend( 16 | 17 | { 18 | /** 19 | * Initializes the instance 20 | * 21 | * @method initialize 22 | */ 23 | initialize: function () { 24 | this.getOptions().configuration = utils.deepExtend({}, [ 25 | { 26 | "format": 'junit', 27 | "path": null 28 | }, 29 | this.getConfiguration() 30 | ]); 31 | 32 | this.__super(); 33 | }, 34 | 35 | 36 | /** 37 | * Validates the given data 38 | * 39 | * @method validate 40 | */ 41 | validate: function () { 42 | this.__super(); 43 | 44 | if (!_.isString(this.getFormat())) { 45 | throw new Error('The "format" parameter is not a string.'); 46 | } 47 | if (!_.isString(this.getPath())) { 48 | throw new Error('The "path" parameter is not a string.'); 49 | } 50 | }, 51 | 52 | 53 | /** 54 | * Gets the file-format 55 | * 56 | * @method getFormat 57 | * @return {string} 58 | */ 59 | getFormat: function () { 60 | return this.getConfiguration().format; 61 | }, 62 | 63 | /** 64 | * Gets the path to the files 65 | * 66 | * @method getPath 67 | * @return {string} 68 | */ 69 | getPath: function () { 70 | return this.getConfiguration().path; 71 | }, 72 | 73 | 74 | /** 75 | * Run the client 76 | * 77 | * @method _run 78 | * @param {string} parentId 79 | * @return {Promise} 80 | * @private 81 | */ 82 | _run: function (parentId) { 83 | 84 | var loaders = ReportManager.getLoaders(), 85 | format = this.getFormat().toLowerCase(), 86 | Class = loaders[format], 87 | loader, 88 | path = this.getPath(), 89 | configuration = this.getConfiguration(); 90 | 91 | if (!Class) { 92 | throw new Error('Unknown loader format "' + format + '"'); 93 | } 94 | 95 | loader = new Class({ 96 | "type": format, 97 | "path": path, 98 | "configuration": configuration.configuration || {} 99 | }); 100 | 101 | loader.on('message', function (areaType, messageType, params) { 102 | if (this.shouldReport()) { 103 | this.getReportManager().processMessage(messageType, params); 104 | } 105 | }.bind(this)); 106 | 107 | loader.on('coverage', function (cov) { 108 | this.getCoverageCollector().add(cov) 109 | }.bind(this)); 110 | 111 | return loader.process(parentId); 112 | } 113 | }); 114 | 115 | module.exports = JUnitTask; 116 | -------------------------------------------------------------------------------- /lib/coverage.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('preceptor-core').Base; 5 | var utils = require('preceptor-core').utils; 6 | var _ = require('underscore'); 7 | 8 | var defaultOptions = require('./defaults/defaultCoverage'); 9 | 10 | /** 11 | * @class Coverage 12 | * @extends Base 13 | * 14 | * @property {object} _options 15 | */ 16 | var Coverage = Base.extend( 17 | 18 | /** 19 | * Coverage constructor 20 | * 21 | * @param {object} options 22 | * @constructor 23 | */ 24 | function (options) { 25 | this.__super(); 26 | 27 | this._options = utils.deepExtend({}, [ defaultOptions, options || {} ]); 28 | 29 | this.initialize(); 30 | }, 31 | 32 | { 33 | /** 34 | * Initializes the instance 35 | * 36 | * @method initialize 37 | */ 38 | initialize: function () { 39 | // Nothing yet 40 | }, 41 | 42 | 43 | /** 44 | * Gets the client-driver instance 45 | * 46 | * @method getInstance 47 | * @return {*} 48 | */ 49 | getInstance: function () { 50 | return this._instance; 51 | }, 52 | 53 | /** 54 | * Gets the options 55 | * 56 | * @method getOptions 57 | * @return {object} 58 | */ 59 | getOptions: function () { 60 | return this._options; 61 | }, 62 | 63 | 64 | /** 65 | * Is coverage active? 66 | * 67 | * @method isActive 68 | * @return {string} 69 | */ 70 | isActive: function () { 71 | return !!this.getOptions().active; 72 | }, 73 | 74 | /** 75 | * Get path to where the coverage data should be written to 76 | * 77 | * @method getPath 78 | * @return {string} 79 | */ 80 | getPath: function () { 81 | return this.getOptions().path; 82 | }, 83 | 84 | /** 85 | * Gets the root-directory 86 | * 87 | * @method getRoot 88 | * @return {string} 89 | */ 90 | getRoot: function () { 91 | return this.getOptions().root; 92 | }, 93 | 94 | /** 95 | * Gets the type of reports to create 96 | * 97 | * @method getReports 98 | * @return {string} 99 | */ 100 | getReports: function () { 101 | return this.getOptions().reports; 102 | }, 103 | 104 | /** 105 | * Get includes for coverage 106 | * 107 | * @method getIncludes 108 | * @return {string[]} 109 | */ 110 | getIncludes: function () { 111 | return this.getOptions().includes; 112 | }, 113 | 114 | /** 115 | * Get excludes for coverage 116 | * 117 | * @method getExcludes 118 | * @return {string[]} 119 | */ 120 | getExcludes: function () { 121 | return this.getOptions().excludes; 122 | }, 123 | 124 | 125 | /** 126 | * Exports data to an object 127 | * 128 | * @method toObject 129 | * @return {object} 130 | */ 131 | toObject: function () { 132 | return utils.deepExtend({}, [this.getOptions()]); 133 | } 134 | }, 135 | 136 | { 137 | /** 138 | * @property TYPE 139 | * @type {string} 140 | * @static 141 | */ 142 | TYPE: 'Coverage' 143 | }); 144 | 145 | module.exports = Coverage; 146 | -------------------------------------------------------------------------------- /lib/abstractClientDecorator.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('preceptor-core').Base; 5 | var utils = require('preceptor-core').utils; 6 | var logger = require('log4js').getLogger(__filename); 7 | var Promise = require('promise'); 8 | 9 | var defaultClientDecorator = require('./defaults/defaultClientDecorator'); 10 | 11 | /** 12 | * @class AbstractClientDecorator 13 | * @extends Base 14 | * 15 | * @property {object} _options 16 | * @property {EventReporter} _eventReporter 17 | */ 18 | var AbstractClientDecorator = Base.extend( 19 | 20 | /** 21 | * Abstract client-decorator constructor 22 | * 23 | * @param {EventReporter} eventReporter 24 | * @param {object} options 25 | * @constructor 26 | */ 27 | function (eventReporter, options) { 28 | this.__super(); 29 | 30 | this._eventReporter = eventReporter; 31 | this._options = utils.deepExtend({}, [defaultClientDecorator, options || {}]); 32 | logger.debug('Construct options', this._options); 33 | 34 | this.initialize(); 35 | }, 36 | 37 | { 38 | /** 39 | * Initializes the instance 40 | * 41 | * @method initialize 42 | */ 43 | initialize: function () { 44 | this._eventReporter.on('message', function (areaType, messageType, params) { 45 | if (this[messageType]) { 46 | this[messageType].apply(this, params); 47 | } 48 | }.bind(this)); 49 | }, 50 | 51 | 52 | /** 53 | * Gets the options 54 | * 55 | * @method getOptions 56 | * @return {object} 57 | */ 58 | getOptions: function () { 59 | return this._options; 60 | }, 61 | 62 | /** 63 | * Gets the client-decorator configuration 64 | * 65 | * @method getConfiguration 66 | * @return {object} 67 | */ 68 | getConfiguration: function () { 69 | return this.getOptions().configuration; 70 | }, 71 | 72 | 73 | /** 74 | * Gets the event-reporter 75 | * 76 | * @method getEventReporter 77 | * @return {object} 78 | */ 79 | getEventReporter: function () { 80 | return this._eventReporter; 81 | }, 82 | 83 | 84 | /** 85 | * Processes the begin of the testing environment 86 | * 87 | * @method processBefore 88 | * @return {Promise} 89 | */ 90 | processBefore: function () { 91 | return Promise.resolve(); 92 | }, 93 | 94 | /** 95 | * Processes the end of the testing environment 96 | * 97 | * @method processAfter 98 | * @return {Promise} 99 | */ 100 | processAfter: function () { 101 | return Promise.resolve(); 102 | }, 103 | 104 | /** 105 | * Processes the beginning of a test 106 | * 107 | * @method processBeforeTest 108 | * @return {Promise} 109 | */ 110 | processBeforeTest: function () { 111 | return Promise.resolve(); 112 | }, 113 | 114 | /** 115 | * Processes the ending of a test 116 | * 117 | * @method processAfterTest 118 | * @return {Promise} 119 | */ 120 | processAfterTest: function () { 121 | return Promise.resolve(); 122 | } 123 | }, 124 | 125 | { 126 | /** 127 | * @property TYPE 128 | * @type {string} 129 | * @static 130 | */ 131 | TYPE: 'AbstractClientDecorator' 132 | }); 133 | 134 | module.exports = AbstractClientDecorator; 135 | -------------------------------------------------------------------------------- /lib/task/shell.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var AbstractTask = require('../abstractTask'); 5 | var childProcess = require('child_process'); 6 | var Promise = require('promise'); 7 | var utils = require('preceptor-core').utils; 8 | var _ = require('underscore'); 9 | 10 | /** 11 | * @class ShellTask 12 | * @extends AbstractTask 13 | * @constructor 14 | */ 15 | var ShellTask = AbstractTask.extend( 16 | 17 | { 18 | /** 19 | * Initializes the instance 20 | * 21 | * @method initialize 22 | */ 23 | initialize: function () { 24 | this.getOptions().configuration = utils.deepExtend({}, [ 25 | { 26 | cwd: null, 27 | env: {}, 28 | cmd: null 29 | }, 30 | this.getConfiguration() 31 | ]); 32 | 33 | this.__super(); 34 | }, 35 | 36 | 37 | /** 38 | * Validates the given data 39 | * 40 | * @method validate 41 | */ 42 | validate: function () { 43 | this.__super(); 44 | 45 | if (this.getCwd() && !_.isString(this.getCwd())) { 46 | throw new Error('The "suite" parameter is not a string.'); 47 | } 48 | if (!_.isObject(this.getEnv())) { 49 | throw new Error('The "env" parameter is not an object.'); 50 | } 51 | if (!_.isString(this.getCommand())) { 52 | throw new Error('The "cmd" parameter is not a string.'); 53 | } 54 | }, 55 | 56 | 57 | /** 58 | * Get command to execute 59 | * 60 | * @method getCommand 61 | * @return {string} 62 | */ 63 | getCommand: function () { 64 | return this.getConfiguration().cmd; 65 | }, 66 | 67 | /** 68 | * Get the environment variables set for the shell 69 | * 70 | * @method getEnv 71 | * @return {object} 72 | */ 73 | getEnv: function () { 74 | return this.getConfiguration().env; 75 | }, 76 | 77 | /** 78 | * Get the current working directory 79 | * 80 | * @method getCwd 81 | * @return {string|null} 82 | */ 83 | getCwd: function () { 84 | return this.getConfiguration().cwd; 85 | }, 86 | 87 | 88 | /** 89 | * Run the client 90 | * 91 | * @method _run 92 | * @param {string} parentId 93 | * @return {Promise} 94 | * @private 95 | */ 96 | _run: function (parentId) { 97 | 98 | return new Promise(function (resolve, reject) { 99 | 100 | var env = this.getEnv() || {}; 101 | env.PARENT_ID = parentId; 102 | 103 | childProcess.exec(this.getCommand(), { 104 | cwd: this.getCwd(), 105 | env: env 106 | 107 | }, function (error, stdout, stderr) { 108 | 109 | stdout = this.getReportManager().parse(stdout, { "#TASK#": parentId }); 110 | stderr = this.getReportManager().parse(stderr, { "#TASK#": parentId }); 111 | 112 | if (this.shouldEchoStdOut() && (stdout.length > 0)) { 113 | process.stdout.write((this.isVerbose() ? this.getLabel() + ": " : '') + stdout); 114 | } 115 | if (this.shouldEchoStdErr() && (stderr.length > 0)) { 116 | process.stderr.write((this.isVerbose() ? this.getLabel() + ": " : '') + stderr); 117 | } 118 | 119 | if (error) { 120 | reject(error); 121 | } else { 122 | resolve(); 123 | } 124 | 125 | }.bind(this)); 126 | }.bind(this)); 127 | } 128 | }); 129 | 130 | module.exports = ShellTask; 131 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var _ = require('underscore'); 5 | var istanbul = require('istanbul'); 6 | var path = require('path'); 7 | var minimatch = require('minimatch'); 8 | 9 | /** 10 | * Runs the client 11 | * 12 | * @class Client 13 | * @method run 14 | * @param {object} options 15 | * @param {function} send 16 | * @private 17 | */ 18 | var run = function (options, send) { 19 | 20 | var clientPath = options.clientPath, 21 | coverage = options.coverage, 22 | parentId = options.parentId, 23 | configuration = options.configuration, 24 | globalConfig = options.globalConfig, 25 | decorators = options.decorators, 26 | decoratorPlugins = options.decoratorPlugins, 27 | 28 | ClientClass, 29 | clientInstance, 30 | 31 | coverageVar = '__preceptorCoverage__', 32 | instrumenter, 33 | transformer, 34 | globalCoverageConfig = globalConfig.coverage, 35 | coverageIncludes = globalCoverageConfig.includes || ['**/*.js'], 36 | coverageExcludes = globalCoverageConfig.excludes || ['**/node_modules/**', '**/test/**', '**/tests/**']; 37 | 38 | // Make global configuration available 39 | global.PRECEPTOR = { 40 | config: globalConfig 41 | }; 42 | 43 | // Create client and run it 44 | ClientClass = require(clientPath); 45 | clientInstance = new ClientClass(decorators, decoratorPlugins, configuration); 46 | clientInstance.on('reportMessage', function (messageType, data) { 47 | send({ 48 | type: "reportMessage", 49 | messageType: messageType, 50 | data: data 51 | }); 52 | }); 53 | 54 | // Is coverage requested? 55 | if (((coverage === undefined) || (coverage === true)) && globalCoverageConfig.active) { 56 | 57 | // Prepare coverage instrumentation 58 | instrumenter = new istanbul.Instrumenter({ coverageVariable: coverageVar, preserveComments: true }); 59 | transformer = instrumenter.instrumentSync.bind(instrumenter); 60 | 61 | // Hook-up transformer for every new file loaded 62 | istanbul.hook.hookRequire(function (filePath) { 63 | var allowed = false; 64 | 65 | // Inclusion 66 | _.each(coverageIncludes, function (include) { 67 | allowed = allowed || minimatch(filePath, include); 68 | }); 69 | 70 | if (allowed) { 71 | // Exclusion 72 | _.each(coverageExcludes, function (exclude) { 73 | allowed = allowed && !minimatch(filePath, exclude); 74 | }); 75 | } 76 | 77 | return allowed; 78 | 79 | }, transformer, {}); 80 | 81 | // Prepare variable 82 | global[coverageVar] = {}; 83 | } 84 | 85 | // Run client 86 | clientInstance.run(parentId).then(function () { 87 | send({ 88 | type: "completion", 89 | data: { 90 | success: true, 91 | coverage: global[coverageVar] 92 | } 93 | }); 94 | global[coverageVar] = {}; // Reset 95 | 96 | }, function (err) { 97 | send({ 98 | type: "completion", 99 | data: { 100 | success: false, 101 | coverage: global[coverageVar], 102 | context: err.stack 103 | } 104 | }); 105 | global[coverageVar] = {}; // Reset 106 | }); 107 | }; 108 | 109 | process.on('message', function (options) { 110 | if (options.type === "run") { 111 | run(options, function () { 112 | process.send.apply(process, arguments); 113 | }); 114 | } else { 115 | throw new Error('Unknown message received: ' + options.type); 116 | } 117 | }); 118 | 119 | process.on('uncaughtException', function (err) { 120 | process.send({ 121 | type: "exception", 122 | message: err.stack 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /bin/preceptor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Copyright 2014, Yahoo! Inc. 3 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 4 | 5 | var PreceptorManager = require('../'); 6 | var path = require('path'); 7 | var _ = require('underscore'); 8 | var utils = require('preceptor-core').utils; 9 | var fs = require('fs'); 10 | 11 | Error.stackTraceLimit = Infinity; 12 | 13 | process.on('uncaughtException', function (err) { 14 | console.log(err.stack); 15 | }); 16 | 17 | var manager = new PreceptorManager(parseArgs(process.argv)); 18 | manager.run().then(function () { 19 | process.exit(0); 20 | 21 | }, function (err) { 22 | console.log("Error: ", err.stack, "\n"); 23 | 24 | if (manager.getConfig().shouldIgnoreErrors()) { 25 | process.exit(0); 26 | } else { 27 | process.exit(1); 28 | } 29 | }); 30 | 31 | 32 | /** 33 | * Prints the help info 34 | * 35 | * @method printHelp 36 | */ 37 | function printHelp () { 38 | console.log("Usage: preceptor [] "); 39 | console.log(""); 40 | console.log(" Runs tests defined in the config."); 41 | console.log(""); 42 | console.log(" Options:"); 43 | console.log(" --config Inline configuration in JSON"); 44 | console.log(" --profile

Profile of configuration"); 45 | console.log(" --subprofile

Sub-profile of configuration"); 46 | console.log(" --version Print version"); 47 | console.log(" --help This help"); 48 | console.log(""); 49 | } 50 | 51 | /** 52 | * Parses the arguments and returns the configuration 53 | * 54 | * @method parseArgs 55 | * @param {string[]} argv 56 | * @return {object} 57 | */ 58 | function parseArgs (argv) { 59 | 60 | var i, argLength = argv.length, 61 | config, 62 | configLine, configFile, 63 | profile, subProfile; 64 | 65 | if (argLength <= 1) { 66 | printHelp(); 67 | process.exit(1); 68 | } 69 | 70 | for (i = 2; i < argLength; i++) { 71 | 72 | try { 73 | 74 | if (argv[i] == "--help") { 75 | printHelp(); 76 | process.exit(0); 77 | 78 | } else if (argv[i] == "--version") { 79 | console.log("preceptor " + manager.version); 80 | 81 | } else if (argv[i] == "--config") { 82 | i++; 83 | configLine = JSON.parse(argv[i]); 84 | 85 | } else if (argv[i] == "--profile") { 86 | i++; 87 | profile = argv[i]; 88 | 89 | } else if (argv[i] == "-p") { 90 | i++; 91 | profile = argv[i]; 92 | 93 | } else if (argv[i] == "--subprofile") { 94 | i++; 95 | subProfile = argv[i]; 96 | 97 | } else if (argv[i] == "-s") { 98 | i++; 99 | subProfile = argv[i]; 100 | 101 | } else if (!configFile) { 102 | 103 | // Get path 104 | if (argv[i].substr(0, 1) == '/') { 105 | configFile = path.resolve(argv[i]); 106 | } else { 107 | configFile = path.resolve(path.join(process.cwd(), argv[i])); 108 | } 109 | 110 | } else { 111 | console.log('Warning: parameter "' + argv[i] + '" ignored. Unknown.'); 112 | } 113 | 114 | } catch (exception) { 115 | var reason = (exception.message !== '') ? "; " + exception.message : ''; 116 | throw new Error("Invalid argument '" + argv[i] + "' for " + argv[i - 1] + reason); 117 | } 118 | } 119 | 120 | if (!configFile) { 121 | configFile = path.resolve(path.join(process.cwd(), 'rule-book')); 122 | } 123 | 124 | configFile = require(configFile); 125 | if (_.isFunction(configFile)) { 126 | configFile = configFile(); 127 | } 128 | 129 | config = utils.deepExtend({}, [configFile || {}]); 130 | 131 | // Profile 132 | if (profile) { 133 | if (config[profile]) { 134 | config = config[profile]; 135 | } else { 136 | throw new Error('Could not find profile "' + profile + '"'); 137 | } 138 | } 139 | 140 | // Sub-Profile 141 | if (subProfile) { 142 | if (config.tasks[subProfile]) { 143 | config.tasks = config.tasks[subProfile]; 144 | } else { 145 | throw new Error('Could not find sub-profile "' + subProfile + '"'); 146 | } 147 | } 148 | 149 | config = utils.deepExtend({}, [config, configLine || {}]); 150 | 151 | return config; 152 | } 153 | 154 | -------------------------------------------------------------------------------- /lib/task/cucumber.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var AbstractForkTask = require('../abstractForkTask'); 5 | var path = require('path'); 6 | var utils = require('preceptor-core').utils; 7 | var _ = require('underscore'); 8 | 9 | /** 10 | * @class CucumberTask 11 | * @extends AbstractForkTask 12 | * @constructor 13 | */ 14 | var CucumberTask = AbstractForkTask.extend( 15 | 16 | { 17 | /** 18 | * Initializes the instance 19 | * 20 | * @method initialize 21 | */ 22 | initialize: function () { 23 | this.setCucumberConfiguration(utils.deepExtend({}, [ 24 | { 25 | path: null, 26 | tags: [], 27 | require: [], 28 | functions: [], 29 | format: 'progress', 30 | coffeeScript: false 31 | }, 32 | this.getCucumberConfiguration() 33 | ])); 34 | 35 | this.__super(); 36 | }, 37 | 38 | 39 | /** 40 | * Validates the given data 41 | * 42 | * @method validate 43 | */ 44 | validate: function () { 45 | this.__super(); 46 | 47 | if (!_.isString(this.getPath())) { 48 | throw new Error('The "path" parameter is not a string.'); 49 | } 50 | if (!_.isArray(this.getTags())) { 51 | throw new Error('The "tags" parameter is not an array of strings.'); 52 | } 53 | if (!_.isArray(this.getRequires())) { 54 | throw new Error('The "require" parameter is not an array of strings.'); 55 | } 56 | if (!_.isArray(this.getFunctions())) { 57 | throw new Error('The "functions" parameter is not an array of functions.'); 58 | } 59 | if (!_.isBoolean(this.shouldOutputCoffeeScript())) { 60 | throw new Error('The "coffeeScript" parameter is not a boolean.'); 61 | } 62 | if (!_.isString(this.getFormat())) { 63 | throw new Error('The "format" parameter is not a string.'); 64 | } 65 | }, 66 | 67 | 68 | /** 69 | * Gets the cucumber configuration 70 | * Overwrite this function if the cucumber configuration is found somewhere else. 71 | * 72 | * @method getCucumberConfiguration 73 | * @return {object} 74 | */ 75 | getCucumberConfiguration: function () { 76 | return this.getConfiguration(); 77 | }, 78 | 79 | /** 80 | * Sets the cucumber configuration 81 | * Overwrite this function if the cucumber configuration is found somewhere else. 82 | * 83 | * @method setCucumberConfiguration 84 | * @param {object} options 85 | */ 86 | setCucumberConfiguration: function (options) { 87 | this.getOptions().configuration = options; 88 | }, 89 | 90 | 91 | /** 92 | * Gets the path to the text files 93 | * Cucumber Option: 94 | * 95 | * @method getPath 96 | * @return {string} 97 | */ 98 | getPath: function () { 99 | return this.getCucumberConfiguration().path; 100 | }, 101 | 102 | /** 103 | * Gets the tags to include/exclude 104 | * Cucumber Option: tags 105 | * 106 | * @method getTags 107 | * @return {string[]} 108 | */ 109 | getTags: function () { 110 | return this.getCucumberConfiguration().tags; 111 | }, 112 | 113 | /** 114 | * Gets the required file before running the tests 115 | * Cucumber Option: require 116 | * 117 | * @method getRequires 118 | * @return {string[]} 119 | */ 120 | getRequires: function () { 121 | return this.getCucumberConfiguration().require; 122 | }, 123 | 124 | /** 125 | * Gets the functions to execute as part of files to execute 126 | * Cucumber Option: 127 | * 128 | * @method getFunctions 129 | * @return {function[]} 130 | */ 131 | getFunctions: function () { 132 | return this.getCucumberConfiguration().functions; 133 | }, 134 | 135 | /** 136 | * Should output in coffee-script? 137 | * Cucumber Option: coffee 138 | * 139 | * @method shouldOutputCoffeeScript 140 | * @return {boolean} 141 | */ 142 | shouldOutputCoffeeScript: function () { 143 | return this.getCucumberConfiguration().coffeeScript; 144 | }, 145 | 146 | /** 147 | * Gets the output format for cucumber 148 | * Cucumber Option: format 149 | * 150 | * @method getFunctions 151 | * @return {string} 152 | */ 153 | getFormat: function () { 154 | return this.getCucumberConfiguration().format; 155 | }, 156 | 157 | 158 | /** 159 | * Run the client 160 | * 161 | * @method _run 162 | * @param {string} parentId 163 | * @return {Promise} 164 | * @private 165 | */ 166 | _run: function (parentId) { 167 | return this.runClient(parentId, path.join(__dirname, 'client', 'cucumber')); 168 | } 169 | }); 170 | 171 | module.exports = CucumberTask; 172 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('preceptor-core').Base; 5 | var utils = require('preceptor-core').utils; 6 | var logger = require('log4js').getLogger(__filename); 7 | var _ = require('underscore'); 8 | var Coverage = require('./coverage.js'); 9 | 10 | var defaultConfig = require('./defaults/defaultConfig'); 11 | 12 | /** 13 | * @class Config 14 | * @extends Base 15 | * 16 | * @property {object} _options 17 | */ 18 | var Config = Base.extend( 19 | 20 | /** 21 | * Config constructor 22 | * 23 | * @param {object} options 24 | * @constructor 25 | */ 26 | function (options) { 27 | this.__super(); 28 | 29 | this._options = utils.deepExtend({}, [defaultConfig, options || {}]); 30 | logger.debug('Construct', this._options); 31 | 32 | this.initialize(); 33 | }, 34 | 35 | { 36 | /** 37 | * Initializes the instance 38 | * 39 | * @method initialize 40 | */ 41 | initialize: function () { 42 | 43 | // Make sure the configuration has the correct structure 44 | this.validate(); 45 | 46 | // Augment options with outside data 47 | this.augment(); 48 | }, 49 | 50 | 51 | /** 52 | * Gets the options 53 | * 54 | * @method getOptions 55 | * @return {object} 56 | */ 57 | getOptions: function () { 58 | return this._options; 59 | }, 60 | 61 | 62 | /** 63 | * Validates the data given 64 | * 65 | * @method validate 66 | */ 67 | validate: function () { 68 | 69 | if (!_.isObject(this.getOptions())) { 70 | throw new Error('The options parameter is not an object.'); 71 | } 72 | if (!_.isBoolean(this.isDebug())) { 73 | throw new Error('The "debug" option is not a boolean.'); 74 | } 75 | if (!_.isBoolean(this.isVerbose())) { 76 | throw new Error('The "verbose" option is not a boolean.'); 77 | } 78 | if (!_.isBoolean(this.shouldIgnoreErrors())) { 79 | throw new Error('The "ignoreErrors" option is not a boolean.'); 80 | } 81 | if (!_.isObject(this.getReportManager())) { 82 | throw new Error('The "reportManager" parameter is not an object.'); 83 | } 84 | if (!_.isArray(this.getReporter())) { 85 | throw new Error('The "reporter" parameter is not an array.'); 86 | } 87 | if (!_.isArray(this.getListener())) { 88 | throw new Error('The "listener" parameter is not an array.'); 89 | } 90 | if (!_.isArray(this.getPlugins())) { 91 | throw new Error('The "plugins" parameter is not an array.'); 92 | } 93 | }, 94 | 95 | /** 96 | * Augments the data with default values 97 | * 98 | * @method augment 99 | */ 100 | augment: function () { 101 | // Nothing yet 102 | }, 103 | 104 | 105 | /** 106 | * Is application in debug mode? 107 | * 108 | * Note: In debug mode, the clients are run in the same process. 109 | * 110 | * @method isDebug 111 | * @return {boolean} 112 | */ 113 | isDebug: function () { 114 | return this.getOptions().debug; 115 | }, 116 | 117 | /** 118 | * Is application in verbose mode? 119 | * 120 | * @method isVerbose 121 | * @return {boolean} 122 | */ 123 | isVerbose: function () { 124 | return this.getOptions().verbose; 125 | }, 126 | 127 | /** 128 | * Should errors of tests be ignored? 129 | * 130 | * @method shouldIgnoreErrors 131 | * @return {boolean} 132 | */ 133 | shouldIgnoreErrors: function () { 134 | return this.getOptions().ignoreErrors; 135 | }, 136 | 137 | /** 138 | * Gets the report-manager configuration 139 | * 140 | * @method getReportManager 141 | * @return {object} 142 | */ 143 | getReportManager: function () { 144 | return this.getOptions().reportManager; 145 | }, 146 | 147 | /** 148 | * Gets the reporter configuration 149 | * 150 | * @method getReporter 151 | * @return {object[]} 152 | */ 153 | getReporter: function () { 154 | return this.getReportManager().reporter; 155 | }, 156 | 157 | /** 158 | * Gets the listener configuration 159 | * 160 | * @method getListener 161 | * @return {object[]} 162 | */ 163 | getListener: function () { 164 | return this.getReportManager().listener; 165 | }, 166 | 167 | /** 168 | * Gets the coverage options 169 | * 170 | * @method getCoverage 171 | * @return {Coverage} 172 | */ 173 | getCoverage: function () { 174 | return new Coverage(this.getOptions().coverage); 175 | }, 176 | 177 | /** 178 | * Gets the plugins configuration 179 | * 180 | * @method getPlugins 181 | * @return {string[]} 182 | */ 183 | getPlugins: function () { 184 | return this.getOptions().plugins; 185 | }, 186 | 187 | 188 | /** 189 | * Exports data to an object 190 | * 191 | * @method toObject 192 | * @return {object} 193 | */ 194 | toObject: function () { 195 | return utils.deepExtend({}, [this.getOptions(), { 196 | "coverage": this.getCoverage().toObject() 197 | }]); 198 | } 199 | }, 200 | 201 | { 202 | /** 203 | * @property TYPE 204 | * @type {string} 205 | * @static 206 | */ 207 | TYPE: 'Config' 208 | }); 209 | 210 | module.exports = Config; 211 | -------------------------------------------------------------------------------- /lib/clientDecorator/plain.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var AbstractClientDecorator = require('../abstractClientDecorator'); 5 | var Promise = require('promise'); 6 | 7 | /** 8 | * @class PlainClientDecorator 9 | * @extends AbstractClientDecorator 10 | * @constructor 11 | */ 12 | var PlainClientDecorator = AbstractClientDecorator.extend( 13 | 14 | { 15 | /** 16 | * Called when reporting starts 17 | * 18 | * @method start 19 | */ 20 | start: function () { 21 | console.log('PLAIN: start'); 22 | }, 23 | 24 | /** 25 | * Called when reporting stops 26 | * 27 | * @method stop 28 | */ 29 | stop: function () { 30 | console.log('PLAIN: stop'); 31 | }, 32 | 33 | 34 | /** 35 | * Reporting is completed 36 | * 37 | * @method complete 38 | */ 39 | complete: function () { 40 | console.log('PLAIN: complete'); 41 | }, 42 | 43 | 44 | /** 45 | * Called when any item has custom data 46 | * 47 | * @method itemData 48 | * @param {string} id 49 | * @param {string} json JSON-data 50 | */ 51 | itemData: function (id, json) { 52 | console.log('PLAIN: itemData', id, json); 53 | }, 54 | 55 | /** 56 | * Called when any item has a custom message 57 | * 58 | * @method itemMessage 59 | * @param {string} id 60 | * @param {string} message 61 | */ 62 | itemMessage: function (id, message) { 63 | console.log('PLAIN: itemMessage', id, message); 64 | }, 65 | 66 | 67 | /** 68 | * Called when suite starts 69 | * 70 | * @method suiteStart 71 | * @param {string} id 72 | * @param {string} parentId 73 | * @param {string} suiteName 74 | */ 75 | suiteStart: function (id, parentId, suiteName) { 76 | console.log('PLAIN: suiteStart', id, parentId, suiteName); 77 | }, 78 | 79 | /** 80 | * Called when suite ends 81 | * 82 | * @method suiteEnd 83 | * @param {string} id 84 | */ 85 | suiteEnd: function (id) { 86 | console.log('PLAIN: suiteEnd', id); 87 | }, 88 | 89 | 90 | /** 91 | * Called when test starts 92 | * 93 | * @method testStart 94 | * @param {string} id 95 | * @param {string} parentId 96 | * @param {string} testName 97 | */ 98 | testStart: function (id, parentId, testName) { 99 | console.log('PLAIN: testStart', id, parentId, testName); 100 | }, 101 | 102 | 103 | /** 104 | * Called when test fails 105 | * 106 | * @method testFailed 107 | * @param {string} id 108 | * @param {string} [message] 109 | * @param {string} [reason] 110 | */ 111 | testFailed: function (id, message, reason) { 112 | console.log('PLAIN: testFailed', id, message, reason); 113 | }, 114 | 115 | /** 116 | * Called when test has an error 117 | * 118 | * @method testError 119 | * @param {string} id 120 | * @param {string} [message] 121 | * @param {string} [reason] 122 | */ 123 | testError: function (id, message, reason) { 124 | console.log('PLAIN: testError', id, message, reason); 125 | }, 126 | 127 | /** 128 | * Called when test has passed 129 | * 130 | * @method testPassed 131 | * @param {string} id 132 | */ 133 | testPassed: function (id) { 134 | console.log('PLAIN: testPassed', id); 135 | }, 136 | 137 | /** 138 | * Called when test is undefined 139 | * 140 | * @method testUndefined 141 | * @param {string} id 142 | */ 143 | testUndefined: function (id) { 144 | console.log('PLAIN: testUndefined', id); 145 | }, 146 | 147 | /** 148 | * Called when test is skipped 149 | * 150 | * @method testSkipped 151 | * @param {string} id 152 | * @param {string} [reason] 153 | */ 154 | testSkipped: function (id, reason) { 155 | console.log('PLAIN: testSkipped', id, reason); 156 | }, 157 | 158 | /** 159 | * Called when test is incomplete 160 | * 161 | * @method testIncomplete 162 | * @param {string} id 163 | */ 164 | testIncomplete: function (id) { 165 | console.log('PLAIN: testIncomplete', id); 166 | }, 167 | 168 | 169 | /** 170 | * Processes the begin of the testing environment 171 | * 172 | * @method processBefore 173 | * @return {Promise} 174 | */ 175 | processBefore: function () { 176 | console.log('PLAIN: processBefore'); 177 | return Promise.resolve(); 178 | }, 179 | 180 | /** 181 | * Processes the end of the testing environment 182 | * 183 | * @method processAfter 184 | * @return {Promise} 185 | */ 186 | processAfter: function () { 187 | console.log('PLAIN: processAfter'); 188 | return Promise.resolve(); 189 | }, 190 | 191 | /** 192 | * Processes the beginning of a test 193 | * 194 | * @method processBeforeTest 195 | * @return {Promise} 196 | */ 197 | processBeforeTest: function () { 198 | console.log('PLAIN: processBeforeTest'); 199 | return Promise.resolve(); 200 | }, 201 | 202 | /** 203 | * Processes the ending of a test 204 | * 205 | * @method processAfterTest 206 | * @return {Promise} 207 | */ 208 | processAfterTest: function () { 209 | console.log('PLAIN: processAfterTest'); 210 | return Promise.resolve(); 211 | } 212 | }); 213 | 214 | module.exports = PlainClientDecorator; 215 | -------------------------------------------------------------------------------- /lib/task/group.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var AbstractTask = require('../abstractTask'); 5 | var logger = require('log4js').getLogger(__filename); 6 | var Promise = require('promise'); 7 | var utils = require('preceptor-core').utils; 8 | var _ = require('underscore'); 9 | 10 | var staticTaskCounter = 0; 11 | 12 | /** 13 | * @class GroupTask 14 | * @extends AbstractTask 15 | * @constructor 16 | */ 17 | var GroupTask = AbstractTask.extend( 18 | 19 | { 20 | /** 21 | * Initializes the instance 22 | * 23 | * @method initialize 24 | */ 25 | initialize: function () { 26 | 27 | if (!_.isArray(this.getTasks())) { 28 | this.getConfiguration().tasks = [this.getTasks()]; 29 | } 30 | 31 | this.getOptions().configuration = utils.deepExtend({}, [ 32 | { 33 | parallel: false, 34 | bail: true, 35 | tasks: [] 36 | }, 37 | this.getConfiguration() 38 | ]); 39 | 40 | this.__super(); 41 | }, 42 | 43 | 44 | /** 45 | * Validates the given data 46 | * 47 | * @method validate 48 | */ 49 | validate: function () { 50 | this.__super(); 51 | 52 | if (!_.isBoolean(this.isParallel())) { 53 | throw new Error('The "parallel" parameter is not a boolean.'); 54 | } 55 | if (!_.isArray(this.getTasks())) { 56 | throw new Error('The "tasks" parameter is not an array.'); 57 | } 58 | if (!_.isBoolean(this.shouldBail())) { 59 | throw new Error('The "bail" parameter is not a boolean.'); 60 | } 61 | }, 62 | 63 | 64 | /** 65 | * Run the tasks in parallel? 66 | * 67 | * @method isParallel 68 | * @return {boolean} 69 | */ 70 | isParallel: function () { 71 | return this.getConfiguration().parallel; 72 | }, 73 | 74 | /** 75 | * Gets the tasks assigned to this group 76 | * 77 | * @method getTasks 78 | * @return {object[]} 79 | */ 80 | getTasks: function () { 81 | return this.getConfiguration().tasks; 82 | }, 83 | 84 | /** 85 | * Bail execution when encountering the first error 86 | * 87 | * @method shouldBail 88 | * @return {boolean} 89 | */ 90 | shouldBail: function () { 91 | return this.getConfiguration().bail; 92 | }, 93 | 94 | 95 | /** 96 | * Applies all decorators 97 | * 98 | * @method applyTaskDecorators 99 | * @param {object[]} tasks 100 | * @param {AbstractTaskDecorator[]} decoratorList 101 | * @return {object[]} 102 | */ 103 | applyTaskDecorators: function (tasks, decoratorList) { 104 | 105 | var decoratorInstance, 106 | resultTasks, 107 | localTasks = tasks, 108 | i; 109 | 110 | // Process all decorators 111 | _.each(decoratorList, function (Decorator) { 112 | 113 | decoratorInstance = new Decorator(); 114 | 115 | // Process all tasks 116 | for(i = 0; i < localTasks.length; i++) { 117 | 118 | resultTasks = decoratorInstance.run(localTasks[i], i + staticTaskCounter); 119 | 120 | if (_.isArray(resultTasks)) { 121 | localTasks = localTasks.slice(0, i).concat(resultTasks, localTasks.slice(i + 1)); 122 | i += resultTasks.length - 1; 123 | } else if (_.isObject(resultTasks)) { 124 | localTasks[i] = resultTasks; 125 | } 126 | } 127 | 128 | }, this); 129 | staticTaskCounter += localTasks.length; 130 | 131 | return localTasks; 132 | }, 133 | 134 | 135 | /** 136 | * Run the client 137 | * 138 | * @method _run 139 | * @param {string} parentId 140 | * @return {Promise} 141 | * @private 142 | */ 143 | _run: function (parentId) { 144 | 145 | var promise = Promise.resolve(), 146 | promiseList = [], 147 | tasks; 148 | 149 | // Apply all decorator to get final list of tasks 150 | tasks = this.applyTaskDecorators(this.getTasks(), this.getTaskDecoratorPlugins()); 151 | if (tasks.length === 0) { 152 | throw new Error('A group without tasks. ' + this.getLabel()) 153 | } 154 | 155 | // Process each task 156 | _.each(tasks, function (taskOptions) { 157 | 158 | var TaskClass, 159 | taskInstance, 160 | currentPromise; 161 | 162 | // Find plugin 163 | TaskClass = this.getTaskPlugin(taskOptions.type); 164 | if (!TaskClass) { 165 | throw new Error('Unknown task: ' + taskOptions.type + ', ' + this.getLabel()); 166 | } 167 | 168 | // Create task instance 169 | taskInstance = new TaskClass( 170 | this.getGlobalConfig(), 171 | this.getCoverageCollector(), 172 | this.getReportManager(), 173 | this.getPlugins(), 174 | taskOptions 175 | ); 176 | 177 | // Run task 178 | currentPromise = promise.then(function () { 179 | logger.debug('Task started.', taskInstance.getLabel()); 180 | return taskInstance.run(parentId).then(function () { 181 | logger.debug('Task completed.', taskInstance.getLabel()); 182 | }); 183 | }.bind(this)); 184 | 185 | // Bail on execution when error or just log? 186 | currentPromise = currentPromise.then(null, function (err) { 187 | if (this.shouldBail()) { 188 | throw err; 189 | } else { 190 | logger.error('Error', err.stack.toString()); 191 | } 192 | }.bind(this)); 193 | 194 | // Run in parallel? 195 | if (this.isParallel()) { 196 | logger.debug('Run task in parallel.', taskInstance.getLabel()); 197 | promiseList.push(currentPromise); 198 | } else { 199 | logger.debug('Run task sequential.', taskInstance.getLabel()); 200 | promise = currentPromise; 201 | } 202 | 203 | }, this); 204 | 205 | // Wait for all to finish? 206 | if (this.isParallel()) { 207 | promise = Promise.all(promiseList); 208 | } 209 | 210 | return promise; 211 | } 212 | }); 213 | 214 | module.exports = GroupTask; 215 | -------------------------------------------------------------------------------- /lib/abstractForkTask.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var childProcess = require('child_process'); 5 | var AbstractTask = require('./abstractTask'); 6 | var _ = require('underscore'); 7 | var Promise = require('promise'); 8 | var path = require('path'); 9 | 10 | var logger = require('log4js').getLogger(__filename); 11 | 12 | /** 13 | * @class AbstractForkTask 14 | * @extends AbstractTask 15 | * @constructor 16 | */ 17 | var AbstractForkTask = AbstractTask.extend( 18 | 19 | { 20 | /** 21 | * Validates the given data 22 | * 23 | * @Method validate 24 | */ 25 | validate: function () { 26 | this.__super(); 27 | if (!_.isBoolean(this.shouldFailOnError())) { 28 | throw new Error('The "failOnError" parameter is not a boolean.'); 29 | } 30 | }, 31 | 32 | 33 | /** 34 | * Should task fail on error? 35 | * 36 | * @method shouldFailOnError 37 | * @return {boolean} 38 | */ 39 | shouldFailOnError: function () { 40 | return this.getOptions().failOnError; 41 | }, 42 | 43 | /** 44 | * Runs the client in a forked environment 45 | * 46 | * @method runClient 47 | * @param {string} parentId 48 | * @param {string} clientPath 49 | * @return {Promise} 50 | */ 51 | runClient: function (parentId, clientPath) { 52 | 53 | var options, 54 | shouldFailOnError = this.shouldFailOnError(), 55 | message; 56 | 57 | options = { 58 | type: "run", 59 | coverage: this.shouldCollectCoverage(), 60 | clientPath: clientPath, 61 | parentId: parentId, 62 | globalConfig: this.getGlobalConfig().toObject(), 63 | configuration: this.getConfiguration(), 64 | decorators: this.getDecorators(), 65 | decoratorPlugins: this.getClientDecoratorPlugins() 66 | }; 67 | 68 | return new Promise(function (resolve, reject) { 69 | 70 | var ClientClass, 71 | clientInstance; 72 | 73 | if (this.inDebug()) { 74 | logger.debug('Fork task-client in debug mode', clientPath); 75 | 76 | ClientClass = require(clientPath); 77 | clientInstance = new ClientClass(options.decorators, options.decoratorPlugins, options.configuration); 78 | 79 | clientInstance.on('reportMessage', function (messageType, data) { 80 | if (this.shouldReport()) { 81 | this.getReportManager().processMessage(messageType, data); 82 | } 83 | }.bind(this)); 84 | 85 | // Make global configuration available 86 | global.PRECEPTOR = { 87 | config: options.globalConfig 88 | }; 89 | 90 | clientInstance.run(parentId).then(function () { 91 | if (this.shouldCollectCoverage()) { 92 | this.getCoverageCollector().add(global.__preceptorCoverage__ || {}); 93 | global.__preceptorCoverage__ = {}; 94 | } 95 | resolve({ success: true }); 96 | }.bind(this), function (err) { 97 | if (this.shouldCollectCoverage()) { 98 | this.getCoverageCollector().add(global.__preceptorCoverage__ || {}); 99 | global.__preceptorCoverage__ = {}; 100 | } 101 | throw err; 102 | }.bind(this)).then(null, function (err) { 103 | if (shouldFailOnError) { 104 | reject(err); 105 | } else { 106 | logger.error(err.message); 107 | resolve(); 108 | } 109 | }); 110 | // In debug-mode, the client cannot be parsed 111 | 112 | } else { 113 | logger.debug('Fork task-client', clientPath, path.resolve(path.join(__dirname, 'client.js'))); 114 | clientInstance = childProcess.fork(path.resolve(path.join(__dirname, 'client.js')), { 115 | silent: true 116 | }); 117 | 118 | clientInstance.on('message', function (dataPackage) { 119 | var err; 120 | 121 | logger.debug('Received message from task-client', dataPackage); 122 | 123 | switch(dataPackage.type) { 124 | 125 | case 'exception': 126 | throw new Error(dataPackage.message); 127 | break; 128 | 129 | case 'completion': 130 | 131 | if (this.shouldCollectCoverage() && dataPackage.data.coverage) { 132 | this.getCoverageCollector().add(dataPackage.data.coverage); 133 | } 134 | 135 | if (dataPackage.data.success) { 136 | resolve(dataPackage.data); 137 | 138 | } else { 139 | err = new Error('Client failed with: ' + JSON.stringify(dataPackage.data.context)); 140 | if (shouldFailOnError) { 141 | reject(err); 142 | } else { 143 | logger.error(err.message); 144 | resolve(); 145 | } 146 | } 147 | break; 148 | 149 | case 'reportMessage': 150 | if (this.shouldReport()) { 151 | this.getReportManager().processMessage(dataPackage.messageType, dataPackage.data); 152 | } 153 | break; 154 | } 155 | }.bind(this)); 156 | 157 | clientInstance.stdout.on('data', function (data) { 158 | if (this.shouldReport()) { 159 | data = this.getReportManager().parse(data.toString('utf-8'), { "#TASK#": parentId }); 160 | if (this.shouldEchoStdOut()) { 161 | process.stdout.write((this.isVerbose() ? this.getName() + ": " : '') + data); 162 | } 163 | } 164 | }.bind(this)); 165 | 166 | clientInstance.stderr.on('data', function (data) { 167 | if (this.shouldReport()) { 168 | data = this.getReportManager().parse(data.toString('utf-8'), { "#TASK#": parentId }); 169 | if (this.shouldEchoStdErr()) { 170 | process.stderr.write((this.isVerbose() ? this.getName() + ": " : '') + data); 171 | } 172 | } 173 | }.bind(this)); 174 | 175 | clientInstance.send(options); 176 | } 177 | 178 | }.bind(this)); 179 | } 180 | }); 181 | 182 | module.exports = AbstractForkTask; 183 | -------------------------------------------------------------------------------- /lib/abstractClient.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('preceptor-core').Base; 5 | var utils = require('preceptor-core').utils; 6 | var ReportManager = require('preceptor-reporter'); 7 | var _ = require('underscore'); 8 | var Promise = require('promise'); 9 | 10 | var defaultClient = require('./defaults/defaultClient'); 11 | 12 | /** 13 | * @class AbstractClient 14 | * @extends Base 15 | * 16 | * @property {object} _options 17 | * @property {AbstractClientDecorator[]} decorators 18 | * @property {ReportManager} _reportManager 19 | * @property {EventReporter} _eventReporter 20 | */ 21 | var AbstractClient = Base.extend( 22 | 23 | /** 24 | * Abstract client constructor 25 | * 26 | * @param {object[]} decorators 27 | * @param {object} decoratorPlugins 28 | * @param {object} options 29 | * @constructor 30 | */ 31 | function (decorators, decoratorPlugins, options) { 32 | this.__super(); 33 | 34 | this._reportManager = new ReportManager({ collect: false }); 35 | this._eventReporter = this._reportManager.addReporter('Event'); 36 | this._eventReporter.on('message', function (areaType, messageType, data) { 37 | // Make sure not to forward admin messages; this should stay in the client 38 | if (["admin"].indexOf(areaType) === -1) { 39 | this.emit('reportMessage', messageType, data); 40 | } 41 | }.bind(this)); 42 | 43 | this._options = utils.deepExtend({}, [defaultClient, options || {}]); 44 | this.decorators = this._setupDecorators(this._eventReporter, decorators, decoratorPlugins); 45 | 46 | this.initialize(); 47 | }, 48 | 49 | { 50 | /** 51 | * Setup decorator plugins 52 | * 53 | * @method _setupDecorators 54 | * @param {EventReporter} eventReporter 55 | * @param {object[]} decorators 56 | * @param {object} decoratorPlugins 57 | * @return {AbstractClientDecorator[]} 58 | * @private 59 | */ 60 | _setupDecorators: function (eventReporter, decorators, decoratorPlugins) { 61 | 62 | var decoratorList = []; 63 | 64 | _.each(decorators, function (currentDecorator) { 65 | 66 | var decoratorPlugin = decoratorPlugins[currentDecorator.type.toLowerCase()], 67 | DecoratorClass, 68 | decoratorInstance; 69 | 70 | if (!decoratorPlugin) { 71 | throw new Error('Unknown decorator: ' + currentDecorator.type); 72 | } 73 | 74 | DecoratorClass = require(decoratorPlugin); 75 | decoratorInstance = new DecoratorClass(eventReporter, currentDecorator); 76 | decoratorList.push(decoratorInstance); 77 | 78 | }, this); 79 | 80 | return decoratorList; 81 | }, 82 | 83 | 84 | /** 85 | * Processes the begin of the testing environment 86 | * 87 | * @method processBefore 88 | * @return {Promise} 89 | */ 90 | processBefore: function () { 91 | 92 | var promise = Promise.resolve(); 93 | 94 | _.each(this.decorators, function (decorator) { 95 | promise = promise.then(function () { 96 | return decorator.processBefore(); 97 | }) 98 | }, this); 99 | 100 | return promise; 101 | }, 102 | 103 | /** 104 | * Processes the end of the testing environment 105 | * 106 | * @method processAfter 107 | * @return {Promise} 108 | */ 109 | processAfter: function () { 110 | 111 | var promise = Promise.resolve(); 112 | 113 | _.each(this.decorators, function (decorator) { 114 | promise = promise.then(function () { 115 | return decorator.processAfter(); 116 | }) 117 | }, this); 118 | 119 | return promise; 120 | }, 121 | 122 | /** 123 | * Processes the beginning of a test 124 | * 125 | * @method processBeforeTest 126 | * @return {Promise} 127 | */ 128 | processBeforeTest: function () { 129 | 130 | var promise = Promise.resolve(); 131 | 132 | _.each(this.decorators, function (decorator) { 133 | promise = promise.then(function () { 134 | return decorator.processBeforeTest(); 135 | }) 136 | }, this); 137 | 138 | return promise; 139 | }, 140 | 141 | /** 142 | * Processes the ending of a test 143 | * 144 | * @method processAfterTest 145 | * @return {Promise} 146 | */ 147 | processAfterTest: function () { 148 | 149 | var promise = Promise.resolve(); 150 | 151 | _.each(this.decorators, function (decorator) { 152 | promise = promise.then(function () { 153 | return decorator.processAfterTest(); 154 | }) 155 | }, this); 156 | 157 | return promise; 158 | }, 159 | 160 | 161 | /** 162 | * Gets the options 163 | * 164 | * @method getOptions 165 | * @return {object} 166 | */ 167 | getOptions: function () { 168 | return this._options; 169 | }, 170 | 171 | /** 172 | * Gets the decorator 173 | * 174 | * @method getDecorator 175 | * @return {AbstractClientDecorator[]} 176 | */ 177 | getDecorator: function () { 178 | return this.decorators; 179 | }, 180 | 181 | 182 | /** 183 | * Gets the report-manager 184 | * 185 | * @method getReportManager 186 | * @return {ReportManager} 187 | */ 188 | getReportManager: function () { 189 | return this._reportManager; 190 | }, 191 | 192 | /** 193 | * Gets the event-reporter 194 | * 195 | * @method getEventReporter 196 | * @return {EventReporter} 197 | */ 198 | getEventReporter: function () { 199 | return this._eventReporter; 200 | }, 201 | 202 | 203 | /** 204 | * Will be called when the client begins 205 | * 206 | * @method run 207 | * @return {Promise} 208 | */ 209 | run: function () { 210 | throw new Error('Unimplemented method "run" in the client.'); 211 | } 212 | }, 213 | 214 | { 215 | /** 216 | * @property TYPE 217 | * @type {string} 218 | * @static 219 | */ 220 | TYPE: 'AbstractClient' 221 | }); 222 | 223 | module.exports = AbstractClient; 224 | -------------------------------------------------------------------------------- /lib/task/client/cucumber.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var AbstractClient = require('../../abstractClient'); 5 | var Promise = require('promise'); 6 | var _ = require('underscore'); 7 | var utils = require('preceptor-core').utils; 8 | 9 | /** 10 | * @class CucumberClient 11 | * @extends AbstractClient 12 | * @constructor 13 | */ 14 | var CucumberClient = AbstractClient.extend( 15 | 16 | { 17 | /** 18 | * Initializes the instance 19 | * 20 | * @method initialize 21 | */ 22 | initialize: function () { 23 | 24 | var self = this; 25 | 26 | this.__super(); 27 | 28 | this.getFunctions().push(function () { 29 | 30 | this.BeforeFeatures(function (event, callback) { 31 | self.processBefore().then(function () { 32 | callback(); 33 | }, function (err) { 34 | console.error(err.stack); 35 | callback(err); 36 | }); 37 | }); 38 | this.AfterFeatures(function (event, callback) { 39 | self.processAfter().then(function () { 40 | callback(); 41 | }, function (err) { 42 | console.error(err.stack); 43 | callback(err); 44 | }); 45 | }); 46 | 47 | this.BeforeStep(function (event, callback) { 48 | self.processBeforeTest().then(function () { 49 | callback(); 50 | }, function (err) { 51 | console.error(err.stack); 52 | callback(err); 53 | }); 54 | }); 55 | this.AfterStep(function (event, callback) { 56 | self.processAfterTest().then(function () { 57 | callback(); 58 | }, function (err) { 59 | console.error(err.stack); 60 | callback(err); 61 | }); 62 | }); 63 | }); 64 | }, 65 | 66 | 67 | /** 68 | * Gets the cucumber configuration 69 | * Overwrite this function if the cucumber configuration is found somewhere else. 70 | * 71 | * @method getCucumberConfiguration 72 | * @return {object} 73 | */ 74 | getCucumberConfiguration: function () { 75 | return this.getOptions(); 76 | }, 77 | 78 | /** 79 | * Sets the cucumber configuration 80 | * Overwrite this function if the cucumber configuration is found somewhere else. 81 | * 82 | * @method setCucumberConfiguration 83 | * @param {object} options 84 | */ 85 | setCucumberConfiguration: function (options) { 86 | this._options = options; 87 | }, 88 | 89 | 90 | /** 91 | * Gets the path to the text files 92 | * Cucumber Option: 93 | * 94 | * @method getPath 95 | * @return {string} 96 | */ 97 | getPath: function () { 98 | return this.getCucumberConfiguration().path; 99 | }, 100 | 101 | /** 102 | * Gets the tags to include/exclude 103 | * Cucumber Option: tags 104 | * 105 | * @method getTags 106 | * @return {string[]} 107 | */ 108 | getTags: function () { 109 | return this.getCucumberConfiguration().tags; 110 | }, 111 | 112 | /** 113 | * Gets the required file before running the tests 114 | * Cucumber Option: require 115 | * 116 | * @method getRequires 117 | * @return {string[]} 118 | */ 119 | getRequires: function () { 120 | return this.getCucumberConfiguration().require; 121 | }, 122 | 123 | /** 124 | * Gets the functions to execute as part of files to execute 125 | * Cucumber Option: 126 | * 127 | * @method getFunctions 128 | * @return {function[]} 129 | */ 130 | getFunctions: function () { 131 | return this.getCucumberConfiguration().functions; 132 | }, 133 | 134 | /** 135 | * Should output in coffee-script? 136 | * Cucumber Option: coffee 137 | * 138 | * @method shouldOutputCoffeeScript 139 | * @return {boolean} 140 | */ 141 | shouldOutputCoffeeScript: function () { 142 | return this.getCucumberConfiguration().coffeeScript; 143 | }, 144 | 145 | /** 146 | * Gets the output format for cucumber 147 | * Cucumber Option: format 148 | * 149 | * @method getFormat 150 | * @return {string} 151 | */ 152 | getFormat: function () { 153 | return this.getCucumberConfiguration().format; 154 | }, 155 | 156 | 157 | /** 158 | * Execute client 159 | * 160 | * @method run 161 | * @param {string} parentId 162 | * @return {Promise} 163 | */ 164 | run: function (parentId) { 165 | 166 | return new Promise(function (resolve, reject) { 167 | 168 | var hook, 169 | options, 170 | done; 171 | 172 | hook = this.getReportManager().loadHook('cucumber', parentId); 173 | 174 | options = this.getCucumberConfiguration(); 175 | options.functions.push(hook); 176 | 177 | this.getReportManager().message().start(); 178 | done = function () { 179 | this.getReportManager().message().stop(); 180 | this.getReportManager().message().complete(); 181 | }.bind(this); 182 | 183 | this._runCucumber(options, function () { 184 | done(); 185 | resolve.apply(this, arguments); 186 | }, function () { 187 | done(); 188 | reject.apply(this, arguments); 189 | }); 190 | 191 | }.bind(this)); 192 | }, 193 | 194 | /** 195 | * Runs the cucumber tests, self-contained 196 | * 197 | * @method _runCucumber 198 | * @param {object} options 199 | * @param {string} options.path 200 | * @param {string[]} [options.tags] 201 | * @param {string[]} [options.require] 202 | * @param {function[]} [options.functions] 203 | * @param {string} [options.format] 204 | * @param {boolean} [options.coffeeScript] 205 | * @param {function} success 206 | * @param {function} failure 207 | * @private 208 | */ 209 | _runCucumber: function (options, success, failure) { 210 | 211 | var args = [], 212 | cli, 213 | Cucumber = require('cucumber'); 214 | 215 | if (options.tags) { 216 | _.each(options.tags, function (tagList) { 217 | if (!_.isArray(tagList)) { 218 | tagList = tagList.split(','); 219 | } 220 | args.push('--tags'); 221 | args.push(tagList.join(',')); 222 | }); 223 | } 224 | 225 | if (options.format) { 226 | args.push('--format'); 227 | args.push(options.format); 228 | } 229 | 230 | if (options.require) { 231 | _.each(options.require, function (require) { 232 | args.push('--require'); 233 | args.push(require); 234 | }); 235 | } 236 | 237 | if (options.coffeeScript) { 238 | args.push('--coffee'); 239 | } 240 | 241 | args.push(options.path); 242 | 243 | // Adds the "functions" feature to cucumber 244 | (function () { 245 | var originalSupportCodeLoader = Cucumber.Cli.SupportCodeLoader, 246 | firstTimeLoaded = false; 247 | 248 | // Overwrite the constructor 249 | Cucumber.Cli.SupportCodeLoader = function () { 250 | 251 | var result = originalSupportCodeLoader.apply(this, arguments), 252 | originalBuildSupportCodeInitializerFromPaths = result.buildSupportCodeInitializerFromPaths; 253 | 254 | // Overwrite the resulting functions since they are created for each object in the constructor 255 | result.buildSupportCodeInitializerFromPaths = function () { 256 | 257 | var wrapperFn = originalBuildSupportCodeInitializerFromPaths.apply(this, arguments); 258 | 259 | // Overwrite the wrapper returned from the builder function 260 | return function () { 261 | 262 | // Execute functions as if they were in files 263 | if (!firstTimeLoaded) { 264 | firstTimeLoaded = true; 265 | 266 | _.each(options.functions, function (fn) { 267 | fn.call(this); 268 | }, this); 269 | } 270 | 271 | wrapperFn.apply(this, arguments); 272 | } 273 | }; 274 | 275 | return result; 276 | }; 277 | }()); 278 | 279 | // Add some standard arguments that will be cut out in cucumber anyways 280 | args.unshift('node'); 281 | args.unshift('cucumber.js'); 282 | 283 | // Run cucumber with options supplied 284 | cli = Cucumber.Cli(args); 285 | cli.run(function (succeeded) { 286 | if (succeeded) { 287 | success(succeeded); 288 | } else { 289 | failure(new Error('Failed')); 290 | } 291 | }); 292 | 293 | return cli; 294 | } 295 | }); 296 | 297 | module.exports = CucumberClient; 298 | -------------------------------------------------------------------------------- /lib/task/mocha.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var AbstractForkTask = require('../abstractForkTask'); 5 | var path = require('path'); 6 | var utils = require('preceptor-core').utils; 7 | var _ = require('underscore'); 8 | 9 | /** 10 | * @class MochaTask 11 | * @extends AbstractForkTask 12 | * @constructor 13 | */ 14 | var MochaTask = AbstractForkTask.extend( 15 | 16 | { 17 | /** 18 | * Initializes the instance 19 | * 20 | * @method initialize 21 | */ 22 | initialize: function () { 23 | this.setMochaConfiguration(utils.deepExtend({}, [ 24 | { 25 | reporter: 'spec', 26 | ui: 'bdd', 27 | colors: true, 28 | inlineDiffs: false, 29 | slow: 75, 30 | timeOuts: true, 31 | timeOut: 2000, 32 | bail: false, 33 | grep: false, 34 | invert: false, 35 | checkLeaks: false, 36 | asyncOnly: false, 37 | globals: [], 38 | paths: [], 39 | functions: [], 40 | recursive: false, 41 | require: [], 42 | sort: false 43 | }, 44 | this.getMochaConfiguration() 45 | ])); 46 | 47 | this.__super(); 48 | }, 49 | 50 | 51 | /** 52 | * Validates the given data 53 | * 54 | * @method validate 55 | */ 56 | validate: function () { 57 | this.__super(); 58 | 59 | if (!_.isString(this.getReporter())) { 60 | throw new Error('The "reporter" parameter is not a string.'); 61 | } 62 | if (!_.isString(this.getUi())) { 63 | throw new Error('The "ui" parameter is not a string.'); 64 | } 65 | if (!_.isBoolean(this.useColors())) { 66 | throw new Error('The "colors" parameter is not a boolean.'); 67 | } 68 | if (!_.isBoolean(this.useInlineDiffs())) { 69 | throw new Error('The "inlineDiffs" parameter is not a boolean.'); 70 | } 71 | if (!_.isNumber(this.getSlowThreshold())) { 72 | throw new Error('The "slow" parameter is not a number.'); 73 | } 74 | if (!_.isBoolean(this.useTimeOuts())) { 75 | throw new Error('The "timeOuts" parameter is not a boolean.'); 76 | } 77 | if (!_.isNumber(this.getTimeOut())) { 78 | throw new Error('The "timeOut" parameter is not a number.'); 79 | } 80 | if (!_.isBoolean(this.shouldBail())) { 81 | throw new Error('The "bail" parameter is not a boolean.'); 82 | } 83 | if (!_.isString(this.getGrep()) && !_.isBoolean(this.getGrep())) { 84 | throw new Error('The "grep" parameter is not a string or boolean.'); 85 | } 86 | if (!_.isBoolean(this.shouldInvert())) { 87 | throw new Error('The "invert" parameter is not a boolean.'); 88 | } 89 | if (!_.isBoolean(this.shouldCheckLeaks())) { 90 | throw new Error('The "checkLeaks" parameter is not a boolean.'); 91 | } 92 | if (!_.isBoolean(this.useAsyncOnly())) { 93 | throw new Error('The "asyncOnly" parameter is not a boolean.'); 94 | } 95 | if (!_.isArray(this.getGlobals())) { 96 | throw new Error('The "globals" parameter is not an array of strings.'); 97 | } 98 | if (!_.isArray(this.getPaths())) { 99 | throw new Error('The "paths" parameter is not an array of strings.'); 100 | } 101 | if (!_.isArray(this.getFunctions())) { 102 | throw new Error('The "functions" parameter is not an array of strings.'); 103 | } 104 | if (!_.isBoolean(this.getRecursive())) { 105 | throw new Error('The "recursive" parameter is not a boolean.'); 106 | } 107 | if (!_.isArray(this.getRequire())) { 108 | throw new Error('The "require" parameter is not an array of strings.'); 109 | } 110 | if (!_.isBoolean(this.shouldSort())) { 111 | throw new Error('The "sort" parameter is not a boolean.'); 112 | } 113 | }, 114 | 115 | 116 | /** 117 | * Gets the mocha configuration 118 | * Overwrite this function if the mocha configuration is found somewhere else. 119 | * 120 | * @method getMochaConfiguration 121 | * @return {object} 122 | */ 123 | getMochaConfiguration: function () { 124 | return this.getConfiguration(); 125 | }, 126 | 127 | /** 128 | * Sets the mocha configuration 129 | * Overwrite this function if the mocha configuration is found somewhere else. 130 | * 131 | * @method setMochaConfiguration 132 | * @param {object} options 133 | */ 134 | setMochaConfiguration: function (options) { 135 | this.getOptions().configuration = options; 136 | }, 137 | 138 | 139 | /** 140 | * Gets the reporter 141 | * Mocha Option: reporter 142 | * 143 | * @method getReporter 144 | * @return {string} 145 | */ 146 | getReporter: function () { 147 | return this.getMochaConfiguration().reporter; 148 | }, 149 | 150 | /** 151 | * Gets the UI interface ('tdd', 'bdd') 152 | * Mocha Option: ui 153 | * 154 | * @method getUi 155 | * @return {string} 156 | */ 157 | getUi: function () { 158 | return this.getMochaConfiguration().ui; 159 | }, 160 | 161 | /** 162 | * Should colors be used in output? 163 | * Mocha Option: colors 164 | * 165 | * @method useColors 166 | * @return {boolean} 167 | */ 168 | useColors: function () { 169 | return this.getMochaConfiguration().colors; 170 | }, 171 | 172 | /** 173 | * Output inline diffs 174 | * Mocha Option: inline-diffs 175 | * 176 | * @method useInlineDiffs 177 | * @return {boolean} 178 | */ 179 | useInlineDiffs: function () { 180 | return this.getMochaConfiguration().inlineDiffs; 181 | }, 182 | 183 | /** 184 | * Gets the threshold for slow tests 185 | * Mocha Option: slow 186 | * 187 | * @method getSlowThreshold 188 | * @return {int} 189 | */ 190 | getSlowThreshold: function () { 191 | return this.getMochaConfiguration().slow; 192 | }, 193 | 194 | /** 195 | * Should time-outs be observed? 196 | * Mocha Option: [no-]timeouts 197 | * 198 | * @method useTimeOuts 199 | * @return {boolean} 200 | */ 201 | useTimeOuts: function () { 202 | return this.getMochaConfiguration().timeOuts; 203 | }, 204 | 205 | /** 206 | * Gets the threshold for too slow test-suites 207 | * Mocha Option: timeout 208 | * 209 | * @method getTimeOut 210 | * @return {int} 211 | */ 212 | getTimeOut: function () { 213 | return this.getMochaConfiguration().timeOut; 214 | }, 215 | 216 | /** 217 | * Should mocha bail on first error? 218 | * Mocha Option: bail 219 | * 220 | * @method shouldBail 221 | * @return {boolean} 222 | */ 223 | shouldBail: function () { 224 | return this.getMochaConfiguration().bail; 225 | }, 226 | 227 | /** 228 | * Gets the test filter 229 | * Mocha Option: grep 230 | * 231 | * @method getGrep 232 | * @return {string|boolean} 233 | */ 234 | getGrep: function () { 235 | return this.getMochaConfiguration().grep; 236 | }, 237 | 238 | /** 239 | * Should the test sequence inverted? 240 | * Mocha Option: invert 241 | * 242 | * @method shouldInvert 243 | * @return {boolean} 244 | */ 245 | shouldInvert: function () { 246 | return this.getMochaConfiguration().invert; 247 | }, 248 | 249 | /** 250 | * Should mocha check for leaks 251 | * Mocha Option: check-leaks 252 | * 253 | * @method shouldCheckLeaks 254 | * @return {boolean} 255 | */ 256 | shouldCheckLeaks: function () { 257 | return this.getMochaConfiguration().checkLeaks; 258 | }, 259 | 260 | /** 261 | * Gets the reporter 262 | * Mocha Option: async-only 263 | * 264 | * @Method useAsyncOnly 265 | * @return {boolean} 266 | */ 267 | useAsyncOnly: function () { 268 | return this.getMochaConfiguration().asyncOnly; 269 | }, 270 | 271 | /** 272 | * Gets the list of defined globals 273 | * Mocha Option: globals 274 | * 275 | * @method getGlobals 276 | * @return {string[]} 277 | */ 278 | getGlobals: function () { 279 | return this.getMochaConfiguration().globals; 280 | }, 281 | 282 | /** 283 | * Gets the path of all tests 284 | * Mocha Option: 285 | * 286 | * @method getPaths 287 | * @return {string[]} 288 | */ 289 | getPaths: function () { 290 | return this.getMochaConfiguration().paths; 291 | }, 292 | 293 | /** 294 | * Gets a list of functions to execute before the tests 295 | * Mocha Option: 296 | * 297 | * @Method getFunctions 298 | * @return {function[]} 299 | */ 300 | getFunctions: function () { 301 | return this.getMochaConfiguration().functions; 302 | }, 303 | 304 | /** 305 | * Should the test-search be recursive? 306 | * Mocha Option: recursive 307 | * 308 | * @method getRecursive 309 | * @return {boolean} 310 | */ 311 | getRecursive: function () { 312 | return this.getMochaConfiguration().recursive; 313 | }, 314 | 315 | /** 316 | * List of files to be required before the tests are run 317 | * Mocha Option: require 318 | * 319 | * @method getRequire 320 | * @return {string[]} 321 | */ 322 | getRequire: function () { 323 | return this.getMochaConfiguration().require; 324 | }, 325 | 326 | /** 327 | * Should tests be sorted after gathering and before executing? 328 | * Mocha Option: sort 329 | * 330 | * @method shouldSort 331 | * @return {boolean} 332 | */ 333 | shouldSort: function () { 334 | return this.getMochaConfiguration().sort; 335 | }, 336 | 337 | 338 | /** 339 | * Run the client 340 | * 341 | * @method _run 342 | * @param {string} parentId 343 | * @return {Promise} 344 | * @private 345 | */ 346 | _run: function (parentId) { 347 | return this.runClient(parentId, path.join(__dirname, 'client', 'mocha')); 348 | } 349 | }); 350 | 351 | module.exports = MochaTask; 352 | -------------------------------------------------------------------------------- /lib/abstractTask.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('preceptor-core').Base; 5 | var utils = require('preceptor-core').utils; 6 | var logger = require('log4js').getLogger(__filename); 7 | var Promise = require('promise'); 8 | var _ = require('underscore'); 9 | var uuid = require('uuid'); 10 | 11 | var defaultTask = require('./defaults/defaultTask'); 12 | 13 | /** 14 | * @class AbstractTask 15 | * @extends Base 16 | * 17 | * @property {ReportManager} _reportManager 18 | * @property {object} _plugins 19 | * @property {object} _options 20 | * @property {object} _config 21 | * @property {Collector} _coverageCollector 22 | */ 23 | var AbstractTask = Base.extend( 24 | 25 | /** 26 | * Abstract task constructor 27 | * 28 | * @param {object} config 29 | * @param {Collector} coverageCollector 30 | * @param {ReportManager} reportManager 31 | * @param {object} plugins 32 | * @param {object} plugins.taskDecorator 33 | * @param {object} plugins.clientDecorator 34 | * @param {object} plugins.task 35 | * @param {object} options 36 | * @constructor 37 | */ 38 | function (config, coverageCollector, reportManager, plugins, options) { 39 | this.__super(); 40 | 41 | this._config = config; 42 | this._coverageCollector = coverageCollector; 43 | this._reportManager = reportManager; 44 | this._plugins = plugins; 45 | this._options = utils.deepExtend({}, [defaultTask, plugins.sharedOptions || {}, options || {}]); 46 | 47 | this.initialize(); 48 | }, 49 | 50 | { 51 | /** 52 | * Initializes the instance 53 | * 54 | * @method initialize 55 | */ 56 | initialize: function () { 57 | 58 | // Make sure the configuration has the correct structure 59 | this.validate(); 60 | 61 | // Augment options with outside data 62 | this.augment(); 63 | }, 64 | 65 | 66 | /** 67 | * Validates the given data 68 | * 69 | * @method validate 70 | */ 71 | validate: function () { 72 | if (!_.isObject(this.getOptions())) { 73 | throw new Error('The options parameter is not an object.'); 74 | } 75 | if (!_.isObject(this.getConfiguration())) { 76 | throw new Error('The "configuration" parameter is not an object.'); 77 | } 78 | if (!_.isObject(this.getDecorators())) { 79 | throw new Error('The "decorators" parameter is not an object.'); 80 | } 81 | if (!_.isString(this.getType())) { 82 | throw new Error('The "type" parameter is not a string.'); 83 | } 84 | if (!_.isString(this.getTaskId())) { 85 | throw new Error('The "taskId" parameter is not a string.'); 86 | } 87 | if (!_.isString(this.getName())) { 88 | throw new Error('The "name" parameter is not a string.'); 89 | } 90 | if (!_.isString(this.getTitle())) { 91 | throw new Error('The "title" parameter is not a string.'); 92 | } 93 | if (!_.isBoolean(this.isSuite())) { 94 | throw new Error('The "suite" parameter is not a boolean.'); 95 | } 96 | if (!_.isBoolean(this.isActive())) { 97 | throw new Error('The "active" parameter is not a boolean.'); 98 | } 99 | if (!_.isBoolean(this.isVerbose())) { 100 | throw new Error('The "verbose" parameter is not a boolean.'); 101 | } 102 | if (!_.isBoolean(this.shouldReport())) { 103 | throw new Error('The "report" parameter is not a boolean.'); 104 | } 105 | if (!_.isBoolean(this.shouldEchoStdOut())) { 106 | throw new Error('The "echoStdOut" parameter is not a boolean.'); 107 | } 108 | if (!_.isBoolean(this.shouldEchoStdErr())) { 109 | throw new Error('The "echoStdErr" parameter is not a boolean.'); 110 | } 111 | }, 112 | 113 | /** 114 | * Augments the data with default values 115 | * 116 | * @method augment 117 | */ 118 | augment: function () { 119 | // Nothing yet 120 | }, 121 | 122 | 123 | /** 124 | * Gets the options 125 | * 126 | * @method getOptions 127 | * @return {object} 128 | */ 129 | getOptions: function () { 130 | return this._options; 131 | }, 132 | 133 | /** 134 | * Gets the client configuration 135 | * 136 | * @method getConfiguration 137 | * @return {object} 138 | */ 139 | getConfiguration: function () { 140 | return this.getOptions().configuration; 141 | }, 142 | 143 | /** 144 | * Gets the decorator list 145 | * 146 | * @method getDecorators 147 | * @return {object[]} 148 | */ 149 | getDecorators: function () { 150 | return this.getOptions().decorators; 151 | }, 152 | 153 | /** 154 | * Gets the global configuration 155 | * 156 | * @method getGlobalConfig 157 | * @returns {Object} 158 | */ 159 | getGlobalConfig: function () { 160 | return this._config; 161 | }, 162 | 163 | /** 164 | * Gets the coverage collector 165 | * 166 | * @method getCoverageCollector 167 | * @returns {Collector} 168 | */ 169 | getCoverageCollector: function () { 170 | return this._coverageCollector; 171 | }, 172 | 173 | /** 174 | * Gets the type of the preceptor task 175 | * 176 | * @method getType 177 | * @return {string} 178 | */ 179 | getType: function () { 180 | return this.getOptions().type; 181 | }, 182 | 183 | /** 184 | * Gets a unique id for the task 185 | * 186 | * @method getTaskId 187 | * @return {string} 188 | */ 189 | getTaskId: function () { 190 | return this.getOptions().taskId; 191 | }, 192 | 193 | /** 194 | * Gets the name of the preceptor task 195 | * 196 | * @method getName 197 | * @return {string} 198 | */ 199 | getName: function () { 200 | return this.getOptions().name; 201 | }, 202 | 203 | /** 204 | * Gets the title of the preceptor task 205 | * 206 | * @method getTitle 207 | * @return {string} 208 | */ 209 | getTitle: function () { 210 | return this.getOptions().title; 211 | }, 212 | 213 | /** 214 | * Run tasks in a suite? 215 | * 216 | * @method isSuite 217 | * @return {boolean} 218 | */ 219 | isSuite: function () { 220 | return this.getOptions().suite; 221 | }, 222 | 223 | /** 224 | * Is the task in debug-mode? 225 | * 226 | * @method inDebug 227 | * @return {boolean} 228 | */ 229 | inDebug: function () { 230 | return this.getOptions().debug; 231 | }, 232 | 233 | /** 234 | * Is the task active? 235 | * 236 | * @method isActive 237 | * @return {boolean} 238 | */ 239 | isActive: function () { 240 | return this.getOptions().active; 241 | }, 242 | 243 | /** 244 | * Is the task verbose? 245 | * 246 | * @method isVerbose 247 | * @return {boolean} 248 | */ 249 | isVerbose: function () { 250 | return this.getOptions().verbose; 251 | }, 252 | 253 | /** 254 | * Should report? 255 | * 256 | * @method shouldReport 257 | * @return {boolean} 258 | */ 259 | shouldReport: function () { 260 | return this.getOptions().report; 261 | }, 262 | 263 | /** 264 | * Should collect coverage? 265 | * 266 | * @method shouldCollectCoverage 267 | * @return {boolean} 268 | */ 269 | shouldCollectCoverage: function () { 270 | return this.getOptions().coverage; 271 | }, 272 | 273 | /** 274 | * Echo std-out output of child-process? 275 | * 276 | * @method shouldEchoStdOut 277 | * @return {boolean} 278 | */ 279 | shouldEchoStdOut: function () { 280 | return this.getOptions().echoStdOut; 281 | }, 282 | 283 | /** 284 | * Echo std-err output of child-process? 285 | * 286 | * @method shouldEchoStdErr 287 | * @return {boolean} 288 | */ 289 | shouldEchoStdErr: function () { 290 | return this.getOptions().echoStdErr; 291 | }, 292 | 293 | 294 | /** 295 | * Gets all plugins 296 | * 297 | * @method getPlugins 298 | * @return {object} 299 | */ 300 | getPlugins: function () { 301 | return this._plugins; 302 | }, 303 | 304 | /** 305 | * Gets all options-decorator plugins 306 | * 307 | * @method getTaskDecoratorPlugins 308 | * @return {AbstractTaskDecorator[]} 309 | */ 310 | getTaskDecoratorPlugins: function () { 311 | return _.values(this.getPlugins().taskDecorator); 312 | }, 313 | 314 | /** 315 | * Gets all client-decorator plugins 316 | * 317 | * @method getClientDecoratorPlugins 318 | * @return {object[]} 319 | */ 320 | getClientDecoratorPlugins: function () { 321 | return this.getPlugins().clientDecorator; 322 | }, 323 | 324 | /** 325 | * Gets a specific task plugin 326 | * 327 | * @method getTaskPlugin 328 | * @param {string} name 329 | * @return {AbstractTask} 330 | */ 331 | getTaskPlugin: function (name) { 332 | return this.getPlugins().task[name.toLowerCase()]; 333 | }, 334 | 335 | 336 | /** 337 | * Gets the report manager 338 | * 339 | * @method getReportManager 340 | * @return {ReportManager} 341 | */ 342 | getReportManager: function () { 343 | return this._reportManager; 344 | }, 345 | 346 | 347 | /** 348 | * Gets the label of the task 349 | * 350 | * @method getLabel 351 | * @return {string} 352 | */ 353 | getLabel: function () { 354 | return this.getName() + '-' + this.getTaskId(); 355 | }, 356 | 357 | 358 | /** 359 | * Run the task 360 | * 361 | * @method run 362 | * @param {string} parentId 363 | * @return {Promise} 364 | */ 365 | run: function (parentId) { 366 | 367 | var suiteId = parentId, 368 | promise; 369 | 370 | if (this.isActive()) { 371 | 372 | // Should task be wrapped in a suite? Start it 373 | if (this.isSuite()) { 374 | suiteId = 'group-' + uuid.v4(); // Generate unique-id 375 | this.getReportManager().message().suiteStart(suiteId, parentId, this.getTitle()); 376 | } 377 | 378 | // Run task 379 | promise = this._run(suiteId); 380 | 381 | // Should tasks be wrapped in a suite? Finish it up 382 | if (this.isSuite()) { 383 | promise = promise.then(function () { 384 | this.getReportManager().message().suiteEnd(suiteId); 385 | }.bind(this), function (err) { 386 | this.getReportManager().message().suiteEnd(suiteId); 387 | throw err; 388 | }.bind(this)); 389 | } 390 | 391 | return promise; 392 | 393 | } else { 394 | logger.debug('Skip task since it is inactive. ' + this.getLabel()); 395 | return Promise.resolve(); 396 | } 397 | }, 398 | 399 | /** 400 | * Run the task 401 | * 402 | * @method _run 403 | * @param {string} parentId 404 | * @return {Promise} 405 | * @private 406 | */ 407 | _run: function (parentId) { 408 | throw new Error('Unimplemented task function "run".'); 409 | } 410 | }, 411 | 412 | { 413 | /** 414 | * @property TYPE 415 | * @type {string} 416 | * @static 417 | */ 418 | TYPE: 'AbstractTask' 419 | }); 420 | 421 | module.exports = AbstractTask; 422 | -------------------------------------------------------------------------------- /lib/task/client/mocha.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var AbstractClient = require('../../abstractClient'); 5 | var Promise = require('promise'); 6 | var _ = require('underscore'); 7 | var utils = require('preceptor-core').utils; 8 | var fs = require('fs'); 9 | var glob = require('glob'); 10 | var path = require('path'); 11 | 12 | var exists = fs.existsSync || path.existsSync; 13 | 14 | Error.stackTraceLimit = Infinity; 15 | 16 | /** 17 | * @class MochaClient 18 | * @extends AbstractClient 19 | * @constructor 20 | */ 21 | var MochaClient = AbstractClient.extend( 22 | 23 | { 24 | /** 25 | * Initializes the instance 26 | * 27 | * @method initialize 28 | */ 29 | initialize: function () { 30 | 31 | var self = this; 32 | 33 | this.__super(); 34 | 35 | this.getFunctions().push(function () { 36 | before(function (done) { 37 | self.processBefore().then(function () { 38 | done(); 39 | }, function (err) { 40 | console.error(err.stack); 41 | done(err); 42 | }); 43 | }); 44 | after(function (done) { 45 | self.processAfter().then(function () { 46 | done(); 47 | }, function (err) { 48 | console.error(err.stack); 49 | done(err); 50 | }); 51 | }); 52 | beforeEach(function (done) { 53 | self.processBeforeTest().then(function () { 54 | done(); 55 | }, function (err) { 56 | console.error(err.stack); 57 | done(err); 58 | }); 59 | }); 60 | afterEach(function (done) { 61 | self.processAfterTest().then(function () { 62 | done(); 63 | }, function (err) { 64 | console.error(err.stack); 65 | done(err); 66 | }); 67 | }); 68 | }); 69 | }, 70 | 71 | 72 | /** 73 | * Gets the mocha configuration 74 | * Overwrite this function if the mocha configuration is found somewhere else. 75 | * 76 | * @method getMochaConfiguration 77 | * @return {object} 78 | */ 79 | getMochaConfiguration: function () { 80 | return this.getOptions(); 81 | }, 82 | 83 | /** 84 | * Sets the mocha configuration 85 | * Overwrite this function if the mocha configuration is found somewhere else. 86 | * 87 | * @method setMochaConfiguration 88 | * @param {object} options 89 | */ 90 | setMochaConfiguration: function (options) { 91 | this._options = options; 92 | }, 93 | 94 | 95 | /** 96 | * Gets the reporter 97 | * Mocha Option: reporter 98 | * 99 | * @method getReporter 100 | * @return {string} 101 | */ 102 | getReporter: function () { 103 | return this.getMochaConfiguration().reporter; 104 | }, 105 | 106 | /** 107 | * Gets the UI interface ('tdd', 'bdd') 108 | * Mocha Option: ui 109 | * 110 | * @method getUi 111 | * @return {string} 112 | */ 113 | getUi: function () { 114 | return this.getMochaConfiguration().ui; 115 | }, 116 | 117 | /** 118 | * Should colors be used in output? 119 | * Mocha Option: colors 120 | * 121 | * @method useColors 122 | * @return {boolean} 123 | */ 124 | useColors: function () { 125 | return this.getMochaConfiguration().colors; 126 | }, 127 | 128 | /** 129 | * Output inline diffs 130 | * Mocha Option: inline-diffs 131 | * 132 | * @method useInlineDiffs 133 | * @return {boolean} 134 | */ 135 | useInlineDiffs: function () { 136 | return this.getMochaConfiguration().inlineDiffs; 137 | }, 138 | 139 | /** 140 | * Gets the threshold for slow tests 141 | * Mocha Option: slow 142 | * 143 | * @method getSlowThreshold 144 | * @return {int} 145 | */ 146 | getSlowThreshold: function () { 147 | return this.getMochaConfiguration().slow; 148 | }, 149 | 150 | /** 151 | * Should time-outs be observed? 152 | * Mocha Option: [no-]timeouts 153 | * 154 | * @method useTimeOuts 155 | * @return {boolean} 156 | */ 157 | useTimeOuts: function () { 158 | return this.getMochaConfiguration().timeOuts; 159 | }, 160 | 161 | /** 162 | * Gets the threshold for too slow test-suites 163 | * Mocha Option: timeout 164 | * 165 | * @method getTimeOut 166 | * @return {int} 167 | */ 168 | getTimeOut: function () { 169 | return this.getMochaConfiguration().timeOut; 170 | }, 171 | 172 | /** 173 | * Should mocha bail on first error? 174 | * Mocha Option: bail 175 | * 176 | * @method shouldBail 177 | * @return {boolean} 178 | */ 179 | shouldBail: function () { 180 | return this.getMochaConfiguration().bail; 181 | }, 182 | 183 | /** 184 | * Gets the test filter 185 | * Mocha Option: grep 186 | * 187 | * @method getGrep 188 | * @return {string|boolean} 189 | */ 190 | getGrep: function () { 191 | return this.getMochaConfiguration().grep; 192 | }, 193 | 194 | /** 195 | * Should the test sequence inverted? 196 | * Mocha Option: invert 197 | * 198 | * @method shouldInvert 199 | * @return {boolean} 200 | */ 201 | shouldInvert: function () { 202 | return this.getMochaConfiguration().invert; 203 | }, 204 | 205 | /** 206 | * Should mocha check for leaks 207 | * Mocha Option: check-leaks 208 | * 209 | * @method shouldCheckLeaks 210 | * @return {boolean} 211 | */ 212 | shouldCheckLeaks: function () { 213 | return this.getMochaConfiguration().checkLeaks; 214 | }, 215 | 216 | /** 217 | * Gets the reporter 218 | * Mocha Option: async-only 219 | * 220 | * @method useAsyncOnly 221 | * @return {boolean} 222 | */ 223 | useAsyncOnly: function () { 224 | return this.getMochaConfiguration().asyncOnly; 225 | }, 226 | 227 | /** 228 | * Gets the list of defined globals 229 | * Mocha Option: globals 230 | * 231 | * @method getGlobals 232 | * @return {string[]} 233 | */ 234 | getGlobals: function () { 235 | return this.getMochaConfiguration().globals; 236 | }, 237 | 238 | /** 239 | * Gets the path of all tests 240 | * Mocha Option: 241 | * 242 | * @method getPaths 243 | * @return {string[]} 244 | */ 245 | getPaths: function () { 246 | return this.getMochaConfiguration().paths; 247 | }, 248 | 249 | /** 250 | * Gets a list of functions to execute before the tests 251 | * Mocha Option: 252 | * 253 | * @method getFunctions 254 | * @return {function[]} 255 | */ 256 | getFunctions: function () { 257 | return this.getMochaConfiguration().functions; 258 | }, 259 | 260 | /** 261 | * Should the test-search be recursive? 262 | * Mocha Option: recursive 263 | * 264 | * @method getRecursive 265 | * @return {boolean} 266 | */ 267 | getRecursive: function () { 268 | return this.getMochaConfiguration().recursive; 269 | }, 270 | 271 | /** 272 | * List of files to be required before the tests are run 273 | * Mocha Option: require 274 | * 275 | * @method getRequire 276 | * @return {string[]} 277 | */ 278 | getRequire: function () { 279 | return this.getMochaConfiguration().require; 280 | }, 281 | 282 | /** 283 | * Should tests be sorted after gathering and before executing? 284 | * Mocha Option: sort 285 | * 286 | * @method shouldSort 287 | * @return {boolean} 288 | */ 289 | shouldSort: function () { 290 | return this.getMochaConfiguration().sort; 291 | }, 292 | 293 | 294 | /** 295 | * Execute client 296 | * 297 | * @method run 298 | * @param {string} parentId 299 | * @return {Promise} 300 | */ 301 | run: function (parentId) { 302 | 303 | return new Promise(function (resolve, reject) { 304 | 305 | var instance, 306 | hook, 307 | done; 308 | 309 | hook = this.getReportManager().loadHook('mocha', parentId); 310 | 311 | this.getReportManager().message().start(); 312 | done = function () { 313 | this.getReportManager().message().stop(); 314 | this.getReportManager().message().complete(); 315 | }.bind(this); 316 | 317 | instance = this._runMocha(this.getMochaConfiguration(), function () { 318 | done(); 319 | resolve.apply(this, arguments); 320 | }, function () { 321 | done(); 322 | reject.apply(this, arguments); 323 | }); 324 | hook(instance); 325 | 326 | }.bind(this)); 327 | }, 328 | 329 | /** 330 | * Runs mocha tests, self-contained 331 | * 332 | * @method _runMocha 333 | * @param {object} options 334 | * @param {string} options.reporter 335 | * @param {string} options.ui 336 | * @param {boolean} options.colors 337 | * @param {boolean} options.inlineDiffs 338 | * @param {int} options.slow 339 | * @param {boolean} options.timeOuts 340 | * @param {int} options.timeOut 341 | * @param {boolean} options.bail 342 | * @param {boolean|string} options.grep 343 | * @param {boolean} options.invert 344 | * @param {boolean} options.checkLeaks 345 | * @param {boolean} options.asyncOnly 346 | * @param {string[]} options.globals 347 | * @param {string[]} [options.paths=['test']] 348 | * @param {functions[]} options.functions 349 | * @param {boolean} options.recursive 350 | * @param {string|string[]} options.require 351 | * @param {boolean} options.sort 352 | * @param {function} success 353 | * @param {function} failure 354 | * @return {*} 355 | * @private 356 | */ 357 | _runMocha: function (options, success, failure) { 358 | 359 | var files = [], 360 | mocha, 361 | runner, 362 | paths, 363 | mochaLoadFiles, 364 | cwd = process.cwd(), 365 | Mocha = require('mocha'); 366 | 367 | options = options || {}; 368 | 369 | mocha = new Mocha(); 370 | mochaLoadFiles = mocha.loadFiles; 371 | 372 | module.paths.push(cwd, path.join(cwd, 'node_modules')); 373 | 374 | // --reporter 375 | mocha.reporter(options.reporter); 376 | 377 | // --ui 378 | mocha.ui(options.ui); 379 | 380 | // --colors 381 | if (options.colors === false) { 382 | mocha.useColors(false); 383 | } else { 384 | mocha.useColors(true); 385 | } 386 | 387 | // --inline-diffs 388 | mocha.useInlineDiffs(options.inlineDiffs); 389 | 390 | // --slow 391 | mocha.suite.slow(options.slow); 392 | 393 | // --[no-]timeouts 394 | mocha.enableTimeouts(options.timeOuts); 395 | 396 | // --timeout 397 | mocha.suite.timeout(options.timeOut); 398 | 399 | // --bail 400 | mocha.suite.bail(options.bail); 401 | 402 | // --grep 403 | if (options.grep) { 404 | mocha.grep(new RegExp(options.grep)); 405 | } 406 | 407 | // --invert 408 | if (options.invert) { 409 | mocha.invert(); 410 | } 411 | 412 | // --check-leaks 413 | if (options.checkLeaks) { 414 | mocha.checkLeaks(); 415 | } 416 | 417 | // --async-only 418 | if (options.asyncOnly) { 419 | mocha.asyncOnly(); 420 | } 421 | 422 | // --globals 423 | mocha.globals(options.globals); 424 | 425 | // Find all test-files 426 | paths = options.paths; 427 | if ((paths.length === 0) && (options.functions.length > 0)) { 428 | paths = ['test']; 429 | } 430 | _.each(paths, function(path) { 431 | files = files.concat(lookupFiles(path, options.recursive)); 432 | }); 433 | 434 | if (options.require) { 435 | _.each(options.require, function (requireFile) { 436 | require(path.resolve(requireFile)); 437 | }); 438 | } 439 | 440 | // Resolve to full path 441 | files = files.map(function (filePath) { 442 | return path.resolve(filePath); 443 | }); 444 | 445 | // --sort 446 | if (options.sort) { 447 | files.sort(); 448 | } 449 | 450 | if ((files.length === 0) && (options.functions.length > 0)) { 451 | files.push(path.join(__dirname, 'resources', 'empty.js')); 452 | } 453 | 454 | // Load files 455 | mocha.files = files; 456 | 457 | // Add prepared functions instead of files 458 | if (options.functions) { 459 | mocha.loadFiles = function () { 460 | 461 | // Load the functions 462 | _.each(options.functions, function (currentFn, index) { 463 | var fileName = currentFn.name || 'function-' + index; 464 | mocha.suite.emit('pre-require', global, fileName, mocha); 465 | mocha.suite.emit('require', (_.isString(currentFn) ? require(currentFn) : currentFn()), fileName, mocha); 466 | mocha.suite.emit('post-require', global, fileName, mocha); 467 | }); 468 | 469 | // Load all the files 470 | mochaLoadFiles.apply(this, arguments); 471 | 472 | // Add the functions as "files" 473 | _.each(options.functions, function (currentFn, index) { 474 | var fileName = currentFn.name || 'function-' + index; 475 | mocha.files.push(fileName); 476 | }); 477 | }; 478 | } 479 | 480 | // Run mocha tests 481 | runner = mocha.run(function (code) { 482 | if (code === 0) { 483 | success(code); 484 | } else { 485 | failure(new Error('Failed')); 486 | } 487 | }); 488 | 489 | process.on('SIGINT', function() { 490 | runner.abort(); 491 | }); 492 | 493 | /** 494 | * Lookup all files in the path given 495 | * 496 | * @method lookupFiles 497 | * @param {string} currentPath 498 | * @param {boolean} [recursive=false] 499 | * @return {string[]} 500 | * @private 501 | */ 502 | function lookupFiles (currentPath, recursive) { 503 | 504 | var files = [], 505 | stat; 506 | 507 | if (!exists(currentPath)) { 508 | 509 | if (exists(currentPath + '.js')) { 510 | currentPath += '.js' 511 | 512 | } else { 513 | files = glob.sync(currentPath); 514 | if (!files.length) { 515 | throw new Error("cannot resolve path (or pattern) '" + currentPath + "'"); 516 | } 517 | return files; 518 | } 519 | } 520 | 521 | stat = fs.statSync(currentPath); 522 | if (stat.isFile()) { 523 | return [currentPath]; 524 | 525 | } else { 526 | 527 | fs.readdirSync(currentPath).forEach(function (file) { 528 | var stat; 529 | 530 | file = path.join(currentPath, file); 531 | 532 | stat = fs.statSync(file); 533 | if (stat.isDirectory()) { 534 | 535 | if (recursive) { 536 | files = files.concat(lookupFiles(file, recursive)); 537 | } 538 | 539 | } else if (stat.isFile() && (path.basename(file)[0] !== '.')) { 540 | files.push(file); 541 | } 542 | }); 543 | 544 | return files; 545 | } 546 | } 547 | 548 | return runner; 549 | } 550 | }); 551 | 552 | module.exports = MochaClient; 553 | -------------------------------------------------------------------------------- /lib/manager.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014, Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('preceptor-core').Base; 5 | var utils = require('preceptor-core').utils; 6 | var ReportManager = require('preceptor-reporter'); 7 | var Promise = require('promise'); 8 | var _ = require('underscore'); 9 | var log4js = require('log4js'); 10 | var path = require('path'); 11 | var istanbul = require('istanbul'); 12 | var mkdirp = require('mkdirp'); 13 | var fs = require('fs'); 14 | 15 | var Config = require('./config'); 16 | var GroupTask = require('./task/group'); 17 | 18 | var defaultShared = require('./defaults/defaultShared'); 19 | 20 | var logger = log4js.getLogger(__filename); 21 | 22 | /** 23 | * @class PreceptorManager 24 | * @extends Base 25 | * 26 | * @property {object} _options 27 | * @property {Config} _configuration 28 | * 29 | * @property {object} _taskDecoratorList 30 | * @property {object} _clientDecoratorList 31 | * @property {object} _taskList 32 | * 33 | * @property {ReportManager} _reportManager 34 | * 35 | * @property {Collector} _coverageCollector 36 | */ 37 | var PreceptorManager = Base.extend( 38 | 39 | /** 40 | * Web-driver server constructor 41 | * 42 | * @param {object} options 43 | * @constructor 44 | */ 45 | function (options) { 46 | this.__super(); 47 | 48 | log4js.setGlobalLogLevel('INFO'); 49 | this._options = options || {}; 50 | 51 | this._taskDecoratorList = {}; 52 | this._clientDecoratorList = {}; 53 | this._taskList = {}; 54 | 55 | this._reportManager = null; 56 | 57 | this._coverageCollector = new istanbul.Collector(); 58 | 59 | this.initialize(); 60 | }, 61 | 62 | { 63 | /** 64 | * Initializes the instance 65 | * 66 | * @method initialize 67 | */ 68 | initialize: function () { 69 | 70 | // Initialize registries 71 | this._initializeTaskDecoratorRegistry(); 72 | this._initializeClientDecoratorRegistry(); 73 | this._initializeTaskRegistry(); 74 | 75 | // Make sure the configuration has the correct structure 76 | this.validate(); 77 | 78 | // Augment options with outside data 79 | this.augment(); 80 | 81 | // Create config object 82 | this._configuration = new Config(this._options.configuration); 83 | if (this.getConfig().isVerbose()) { 84 | log4js.setGlobalLogLevel('DEBUG'); 85 | } 86 | 87 | // Set-up the report-manager 88 | this._reportManager = new ReportManager(this.getConfig().getReportManager()); 89 | 90 | // Load custom plugins 91 | _.each(this.getConfig().getPlugins(), function (pluginPath) { 92 | var Plugin = require(pluginPath); 93 | 94 | if (Plugin.loader && _.isFunction(Plugin.loader)) { 95 | Plugin.loader(this); 96 | } else { 97 | throw new Error("The plugin " + pluginPath + " doesn't have a loader."); 98 | } 99 | }, this); 100 | }, 101 | 102 | /** 103 | * Initializes the options-decorator registry 104 | * 105 | * @method _initializeTaskDecoratorRegistry 106 | * @private 107 | */ 108 | _initializeTaskDecoratorRegistry: function () { 109 | var defaultElements = [ 110 | { name: 'group', fileName: 'group' }, 111 | { name: 'identifier', fileName: 'identifier' }, 112 | { name: 'decorator', fileName: 'decorator' } 113 | ]; 114 | 115 | _.each(defaultElements, function (entry) { 116 | entry.path = path.join(__dirname, 'taskDecorator', entry.fileName); 117 | entry.fn = require(entry.path); 118 | }, this); 119 | 120 | this.registerTaskDecoratorRange(defaultElements); 121 | }, 122 | 123 | /** 124 | * Initializes the client-decorator registry 125 | * 126 | * @method _initializeClientDecoratorRegistry 127 | * @private 128 | */ 129 | _initializeClientDecoratorRegistry: function () { 130 | var defaultElements = [ 131 | { name: 'plain', fileName: 'plain' } 132 | ]; 133 | 134 | _.each(defaultElements, function (entry) { 135 | entry.path = path.join(__dirname, 'clientDecorator', entry.fileName); 136 | }, this); 137 | 138 | this.registerClientDecoratorRange(defaultElements); 139 | }, 140 | 141 | /** 142 | * Initializes the task registry 143 | * 144 | * @method _initializeTaskRegistry 145 | * @private 146 | */ 147 | _initializeTaskRegistry: function () { 148 | var defaultElements = [ 149 | { name: 'cucumber', fileName: 'cucumber' }, 150 | { name: 'group', fileName: 'group' }, 151 | { name: 'kobold', fileName: 'kobold' }, 152 | { name: 'loader', fileName: 'loader' }, 153 | { name: 'mocha', fileName: 'mocha' }, 154 | { name: 'node', fileName: 'node' }, 155 | { name: 'shell', fileName: 'shell' } 156 | ]; 157 | 158 | _.each(defaultElements, function (entry) { 159 | entry.path = path.join(__dirname, 'task', entry.fileName); 160 | entry.fn = require(entry.path); 161 | }, this); 162 | 163 | this.registerTaskRange(defaultElements); 164 | }, 165 | 166 | 167 | /** 168 | * Gets the options 169 | * 170 | * @method getOptions 171 | * @return {object} 172 | */ 173 | getOptions: function () { 174 | return this._options; 175 | }, 176 | 177 | /** 178 | * Gets the configuration object 179 | * 180 | * @method getConfig 181 | * @return {Config} 182 | */ 183 | getConfig: function () { 184 | return this._configuration; 185 | }, 186 | 187 | /** 188 | * Gets all the shared options for tasks 189 | * 190 | * @method getSharedTaskOptions 191 | * @return {object} 192 | */ 193 | getSharedTaskOptions: function () { 194 | return this.getOptions().shared || {}; 195 | }, 196 | 197 | /** 198 | * Gets a list of task options 199 | * 200 | * @method getTasks 201 | * @return {object[]} 202 | */ 203 | getTasks: function () { 204 | return this.getOptions().tasks || []; 205 | }, 206 | 207 | 208 | /** 209 | * Gets the report-manager 210 | * 211 | * @method getReportManager 212 | * @return {ReportManager} 213 | */ 214 | getReportManager: function () { 215 | return this._reportManager; 216 | }, 217 | 218 | 219 | /** 220 | * Validate data given 221 | * 222 | * @method validate 223 | */ 224 | validate: function () { 225 | if (!_.isObject(this.getOptions())) { 226 | throw new Error('The options parameter is not an object.'); 227 | } 228 | if (!_.isObject(this.getSharedTaskOptions())) { 229 | throw new Error('The shared section is not an object.'); 230 | } 231 | if (!_.isArray(this.getTasks()) && !_.isObject(this.getTasks())) { 232 | throw new Error('The shared section is not an object or array.'); 233 | } 234 | }, 235 | 236 | /** 237 | * Augment data with default values 238 | * 239 | * @method augment 240 | */ 241 | augment: function () { 242 | 243 | // Inject default values 244 | this.getOptions().shared = utils.deepExtend({}, [defaultShared, this.getSharedTaskOptions()]); 245 | logger.debug('Augmented options', this._options); 246 | }, 247 | 248 | 249 | /** 250 | * Gets a dictionary of registered options-decorator 251 | * 252 | * @method getTaskDecoratorList 253 | * @return {object} 254 | */ 255 | getTaskDecoratorList: function () { 256 | return this._taskDecoratorList; 257 | }, 258 | 259 | /** 260 | * Checks if a options-decorator is registered 261 | * 262 | * @method hasTaskDecorator 263 | * @param {string} name 264 | * @return {boolean} 265 | */ 266 | hasTaskDecorator: function (name) { 267 | return !!this._taskDecoratorList[name.toLowerCase()]; 268 | }, 269 | 270 | /** 271 | * Gets a specific registered options-decorator 272 | * 273 | * @method getTaskDecorator 274 | * @param {string} name 275 | * @return {function} 276 | */ 277 | getTaskDecorator: function (name) { 278 | return this._taskDecoratorList[name.toLowerCase()]; 279 | }, 280 | 281 | /** 282 | * Registers a options-decorator 283 | * 284 | * @method registerTaskDecorator 285 | * @param {string} name 286 | * @param {function} contr 287 | */ 288 | registerTaskDecorator: function (name, contr) { 289 | this._taskDecoratorList[name.toLowerCase()] = contr; 290 | }, 291 | 292 | /** 293 | * Registers a list of options-decorators 294 | * 295 | * @method registerTaskDecoratorRange 296 | * @param {object[]} list `{ name: , fn: }` 297 | */ 298 | registerTaskDecoratorRange: function (list) { 299 | _.each(list, function (entry) { 300 | this.registerTaskDecorator(entry.name, entry.fn); 301 | }, this); 302 | }, 303 | 304 | 305 | /** 306 | * Gets a dictionary of registered client-decorator 307 | * 308 | * @method getClientDecoratorList 309 | * @return {object} 310 | */ 311 | getClientDecoratorList: function () { 312 | return this._clientDecoratorList; 313 | }, 314 | 315 | /** 316 | * Checks if a client-decorator is registered 317 | * 318 | * @method hasClientDecorator 319 | * @param {string} name 320 | * @return {boolean} 321 | */ 322 | hasClientDecorator: function (name) { 323 | return !!this._clientDecoratorList[name.toLowerCase()]; 324 | }, 325 | 326 | /** 327 | * Gets a specific registered client-decorator 328 | * 329 | * @method getClientDecorator 330 | * @param {string} name 331 | * @return {function} 332 | */ 333 | getClientDecorator: function (name) { 334 | return this._clientDecoratorList[name.toLowerCase()]; 335 | }, 336 | 337 | /** 338 | * Registers a client-decorator 339 | * 340 | * @method registerClientDecorator 341 | * @param {string} name 342 | * @param {string} path 343 | */ 344 | registerClientDecorator: function (name, path) { 345 | this._clientDecoratorList[name.toLowerCase()] = path; 346 | }, 347 | 348 | /** 349 | * Registers a list of client-decorators 350 | * 351 | * @method registerClientDecoratorRange 352 | * @param {object[]} list `{ name: , path: }` 353 | */ 354 | registerClientDecoratorRange: function (list) { 355 | _.each(list, function (entry) { 356 | this.registerClientDecorator(entry.name, entry.path); 357 | }, this); 358 | }, 359 | 360 | 361 | /** 362 | * Gets a dictionary of registered tasks 363 | * 364 | * @method getTaskList 365 | * @return {object} 366 | */ 367 | getTaskList: function () { 368 | return this._taskList; 369 | }, 370 | 371 | /** 372 | * Checks if a task is registered 373 | * 374 | * @method hasTask 375 | * @param {string} name 376 | * @return {boolean} 377 | */ 378 | hasTask: function (name) { 379 | return !!this._taskList[name.toLowerCase()]; 380 | }, 381 | 382 | /** 383 | * Gets a specific registered task 384 | * 385 | * @method getTask 386 | * @param {string} name 387 | * @return {function} 388 | */ 389 | getTask: function (name) { 390 | return this._taskList[name.toLowerCase()]; 391 | }, 392 | 393 | /** 394 | * Registers a task 395 | * 396 | * @method registerTask 397 | * @param {string} name 398 | * @param {function} contr 399 | */ 400 | registerTask: function (name, contr) { 401 | this._taskList[name.toLowerCase()] = contr; 402 | }, 403 | 404 | /** 405 | * Registers a list of task 406 | * 407 | * @method registerTaskRange 408 | * @param {object[]} list `{ name: , fn: }` 409 | */ 410 | registerTaskRange: function (list) { 411 | _.each(list, function (entry) { 412 | this.registerTask(entry.name, entry.fn); 413 | }, this); 414 | }, 415 | 416 | 417 | /** 418 | * Run the preceptor application 419 | * 420 | * @method run 421 | * @return {Promise} 422 | */ 423 | run: function () { 424 | var promise = Promise.resolve(), 425 | config = this.getConfig(), 426 | reportManager = this.getReportManager(), 427 | coverageCollector = this._coverageCollector, 428 | eventReporter; 429 | 430 | logger.debug('Shared', this.getSharedTaskOptions()); 431 | logger.debug('Config', config.toObject()); 432 | logger.debug('Tasks', this.getTasks()); 433 | 434 | logger.debug('Create reporter'); 435 | eventReporter = reportManager.addReporter("Event"); // Needed to forward it to the client-decorator 436 | reportManager.addReporterRange(config.getReporter()); 437 | reportManager.addListenerRange(config.getListener()); 438 | 439 | // Output events from reporter 440 | eventReporter.on('message', function (areaType, messageType, params) { 441 | logger.debug('Event-Reporter: [' + areaType + '] ' + messageType, params); 442 | }); 443 | 444 | // Start reporter 445 | reportManager.message().start(); 446 | 447 | // Run task 448 | promise = promise.then(function () { 449 | 450 | var task = new GroupTask(config, coverageCollector, reportManager, { 451 | taskDecorator: this.getTaskDecoratorList(), 452 | clientDecorator: this.getClientDecoratorList(), 453 | task: this.getTaskList(), 454 | sharedOptions: this.getSharedTaskOptions() 455 | }, { 456 | type: 'group', 457 | taskId: 'root-task', 458 | name: 'Root Task', 459 | title: 'Preceptor', 460 | configuration: { 461 | parallel: false, 462 | tasks: this.getTasks() 463 | } 464 | }); 465 | return task.run(undefined); 466 | 467 | }.bind(this)); 468 | 469 | // Stop reporter before leaving 470 | promise = promise.then(function () { 471 | reportManager.message().stop(); 472 | reportManager.message().complete(); 473 | if (this.getConfig().getCoverage().isActive()) { 474 | this._writeCoverage(this.getConfig().getCoverage()); 475 | } 476 | }.bind(this), function (err) { 477 | reportManager.message().stop(); 478 | reportManager.message().complete(); 479 | if (this.getConfig().getCoverage().isActive()) { 480 | this._writeCoverage(this.getConfig().getCoverage()); 481 | } 482 | throw err; 483 | }.bind(this)); 484 | 485 | return promise; 486 | }, 487 | 488 | /** 489 | * Write the coverage report collected 490 | * 491 | * @method _writeCoverage 492 | * @param {object} coverageConfiguration 493 | * @private 494 | */ 495 | _writeCoverage: function (coverageConfiguration) { 496 | var istanbulOverride, istanbulConfiguration, reporter, reportingDir, coverageFile, coverage, reports; 497 | 498 | // Setup configuration 499 | istanbulOverride = { 500 | "instrumentation": { 501 | "root": coverageConfiguration.getRoot() 502 | }, 503 | "reporting": { 504 | "dir": coverageConfiguration.getPath() 505 | } 506 | }; 507 | istanbulConfiguration = istanbul.config.loadObject(null, istanbulOverride); 508 | 509 | // Get configuration options 510 | reportingDir = path.resolve(istanbulConfiguration.reporting.dir()); 511 | coverageFile = path.resolve(reportingDir, 'coverage.json'); 512 | reports = coverageConfiguration.getReports() || []; 513 | 514 | // Create folder for reporting if not exists 515 | mkdirp.sync(reportingDir); 516 | 517 | if (reports.indexOf('file') !== -1) { 518 | 519 | // Write JSON file 520 | coverage = this._coverageCollector.getFinalCoverage(); 521 | fs.writeFileSync(coverageFile, JSON.stringify(coverage, 4)); 522 | 523 | // Filter "file"-report 524 | reports = reports.filter(function (entry) { 525 | return entry !== 'file'; 526 | }); 527 | } 528 | 529 | // Write report 530 | reporter = new istanbul.Reporter(istanbulConfiguration); 531 | reporter.addAll(reports); 532 | reporter.write(this._coverageCollector, true, function () {}); 533 | } 534 | }, 535 | 536 | { 537 | /** 538 | * @property TYPE 539 | * @type {string} 540 | * @static 541 | */ 542 | TYPE: 'Preceptor' 543 | }); 544 | 545 | // Expose plugin interfaces 546 | 547 | /** 548 | * @property AbstractClient 549 | * @type {AbstractClient} 550 | * @static 551 | */ 552 | PreceptorManager.AbstractClient = require('./abstractClient'); 553 | 554 | /** 555 | * @property AbstractClientDecorator 556 | * @type {AbstractClientDecorator} 557 | * @static 558 | */ 559 | PreceptorManager.AbstractClientDecorator = require('./abstractClientDecorator'); 560 | 561 | /** 562 | * @property AbstractForkTask 563 | * @type {AbstractForkTask} 564 | * @static 565 | */ 566 | PreceptorManager.AbstractForkTask = require('./abstractForkTask'); 567 | 568 | /** 569 | * @property AbstractTaskDecorator 570 | * @type {AbstractTaskDecorator} 571 | * @static 572 | */ 573 | PreceptorManager.AbstractTaskDecorator = require('./abstractTaskDecorator'); 574 | 575 | /** 576 | * @property AbstractTask 577 | * @type {AbstractTask} 578 | * @static 579 | */ 580 | PreceptorManager.AbstractTask = require('./abstractTask'); 581 | 582 | /** 583 | * @property version 584 | * @type {string} 585 | * @static 586 | */ 587 | PreceptorManager.version = require('../package.json').version; 588 | 589 | module.exports = PreceptorManager; 590 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Preceptor Logo](https://raw.githubusercontent.com/yahoo/preceptor/master/images/logo1.png) 2 | 3 | Preceptor is a test-runner and test-aggregator that runs multiple tests and testing frameworks in parallel, sequential, or a combination there of, aggregating all of the test-results and coverage-reports. 4 | 5 | 6 | [![Build Status](https://secure.travis-ci.org/yahoo/preceptor.png)](http://travis-ci.org/yahoo/preceptor) 7 | [![npm version](https://badge.fury.io/js/preceptor.svg)](http://badge.fury.io/js/preceptor) 8 | 9 | [![NPM](https://nodei.co/npm/preceptor.png?downloads=true)](https://nodei.co/npm/preceptor/) 10 | 11 | 12 | [API-Documentation](http://yahoo.github.io/preceptor/docs/) 13 | 14 | [Coverage Report](http://yahoo.github.io/preceptor/coverage/lcov-report/) 15 | 16 | ##Quick Look 17 | Main-Features: 18 | * Runs tests sequentially, in parallel, or a combination thereof. 19 | * Support of multiple commonly used testing frameworks including: Mocha, Cucumber, and Kobold. 20 | * Test-report aggregation, combining all of the test-results in one place, supporting a big list of commonly used reporters, including display reporters such as “Spec” and “Dot” and file-format reporters such as “JUnit” and “TAP”, all of which can be used at the same time. 21 | * Centralized collection of code-coverage metrics for every test that ran, merging all of the results in one place, giving the developers and testers a complete picture of what has been tested, and what might needs more tests. 22 | 23 | Features through plugins: 24 | * Selenium test management, starting-up and tearing-down Selenium servers transparently for the developers and testers, injecting code to the test-client, reducing with it the boilerplate code needed to run Selenium tests. This will also help simplify the configuration of the Selenium test system. 25 | * Client-side code-coverage collection, merging the code-coverage reports of an application that ran in a browser. 26 | 27 | 28 | 29 | **Table of Contents** 30 | * [Installation](#installation) 31 | * [What is Preceptor?](#what-is-preceptor) 32 | * [Getting Started](#getting-started) 33 | * [Usage](#usage) 34 | * [Client-API](#client-api) 35 | * [Command-Line Usage](#command-line-usage) 36 | * [Testing Lifecycle](#testing-lifecycle) 37 | * [Administrative states](#administrative-states) 38 | * [Suite management](#suite-management) 39 | * [Test management](#test-management) 40 | * [Configuration](#configuration) 41 | * [Global configuration](#global-configuration) 42 | * [Reporting](#reporting) 43 | * [Coverage](#coverage) 44 | * [Plugins](#plugins) 45 | * [Shared configuration](#shared-configuration) 46 | * [Tasks](#tasks) 47 | * [General configuration](#general-configuration) 48 | * [Cucumber Task](#cucumber-task) 49 | * [Mocha Task](#mocha-task) 50 | * [Kobold Task](#kobold-task) 51 | * [Loader Task](#loader-task) 52 | * [Node Task](#node-task) 53 | * [Shell Task](#shell-task) 54 | * [Group Task](#group-task) 55 | * [Task Decorators](#task-decorators) 56 | * [Client Decorators](#client-decorators) 57 | * [Client Runner](#client-runner) 58 | * [Plugin Naming](#plugin-naming) 59 | * [API-Documentation](#api-documentation) 60 | * [Tests](#tests) 61 | * [Third-party libraries](#third-party-libraries) 62 | * [License](#license) 63 | 64 | 65 | ##Installation 66 | 67 | Install this module with the following command: 68 | ```shell 69 | npm install preceptor 70 | ``` 71 | 72 | To add the module to your ```package.json``` dependencies: 73 | ```shell 74 | npm install --save preceptor 75 | ``` 76 | To add the module to your ```package.json``` dev-dependencies: 77 | ```shell 78 | npm install --save-dev preceptor 79 | ``` 80 | 81 | ##What is Preceptor? 82 | 83 | Today, there are a lot of testing frameworks out there, many of which are optimized for testing specific areas. A couple of these frameworks are: 84 | * *Mocha* - The most popular unit-testing tool (according to NPM downloads), testing each individual unit/component of a system in isolation. 85 | * *Cucumber* - A high-level acceptance testing tool that tests features through scenarios, giving product owners and teams a higher degree of collaboration, and management a deeper understanding of the systems and to what extend it is tested. 86 | * *Kobold* - A visual regression testing tool, making sure that any visual changes are recognized before the code goes out. 87 | * ... 88 | 89 | All of these testing-frameworks produce their own test results - often in different output formats. Additional testing tools, like Selenium, create even more results, generating the results for tests that were run in multiple browsers. CI systems help here a bit since they can usually handle multiple results, but all the results are usually plugged into one root-node, making it hard to organize and analyse the test-results. Some engineers spend a good amount of time aggregating all of these results in a reasonable manner, creating often one-off hacks to merge all the results. Others just manually parse through the results, determining if any kind of problems were encountered during the most recent test-run, loosing the ability to have a one-look green/red summary. 90 | 91 | Code-coverage metrics pose a similar problem: code-coverage reports are created independently and in isolation of tests that run for the same system in different testing frameworks or in separate tests (unit, component, integration, functional, acceptance, ...). It is challenging to have a complete overview of all coverage metrics as there is never only one coverage-report for all the tests. What is usually the case is that only some tests have coverage reports, possibly having only a subset of the metrics that could have been collected - most testing systems, for example, are not collecting client-side coverage-reports when running integration tests with Selenium; these are valuable coverage results. 92 | 93 | In addition, all of these tools need to be coordinated, running sequentially, in parallel, or even a combination thereof. However, many testing tools do not have this capability build-in, resulting in third-party developers create their own solutions which are quite often not much more than a hack, having also quite possibly inconsistent interfaces as they are usually not created by the same people. All of this coordination can easily turn the code-base into an unmaintainable mess, quite possibly triggering false-positives due to flaky testing infrastructure and code. 94 | 95 | Preceptor tries to solve all of these problems by using a consistent way of configuring the tools, managing the workflow within a single configuration file, and aggregating the test-results and code-coverage reports in one place. In addition, it adds a flexible and extensible plugin infrastructure to the test-runner, introducing even more features like a Selenium / WebDriver integration, injecting client/server code in setup/teardown methods of different testing frameworks, keeping the test-code free of glue-code. 96 | 97 | Preceptor comes with the following features out of the box: 98 | * Test-report aggregation, combining all of the test-results in one place, supporting a big list of commonly used reporters, including display reporters such as “Spec” and “Dot” and file-format reporters such as “JUnit” and “TAP”, all of which can be used at the same time. 99 | * Centralized collection of code-coverage metrics for every test that ran, merging all of the results in one place, giving the developers, testers, and managers a complete picture of what has been tested, and what might need more tests. 100 | * Coordination of tests, queuing them, running them in parallel, or composing them - any way the developer needs or wants them to run. 101 | * Support of multiple commonly used testing frameworks including: Mocha, Cucumber, and Kobold. Support for other frameworks and test-runners like Jasmine and Karma are a WIP. 102 | * Report listeners to parse results from unsupported testing-framework through the usage of commonly used protocols like the TeamCity test-result protocol that is used by the IntelliJ IDEs. 103 | 104 | As mentioned, plugins add even more features, including: 105 | * Selenium test management, starting-up and tearing-down Selenium servers transparently for the developers and testers, injecting code to the test-client, reducing with it the boilerplate code needed to run Selenium tests. This will also help simplify the configuration of the Selenium test system. 106 | * Client-side code-coverage collection, merging the code-coverage reports of an application that ran in a browser. 107 | 108 | ##Getting Started 109 | 110 | Preceptor comes with a command-line tool that takes a configuration file that defines what sequence the task should run, what data needs to be collected, and how some tasks affect others. 111 | 112 | Let's have a look at some examples that uses Mocha as sole testing tool. Preceptor can be used to mix multiple testing frameworks and independent tests at the same time, however, I will only use Mocha for the next examples. 113 | 114 | To get started, create the following file with the name ```config.js```: 115 | ```javascript 116 | module.exports = { 117 | "configuration": { 118 | "reportManager": { 119 | "reporter": [ 120 | { "type": "Spec" }, 121 | { "type": "List", "progress": false } 122 | ] 123 | } 124 | }, 125 | 126 | "tasks": [ 127 | { // Task 128 | "type": "mocha", 129 | "title": "Everything", // Just giving the task a title 130 | "configuration": { 131 | "paths": [__dirname + "/mocha/test1.js"] 132 | } 133 | } 134 | ] 135 | }; 136 | ``` 137 | This configuration adds a Mocha task and two reports: 138 | * ```Spec``` - A detailed console report that is very similar to Mocha's ```spec``` reporter. 139 | * ```List``` - Comprehensive list of all problems encountered which will be printed add the end of all test-runs. 140 | 141 | As SUT, add the following ```lib.js``` file to the same folder: 142 | ```javascript 143 | console.log('Mostly Harmless'); 144 | 145 | module.exports = { 146 | 147 | answerToLifeTheUniverseAndEverything: function () { 148 | return 42; 149 | }, 150 | 151 | whatToDo: function (phrase) { 152 | return (phrase == "don't panic"); 153 | }, 154 | 155 | allThereIs: function () { 156 | return 6 * 9; 157 | }, 158 | 159 | worstFirstMillionYears: function () { 160 | return 10 + 10 + 10; 161 | }, 162 | 163 | message: function () { 164 | console.log("So Long, and Thanks for All the Fish."); 165 | }, 166 | 167 | startInfiniteImprobabilityDriver: function () { 168 | throw new Error("infinitely improbable"); 169 | } 170 | }; 171 | ``` 172 | 173 | The tests live in a sub-folder with the name ```mocha```. For now, let's add a couple of simple tests that give you a glimpse on how test-results will look. 174 | 175 | Copy the following content into ```mocha/test1.js```: 176 | ```javascript 177 | var assert = require('assert'); 178 | var lib = require('../lib'); 179 | 180 | it('should know the answer to life, the universe, and everything', function () { 181 | assert.equal(lib.answerToLifeTheUniverseAndEverything(), 42); 182 | }); 183 | 184 | describe('The End', function () { 185 | 186 | it('should print something', function () { 187 | lib.message(); 188 | }); 189 | }); 190 | ``` 191 | 192 | Preceptor adds a command-line tool that can be executed by calling ```preceptor``` in the shell (when installed globally). Run Preceptor with the above created configuration file: 193 | ```shell 194 | preceptor config.js 195 | ``` 196 | The result should look something like this (colors are not shown): 197 | ```shell 198 | ✓ should know the answer to life, the universe, and everything 199 | 200 | The End 201 | ✓ should print something 202 | ``` 203 | You can see that the output that was created in ```lib.js``` isn't visible anywhere. By default, Preceptor will hide any output created by tests, the SUT, and the testing-framework, displaying only the output of loaded reporters. This behavior can be changed by modifying values in the task-options for each individual tasks. For more information on how to do this, please refer to the API documentation or see below. 204 | 205 | Now, to show some of the features, let's add two more test files: 206 | 207 | **mocha/test2.js** 208 | ```javascript 209 | var assert = require('assert'); 210 | var lib = require('../lib'); 211 | 212 | it('should find all there is', function () { 213 | assert.equal(lib.allThereIs(), 42); 214 | }); 215 | 216 | it('should calculate the worst first million years for marvin', function () { 217 | assert.equal(lib.worstFirstMillionYears(), 30); 218 | }); 219 | ``` 220 | 221 | **mocha/test3.js** 222 | ```javascript 223 | var assert = require('assert'); 224 | var lib = require('../lib'); 225 | 226 | it('should not panic', function () { 227 | assert.ok(lib.whatToDo("don't panic")); 228 | }); 229 | 230 | it('should panic', function () { 231 | assert.ok(!lib.whatToDo("do not panic")); 232 | }); 233 | ``` 234 | 235 | Let's run them all sequentially. Simply add them to the task-list: 236 | ```javascript 237 | module.exports = { 238 | "configuration": { 239 | "reportManager": { 240 | "reporter": [ 241 | { "type": "Spec" }, 242 | { "type": "List", "progress": false } 243 | ] 244 | } 245 | }, 246 | 247 | "tasks": [ 248 | { 249 | "type": "mocha", 250 | "title": "Everything", 251 | "configuration": { 252 | "paths": [__dirname + "/mocha/test1.js"] 253 | } 254 | }, 255 | { 256 | "type": "mocha", 257 | "title": "Calculations", 258 | "configuration": { 259 | "paths": [__dirname + "/mocha/test2.js"] 260 | } 261 | }, 262 | { 263 | "type": "mocha", 264 | "title": "Phrasing", 265 | "configuration": { 266 | "paths": [__dirname + "/mocha/test3.js"] 267 | } 268 | } 269 | ] 270 | }; 271 | ``` 272 | Run Preceptor: 273 | ```shell 274 | ✓ should know the answer to life, the universe, and everything 275 | 276 | The End 277 | ✓ should print something 278 | 1) should find all there is 279 | ✓ should calculate the worst first million years for marvin 280 | ✓ should not panic 281 | ✓ should panic 282 | 283 | 1) Root should find all there is 284 | 54 == 42 285 | AssertionError: 54 == 42 286 | at Context. (.../mocha/test2.js:5:9) 287 | ... // Abbreviated 288 | ``` 289 | 290 | This example shows a failed test, displaying some details and the stack-trace that is abbreviated here in the output for our examples. Let's keep this failing test around for now. 291 | 292 | Great! However, now, the tests appear to run all in the same top-level test-suite - we cannot easily distinguish which tests were run in which task. Let's change this. Add a ```suite:true``` to each task, turning each task into a virtual test-suite that creates a new level of test-suites in the test-results using the ```title``` given. 293 | ```javascript 294 | module.exports = { 295 | "configuration": { 296 | "reportManager": { 297 | "reporter": [ 298 | { "type": "Spec" }, 299 | { "type": "List", "progress": false } 300 | ] 301 | } 302 | }, 303 | 304 | "tasks": [ 305 | { 306 | "type": "mocha", 307 | "title": "Everything", 308 | "suite": true, 309 | "configuration": { 310 | "paths": [__dirname + "/mocha/test1.js"] 311 | } 312 | }, 313 | { 314 | "type": "mocha", 315 | "title": "Calculations", 316 | "suite": true, 317 | "configuration": { 318 | "paths": [__dirname + "/mocha/test2.js"] 319 | } 320 | }, 321 | { 322 | "type": "mocha", 323 | "title": "Phrasing", 324 | "suite": true, 325 | "configuration": { 326 | "paths": [__dirname + "/mocha/test3.js"] 327 | } 328 | } 329 | ] 330 | }; 331 | ``` 332 | The output for this configuration is: 333 | ```shell 334 | Everything 335 | ✓ should know the answer to life, the universe, and everything 336 | 337 | The End 338 | ✓ should print something 339 | 340 | Calculations 341 | 1) should find all there is 342 | ✓ should calculate the worst first million years for marvin 343 | 344 | Phrasing 345 | ✓ should not panic 346 | ✓ should panic 347 | 348 | 1) Calculations should find all there is 349 | 54 == 42 350 | AssertionError: 54 == 42 351 | at Context. (.../mocha/test2.js:5:9) 352 | ... // Abbreviated 353 | ``` 354 | Great! That looks already a lot better. 355 | 356 | But now, I want to run the first tests in parallel, and the last test should run when the first two tests are both completed. Simply wrap the first two tasks in an array literal. This will group the tasks together. 357 | ```javascript 358 | module.exports = { 359 | "configuration": { 360 | "reportManager": { 361 | "reporter": [ 362 | { "type": "Spec" }, 363 | { "type": "List", "progress": false } 364 | ] 365 | } 366 | }, 367 | 368 | "tasks": [ 369 | [{ // <----- 370 | "type": "mocha", 371 | "title": "Everything", 372 | "suite": true, 373 | "configuration": { 374 | "paths": [__dirname + "/mocha/test1.js"] 375 | } 376 | }, 377 | { 378 | "type": "mocha", 379 | "title": "Calculations", 380 | "suite": true, 381 | "configuration": { 382 | "paths": [__dirname + "/mocha/test2.js"] 383 | } 384 | }], // <----- 385 | { 386 | "type": "mocha", 387 | "title": "Phrasing", 388 | "suite": true, 389 | "configuration": { 390 | "paths": [__dirname + "/mocha/test3.js"] 391 | } 392 | } 393 | ] 394 | }; 395 | 396 | ``` 397 | Let's run it again: 398 | ```shell 399 | Everything 400 | 401 | Calculations 402 | ✓ should know the answer to life, the universe, and everything 403 | 404 | The End 405 | ✓ should print something 406 | 1) should find all there is 407 | ✓ should calculate the worst first million years for marvin 408 | 409 | Phrasing 410 | ✓ should not panic 411 | ✓ should panic 412 | 413 | 1) Calculations should find all there is 414 | 54 == 42 415 | AssertionError: 54 == 42 416 | at Context. (.../mocha/test2.js:5:9) 417 | ... // Abbreviated 418 | ``` 419 | You can see that the tests are running in parallel now. However, the ```Spec``` reporter prints the test-results as soon as it receives them from the test-clients, turning the output into a mess. This is the default behavior of most of the console reporters. We can change this behavior by setting the ```progress``` property to ```false```. When this flag is set to ```false```, the reporter will organize all test-results in a tree, printing them in an ordered manner just when Preceptor completes all tests. 420 | 421 | To make it a little bit more interesting, let's add also the ```Duration``` reporter; it will print the total duration. The value of ```progress``` for the ```Duration``` reporter is by default ```false```, so no need to set this explicitly. 422 | ```javascript 423 | module.exports = { 424 | "configuration": { 425 | "reportManager": { 426 | "reporter": [ 427 | { "type": "Spec", "progress": false }, 428 | { "type": "List", "progress": false }, 429 | { "type": "Duration" } 430 | ] 431 | } 432 | }, 433 | 434 | // ... 435 | }; 436 | ``` 437 | The output is now: 438 | ```shell 439 | Everything 440 | ✓ should know the answer to life, the universe, and everything 441 | 442 | The End 443 | ✓ should print something 444 | 445 | Calculations 446 | 1) should find all there is 447 | ✓ should calculate the worst first million years for marvin 448 | 449 | Phrasing 450 | ✓ should not panic 451 | ✓ should panic 452 | 453 | 1) Calculations should find all there is 454 | 54 == 42 455 | AssertionError: 54 == 42 456 | at Context. (.../mocha/test2.js:5:9) 457 | ... // Abbreviated 458 | 459 | Time: 531 milliseconds 460 | ``` 461 | The test-results are printed correctly now. Also, the duration is printed just as we wanted it. 462 | 463 | With Preceptor, you can add as many reporters as you want, all of which are run at the same time - no matter if they are console or file reporters. 464 | 465 | Let's add some file reporters: JUnit and TAP 466 | ```javascript 467 | module.exports = { 468 | "configuration": { 469 | "reportManager": { 470 | "reporter": [ 471 | { "type": "Spec", "progress": false }, 472 | { "type": "List", "progress": false }, 473 | { "type": "Duration" }, 474 | { "type": "Junit", path: "junit.xml" }, 475 | { "type": "Tap", path: "tap.txt" } 476 | ] 477 | } 478 | }, 479 | 480 | // ... 481 | }; 482 | ``` 483 | When running this configuration, two files will be created in the current working directory called ```junit.xml``` and ```tap.txt```, holding JUnit and TAP results respectively. 484 | 485 | Now, I want to collect code-coverage reports from all the tests that were run. For that, add a ```coverage``` section to the global Preceptor configuration, activating it with ```active:true```, and may be supply additional configuration options. 486 | ```javascript 487 | module.exports = { 488 | "configuration": { 489 | "reportManager": { 490 | "reporter": [ 491 | { "type": "Spec", "progress": false }, 492 | { "type": "List", "progress": false }, 493 | { "type": "Duration" }, 494 | { "type": "Junit", path: "junit.xml" }, 495 | { "type": "Tap", path: "tap.txt" } 496 | ] 497 | }, 498 | "coverage": { 499 | "active": true, // Activate coverage 500 | 501 | "path": "./coverage" // This is the default path 502 | 503 | // Add here any other coverage options. 504 | // See below for more information 505 | } 506 | }, 507 | 508 | // ... 509 | }; 510 | ``` 511 | The output is now: 512 | ```shell 513 | Everything 514 | ✓ should know the answer to life, the universe, and everything 515 | 516 | The End 517 | ✓ should print something 518 | 519 | Calculations 520 | 1) should find all there is 521 | ✓ should calculate the worst first million years for marvin 522 | 523 | Phrasing 524 | ✓ should not panic 525 | ✓ should panic 526 | 527 | 1) Calculations should find all there is 528 | 54 == 42 529 | AssertionError: 54 == 42 530 | at Context. (.../mocha/test2.js:5:9) 531 | ... // Abbreviated 532 | 533 | Time: 572 milliseconds 534 | 535 | 536 | =============================== Coverage summary =============================== 537 | Statements : 96.3% ( 26/27 ) 538 | Branches : 100% ( 0/0 ) 539 | Functions : 92.31% ( 12/13 ) 540 | Lines : 96.3% ( 26/27 ) 541 | ================================================================================ 542 | ``` 543 | 544 | By default, the coverage reporter creates three reports: 545 | * ```text-summary``` - The short coverage summary in the console 546 | * ```file``` - JSON file holding all coverage details, created in the ```path``` directory 547 | * ```lcov``` - HTML report in the ```path``` directory 548 | 549 | This concludes the introduction of Preceptor. Please read the general overview that follows, or refer to the API documentation that is included with this module. 550 | 551 | ## Usage 552 | 553 | ###Command-Line usage 554 | 555 | Preceptor is started through the command-line that can be found in the ```bin``` folder. You can run the application with 556 | 557 | ```shell 558 | preceptor [options] [config-file] 559 | ``` 560 | 561 | The ```config-file``` is by default ```rule-book.js``` or ```rule-book.json``` when left-off, where by the first one has priority over the second. 562 | 563 | The command-line tool exposes a couple of flags and parameters: 564 | ``` 565 | --config j Inline JSON configuration or configuration overwrites 566 | --profile p Profile of configuration 567 | --subprofile p Sub-profile of configuration 568 | --version Print version 569 | --help This help 570 | ``` 571 | 572 | For profiles, see the profile section below. 573 | 574 | The ```config``` options adds the possibility to create or overwrite configuration directly from the console. The inline configuration-options are applied after the profile selection. 575 | Objects are merged if a configuration file was selected, and arrays will be appended to already available lists. 576 | 577 | ###Client-API 578 | 579 | Preceptor injects an object (```PRECEPTOR```) into the gloabl scope. 580 | * ```PRECEPTOR.config``` - Global configuration object from the Preceptor configuration 581 | 582 | ####Examples 583 | 584 | The supplied configuration information is very helpful for test customization. 585 | For example, you might want to supply a different base-url for tests run locally or on a CI system, or you would want to customize browser window sizes. 586 | 587 | Here is an example for a Precpetor configuration file: 588 | ```javascript 589 | 590 | module.exports = { 591 | 592 | "configuration": { 593 | "reportManager": { 594 | "reporter": [ { "type": "Spec", "progress": true } ] 595 | }, 596 | "plugins": [ "preceptor-webdriver" ], 597 | 598 | "settings": { // Settings needed by the test-client 599 | "windowWidth": 1280, // Width of browser window 600 | "windowHeight": 768, // Height of browser window 601 | "webBaseUrl": "http://localhost" // Url of website 602 | // May be some other options - add whatever you want here. Preceptor will ignore unknown options. 603 | } 604 | }, 605 | 606 | "tasks": [ 607 | ... 608 | ] 609 | }; 610 | ``` 611 | 612 | In the client code then, you can gather this information like this: 613 | ```javascript 614 | var settings = PRECEPTOR.config.settings; 615 | var activeWindow = browser.activeWindow(); 616 | 617 | activeWindow.resize(settings.windowWidth, settings.windowHeight); 618 | activeWindow.navigator.navigateTo(settings.webBaseUrl + '/order/324'; 619 | ``` 620 | 621 | ###Testing Lifecycle 622 | To understand how Preceptor works, we have to understand first what the testing lifecycle is. Tests are run usually through a common set of states that can be defined as the testing lifecycle. 623 | 624 | This include: 625 | * start of all tests (not all frameworks support this) 626 | * start of test suites 627 | * start of tests 628 | * end of tests (with different types of results) 629 | * end of test suites 630 | * end of all tests (not all frameworks support this) 631 | 632 | Each individual state will be triggered in Preceptor so that plugins can react on these state changes. This includes the reporter, the client, the client decorators, the tasks, and Preceptor itself. 633 | 634 | Preceptor breaks them (and some additional states) into 4 groups: 635 | * Admin - Administrative states 636 | * Suite - Suite management 637 | * Test - Test management 638 | * Item - Item management for parts of a test 639 | 640 | Let's describe them in more detail by describing their function and what information in conveyed. 641 | 642 | 643 | ####Administrative states 644 | Administrative states will be triggered for events that change the collection and run behavior of Preceptor itself. 645 | 646 | 647 | #####State: Start 648 | The ```start``` state will be triggered just when Preceptor starts to run. 649 | 650 | Parameters: 651 | * __none__ 652 | 653 | 654 | #####State: Stop 655 | The ```stop``` state will be triggered just when Preceptor completes all tests. 656 | 657 | Parameters: 658 | * __none__ 659 | 660 | 661 | #####State: Complete 662 | The ```complete``` state will be triggered when Preceptor completed all the export jobs. 663 | 664 | Parameters: 665 | * __none__ 666 | 667 | 668 | ####Suite management 669 | These states handle any changes to test-suites. 670 | 671 | 672 | #####State: Suite Start 673 | The ```suiteStart``` state will be triggered when a new test-suite starts. Please be aware that it is possible to have test-suites within other test-suites. 674 | 675 | Parameters: 676 | * ```id``` - Identifier of test-suite. Preceptor creates a unique identifier for each state to be able to identifier related state changes. 677 | * ```parentId``` - Parent identifier of parent test-suite. This makes it possible for Preceptor to create a tree of tests and test-suites. For the first test-suite, this will be the id of the ```start``` event. 678 | * ```suiteName``` - Name of the suite that should be used for reporting. 679 | 680 | 681 | #####State: Suite End 682 | The ```suiteEnd``` state will be triggered when a test-suite completed. 683 | 684 | Parameters: 685 | * ```id``` - Identifier of test-suite, the same identifier that was given to ```suiteStart```. 686 | 687 | 688 | 689 | ####Test management 690 | Test management states are states that transmit test and test-result information. 691 | 692 | #####State: Test Start 693 | The ```testStart``` state will be triggered when a new test starts. 694 | 695 | Parameters: 696 | * ```id``` - Identifier of test. Preceptor creates a unique identifier for each state to be able to identifier related state changes. 697 | * ```parentId``` - Parent identifier of parent test-suite. This makes it possible for Preceptor to create a tree of tests and test-suites. For the first test without a parent test-suite, this will be the id of the ```start``` event. 698 | * ```testName``` - Name of the test that should be used for reporting. 699 | 700 | 701 | #####State: Test Passed 702 | The ```testPassed``` state will be triggered when a test has passed. 703 | 704 | Parameters: 705 | * ```id``` - Identifier of test that was given to the related ```testStart```. 706 | 707 | 708 | #####State: Test Failed 709 | The ```testFailed``` state will be triggered when a test has failed. A failure is an unexpected test-result, often triggered by assertions. 710 | 711 | Parameters: 712 | * ```id``` - Identifier of test that was given to the related ```testStart```. 713 | * ```message``` - Short description of the failure 714 | * ```reason``` - Detailed description of the failure (often with stack-trace) 715 | 716 | 717 | #####State: Test Error 718 | The ```testError``` state will be triggered when a test had an error. An error is an unexpected exception that was not triggered by an assertion. 719 | 720 | Parameters: 721 | * ```id``` - Identifier of test that was given to the related ```testStart```. 722 | * ```message``` - Short description of the error 723 | * ```reason``` - Detailed description of the error (often with stack-trace) 724 | 725 | 726 | #####State: Test Undefined 727 | The ```testUndefined``` state will be triggered when a test hasn't been defined. This is triggered when the testing-framework recognizes a test, but could not find the test-implementation. Not all testing frameworks support this state. 728 | 729 | Parameters: 730 | * ```id``` - Identifier of test that was given to the related ```testStart```. 731 | 732 | 733 | #####State: Test Skipped 734 | The ```testSkipped``` state will be triggered when a test has been skipped. Tests are usually skipped when for example sub-systems that are dependencies for test are not available. 735 | 736 | Parameters: 737 | * ```id``` - Identifier of test that was given to the related ```testStart```. 738 | * ```reason``` - Reason for skipping this test. 739 | 740 | 741 | #####State: Test Incomplete 742 | The ```testIncomplete``` state will be triggered when a test is available, but incomplete. This happens when tests are written, but are not completed yet. 743 | 744 | Parameters: 745 | * ```id``` - Identifier of test that was given to the related ```testStart```. 746 | 747 | 748 | ###Configuration 749 | 750 | The configuration file has three top-level properties: 751 | * ```configuration``` - Global Preceptor configuration that describes Preceptor behavior. 752 | * ```shared``` - Shared task options. 753 | * ```tasks``` - Individual task options which are run by default in sequence. 754 | 755 | ####Profiles 756 | The configuration file supports multiple layers of profiles: 757 | * Global Profile 758 | * Tasks Profile 759 | 760 | As an example, let's use the following abbreviated configuration: 761 | 762 | __config.js__ 763 | ```javascript 764 | module.exports = { 765 | 766 | "configuration": { 767 | "reportManager": { "reporter": [ { "type": "Spec" } ] } 768 | }, 769 | 770 | "tasks": [ 771 | { 772 | "type": "mocha", 773 | "configuration": { 774 | "paths": [__dirname + "/mocha/test1.js"] 775 | } 776 | } 777 | ] 778 | }; 779 | ``` 780 | 781 | #####Global Profile 782 | Global profiles can be used to toggle between full-configurations: 783 | 784 | ```javascript 785 | module.exports = { 786 | 787 | "profile1": { 788 | "configuration": { 789 | "reportManager": { "reporter": [ { "type": "Spec" } ] } 790 | }, 791 | 792 | "tasks": [ 793 | { 794 | "type": "mocha", 795 | "configuration": { 796 | "paths": [__dirname + "/mocha/test1.js"] 797 | } 798 | } 799 | ] 800 | }, 801 | 802 | "profile2": { 803 | "configuration": { 804 | "reportManager": { "reporter": [ { "type": "Dot" } ] } 805 | }, 806 | 807 | "tasks": [ 808 | { 809 | "type": "mocha", 810 | "configuration": { 811 | "paths": [__dirname + "/mocha/test2.js"] 812 | } 813 | } 814 | ] 815 | } 816 | }; 817 | ``` 818 | 819 | To select the second profile, call Preceptor as follows: 820 | ```shell 821 | preceptor --profile profile2 config.js 822 | ``` 823 | 824 | The first profile can be selected by supplying ```profile1``` instead, or whatever name you give the corresponding profiles. 825 | Be sure to always supply a profile when it is available since the default behavior is to not look for a profile. 826 | 827 | #####Tasks Profile 828 | Task profiles are very similar to global profiles, but are there for toggling task-lists. This can be very useful, when global configurations are shared between multiple profiles. Task profiles are also called sub-profiles since they toggle configuration below the global profile selection. 829 | 830 | An example looks like this: 831 | ```javascript 832 | module.exports = { 833 | 834 | "configuration": { 835 | "reportManager": { "reporter": [ { "type": "Spec" } ] } 836 | }, 837 | 838 | "tasks": { 839 | "sub-profile1": [ 840 | { 841 | "type": "mocha", 842 | "configuration": { 843 | "paths": [__dirname + "/mocha/test1.js"] 844 | } 845 | } 846 | ], 847 | "sub-profile2": [ 848 | { 849 | "type": "mocha", 850 | "configuration": { 851 | "paths": [__dirname + "/mocha/test2.js"] 852 | } 853 | } 854 | ] 855 | } 856 | }; 857 | ``` 858 | 859 | To select the first sub-profile, call Preceptor as follows: 860 | ```shell 861 | preceptor --subprofile sub-profile1 config.js 862 | ``` 863 | 864 | Be sure to always supply a sub-profile when it is available since the default behavior is to not look for a sub-profile. 865 | 866 | #####Combine Profiles 867 | It is also possible to combine both profile methods. 868 | 869 | ```javascript 870 | module.exports = { 871 | 872 | "ci": { 873 | "configuration": { 874 | "reportManager": { "reporter": [ { "type": "Dot" } ] } 875 | }, 876 | 877 | "tasks": { 878 | "acceptance": [ 879 | { 880 | "type": "mocha", 881 | "configuration": { 882 | "paths": [__dirname + "/build/mocha/test1.js"] 883 | } 884 | } 885 | ], 886 | "integration": [ 887 | { 888 | "type": "mocha", 889 | "configuration": { 890 | "paths": [__dirname + "/build/mocha/test2.js"] 891 | } 892 | } 893 | ] 894 | } 895 | }, 896 | "dev": { 897 | "configuration": { 898 | "reportManager": { "reporter": [ { "type": "Spec" } ] } 899 | }, 900 | 901 | "tasks": { 902 | "acceptance": [ 903 | { 904 | "type": "mocha", 905 | "configuration": { 906 | "paths": [__dirname + "/../mocha/test1.js"] 907 | } 908 | } 909 | ], 910 | "integration": [ 911 | { 912 | "type": "mocha", 913 | "configuration": { 914 | "paths": [__dirname + "/../mocha/test2.js"] 915 | } 916 | } 917 | ] 918 | } 919 | } 920 | }; 921 | ``` 922 | 923 | With this example, you could run the acceptance tests from your local machine with: 924 | ```shell 925 | preceptor --profile dev --subprofile acceptance config.js 926 | ``` 927 | 928 | ####Global configuration 929 | Preceptors task-independent behavior is configured in this section. It has the following properties: 930 | 931 | * ```verbose``` - Flag for verbose output (default: false) 932 | * ```reportManager``` - Reporting configuration 933 | * ```reportManager.reporter``` - List of loaded reporters 934 | * ```reportManager.listener``` - List of loaded listeners 935 | * ```coverage``` - Coverage option 936 | * ```plugins``` - List of plugin modules to load 937 | * ```ignoreErrors``` - When set, errors that occur during testing will not be triggered on the Preceptor process (default: false) 938 | 939 | ######Reporting 940 | The report manager describes what reporters should be used and what it should listen for to receive testing lifecycle events from unsupported clients. 941 | See the ```preceptor-reporter``` project documentation for more information on the ```reportManager``` property. 942 | 943 | ######Coverage 944 | Coverage report collection can be configured in this section. It has the following options: 945 | * ```active``` - Flag that turns coverage collection on or off (default: false - off) 946 | * ```root``` - Root of coverage data. This is used to reduce depth of coverage paths. 947 | * ```path``` - Path to where the file coverage reports should be exported to. 948 | * ```reports``` - List of reports to export to. (default: ['file', 'lcov', 'text-summary']) 949 | * ```includes``` - List of patterns for files to include scripts into the coverage report. (default: ['**/*.js']) 950 | * ```excludes``` - List of patterns to exclude files and directories of scripts that were previously whitelisted with the ```includes``` option. (default: ['**/node_modules/**', '**/test/**', '**/tests/**']) 951 | 952 | This feature uses Istanbul for collecting and merging coverage reports. See the Istanbul project [website](https://github.com/gotwarlost/istanbul) for more information. 953 | 954 | __Additional Reports__ 955 | Preceptor adds the ```file``` report to the list of reports to be able to disable the export of the JSON data. The ```file``` value will not be given to Istanbul since it is not available there. 956 | 957 | ####Shared configuration 958 | Any value that is assigned to the 'shared' object in the configuration root will be assigned as default for all task options. Task options then can overwrite these values. This gives a developer the opportunity to set own default values for properties, overwriting the default values that were given by the system. 959 | 960 | ######Plugins 961 | The Preceptor system supports the loading of custom plugins by installing the modules through NPM, and by adding the module name to this list. Preceptor then tries to load each of these modules by executing the ```loader``` function on the exported module interface, giving it the instance of Preceptor itself. The plugin can then register itself to Preceptor. See the ```preceptor-webdriver``` project for an example. 962 | 963 | ###Tasks 964 | 965 | The following tasks for testing-frameworks are supported by default: 966 | * Cucumber - Cucumber.js testing framework 967 | * Mocha - Mocha unit testing framework 968 | * Kobold - Kobold visual regression testing framework 969 | 970 | Preceptor implements also some general purpose tasks: 971 | * Node - Running node scripts 972 | * Shell - Running shell scripts 973 | * Group - Grouping tasks 974 | 975 | The following sections describe the configuration and usage of these tasks 976 | 977 | ####General Configuration 978 | Tasks have a common set of configuration options that can be set on the root of the task. Any custom configuration option, that will be listed for each task below, should be set on the ```configuration``` object instead of the root (see "Object and List Configuration"). The reason for this separation is flexibility for new features in future version of Preceptor without breaking any task or custom task that might add similar named options. 979 | 980 | #####Value Configuration 981 | * ```taskId``` - Identifier of task. If none is given, then Preceptor will assign an identifier and may use it in error reports. 982 | * ```type``` - Identifier for task-plugin. For example, ```type:'mocha'``` will use the Mocha task-plugin. 983 | * ```name``` - Name of task. This is a more user friendly version of ```taskId```. If none is given, then it uses the ```taskId``` value. 984 | * ```title``` - Description of task that might be used in test-results. 985 | 986 | #####Object and List Configuration 987 | * ```configuration``` - Custom configuration for task-specific options. See below for the task options. 988 | * ```decorators``` - List of client decorators that should be loaded on the client. See "Client Decorators" section for more information. 989 | 990 | #####Flag Configuration 991 | * ```active``` - Flag that defines if task is active or inactive. Inactive tasks will be ignored by Preceptor, skipping any tests and results from the task. (default: true) 992 | * ```suite``` - Flag that defines if task should be used as a virtual test-suite. A virtual test-suite injects itself into the test-results. This makes it possible to compose the test-results however it is needed. (default: false) 993 | * ```debug``` - Flag that defines if task is run in debug mode. In debug-mode, the client is run directly in the Preceptor process. Output is not caught by Preceptor and directly printed to std-out, and a breakpoint will stop the Preceptor process. Avoid using this flag long-term. (default: false) 994 | * ```report``` - Flag that determines if task uses the globally configured reporter. (default: true) If deactivated, testing life-cycle events will be muted. 995 | * ```coverage``` - Flag that determines if task uses the globally configured coverage collector. (default: false) If deactivated, collected coverage won't be merged 996 | * ```verbose``` - Prints every step taken by Preceptor. This adds a lot of output and might be overwhelming at first. (default: false) 997 | * ```failOnError``` - Flag that defines if task should skip all other tests and fail Preceptor. (default: false) 998 | * ```echoStdOut``` - Flag that defines if task should echo all std-out data. 999 | * ```echoStdErr``` - Flag that defines if task should echo all std-err data. 1000 | 1001 | 1002 | ####Cucumber Task 1003 | 1004 | The ```type```-value for this task is ```cucumber```. 1005 | 1006 | Task options: 1007 | * ```path``` - Path to the tests (required) 1008 | * ```tags``` - List of tags to accept. These can be strings with one tag, or an array of tags. These tags should have an '@' and may be the '~' if required. See Cucumber.js documentation for more information. 1009 | * ```format``` - Output format for test results (default: 'progress') 1010 | * ```functions``` - A list of functions to import (still WIP). The function can have a ```name``` property to be used in verbose logging. 1011 | * ```coffeScript``` - Flag that defines that output should be in coffee-script. 1012 | 1013 | 1014 | ####Mocha Task 1015 | 1016 | The ```type```-value for this task is ```mocha```. 1017 | 1018 | Task options: 1019 | * ```reporters``` - Reporter to use (default: 'spec') 1020 | * ```ui``` - API interface to use (default: 'bdd') 1021 | * ```colors``` - Use colors in output (default: true) 1022 | * ```inlineDiff``` - Inline-diff support (default: false) 1023 | * ```slow``` - Defines how many milliseconds is considered as a slow test (default: 75) 1024 | * ```timeOuts``` - Specifies if time-outs should be enforced (default: true) 1025 | * ```timeOut``` - Describes when a test is considered to be in time-out (in milliseconds) (default: 2000) 1026 | * ```bail``` - Stop execution on the first error (default: false) 1027 | * ```grep``` - Filter to use for tests (default: false - off) 1028 | * ```invert``` - Invert ```grep``` filter (default: false) 1029 | * ```checkLeaks``` - Checks for memory leaks (default: false) 1030 | * ```asyncOnly``` - Enforce all tests to be asynchronous (default: false) 1031 | * ```globals``` - List of defined globals 1032 | * ```paths``` - A list of paths to the tests (default: ['test']) 1033 | * ```functions``` - A list of functions to import as tests. The function can have a ```name``` property to be used in verbose logging. 1034 | * ```recursive``` - Recursive search in the test paths (default: false) 1035 | * ```require``` - List of files to be require'd before tests run. 1036 | * ```sort``` - Sort tests before running the tests (default: false) 1037 | 1038 | 1039 | ####Kobold Task 1040 | 1041 | The ```type```-value for this task is ```kobold```. 1042 | 1043 | Task options: 1044 | * ```verbose``` - Verbose output (default: true) 1045 | * ```failForOrphans``` - Flag that defines if Kobold should fail tests when Orphans are encountered. Orphans are screens that were previously approved, but are missing in the most recent test-run. This can be the case when tests are removed, or when the testing-framework was interrupted. (default: true) 1046 | * ```failOnAdditions``` - Flag that defines if Kobold should fail tests when Additions are encountered. Additions are screens that have never been seen (approved) before, but that are available during the most recent test-run. These usually happen when tests are added; these screens should be reviewed before approving. (default: true) 1047 | * ```build``` - Identifier for the current build. This can be an identifier from a CI or a random generated one. This value is used to distinguish multiple test-runs from each-other, and is given to the remote storage. (default: process.env.BUILD_NUMBER or process.env.USER + ) 1048 | * ```blinkDiff``` - Configuration for the blink-diff tool. 1049 | * ```mocha``` - Mocha options for Kobold tests since it is build on-top of Mocha. (See above) 1050 | * ```storage``` - Storage configuration for Kobold. See the project website for more information. 1051 | * ```source``` - Source of screens. Uses ```storage``` as default. See the project website for more information. 1052 | * ```destination``` - Destination for screens. Uses ```storage``` as default. See the project website for more information. 1053 | 1054 | The storage configuration has also the following values by default: 1055 | * ```type``` - 'File' 1056 | * ```options.approvedFolderName``` - 'approved' 1057 | * ```options.buildFolderName``` - 'build' 1058 | * ```options.highlightFolderName``` - 'highlight' 1059 | 1060 | 1061 | ####Loader Task 1062 | 1063 | This task imports already available test-report files, including JUnit and TAP files. The ```type```-value for this task is ```loader```. 1064 | 1065 | Task options: 1066 | * ```format``` - Format of file-import (i.e. ```junit```, ```tap``` or ```istanbul```). See the [Preceptor-Reporter](http://yahoo.github.io/preceptor-reporter/#loader) project for all available options. 1067 | * ```path``` - Glob to select files that should be imported 1068 | * ```configuration``` - Custom configuration for each file-format type 1069 | 1070 | __Example:__ 1071 | ```javascript 1072 | { 1073 | "type": "loader", 1074 | "title": "JUnit import", 1075 | "suite": true, // Wrap it in a suite using the title from above 1076 | 1077 | "configuration": { // Loader-task specific configuration 1078 | 1079 | "format": "junit", // Use "junit" as the format (is default) 1080 | "path": "junit-*.xml", // Glob to select import files 1081 | 1082 | "configuration": { 1083 | // Custom JUnit loader configuration 1084 | "topLevel": false 1085 | } 1086 | } 1087 | } 1088 | ``` 1089 | 1090 | ####Node Task 1091 | 1092 | The ```type```-value for this task is ```node```. 1093 | 1094 | Task options: 1095 | * ```path``` - Path to JavaScript file that should be executed. The JavaScript file will be running in its own process. 1096 | 1097 | 1098 | ####Shell Task 1099 | 1100 | The ```type```-value for this task is ```shell```. 1101 | 1102 | Task options: 1103 | * ```cwd``` - Defines the current working directory for the script. 1104 | * ```env``` - List of key-value pairs (in an object) of environment variables set for the shell script. (default: {}) 1105 | * ```cmd``` - Command to be executed in shell. The command will be run in its own process. 1106 | 1107 | 1108 | ####Group Task 1109 | 1110 | The ```type```-value for this task is ```group```. 1111 | 1112 | Task options: 1113 | * ```parallel``` - Flag that defines if tasks should be run in parallel (default: false) 1114 | * ```tasks``` - List of task-options that should be run within this group. 1115 | 1116 | The ```group``` Task Decorator uses this task to add the array-literal feature to the configuration. 1117 | 1118 | 1119 | ###Task Decorators 1120 | 1121 | A task decorator is a plugin that can modify task-options before they are applied. This makes the configuration very flexible as the whole configuration scheme can be changed with these plugins. 1122 | 1123 | The following task decorators are build-in: 1124 | * ```group``` - Adds the array-literal feature for running tasks in parallel. It replaces the array-literal with a group that has the ```parallel``` flag set, injecting all the tasks into the ```task``` options of the group-task. 1125 | * ```identifier``` - Makes sure that identifiers are given for ```taskId```, ```name```, and ```title```. ```taskId``` will receive an unique id if not given, and all others might inherit the value if they are also missing - ```name``` from ```taskId``` and ```title``` from ```name```. 1126 | 1127 | 1128 | ###Client Decorators 1129 | 1130 | Client decorators are "tasks" that will be run in the client process by the client-runner. The decorators attach themselves to test-hooks that are triggered during the testing lifecycle (see above). 1131 | 1132 | There are two types of hooks: 1133 | * Event hooks - Events that trigger without the possibility to halt the testing framework. Event hooks are triggered for all events during the testing lifecycle (see above). 1134 | * Activity hooks - Methods that are triggered that should return a Promise. This gives the client decorator the possibility to halt the testing framework for a while to execute some other tasks first. 1135 | 1136 | There are four activity hooks: 1137 | * ```processBefore``` - Triggered once before a suite of tests. 1138 | * ```processBeforeEach``` - Triggered before each test-case. 1139 | * ```processAfterEach``` - Triggered after each test-case. 1140 | * ```processAfter``` - Triggered once after a suite of tests. 1141 | 1142 | Out of the box, Preceptor supports the following client decorators: 1143 | * ```plain``` - Prints each of the triggers to the console. This client-decorator should be used for debugging purposes when creating client-decorators. 1144 | 1145 | The ```preceptor-webdriver``` plugin adds the WebDriver client-decorator for injecting Selenium setup/tear-down code into the client. 1146 | 1147 | Client decorators can be added to every task that supports the testing lifecycle events (generally all testing frameworks), and it is added through the ```decorators``` list in the task-options. 1148 | 1149 | ####Configuration 1150 | Client decorators has the following common options: 1151 | * ```type``` - Name of the plugin for client decorators 1152 | * ```configuration``` - Client decorator specific configuration 1153 | 1154 | The general client decoration configuration namespace is reserved for future changes to all client decorators. Client decorator specific configuration should be described in ```configuration```. 1155 | 1156 | 1157 | ###Client Runner 1158 | Clients are generally run in its own process. Each of these processes communicates with Preceptor, making it also possible to instruct the client to run client decorators in its process. For this to work, each testing-framework needs a client-runner. This is abstracted away, but is required when writing a plugin. The following client-runners are available: 1159 | * ```cucumber``` - Runs Cucumber.js 1160 | * ```mocha``` - Runs Mocha 1161 | * ```kobold``` - Runs Kobold. Uses internally the ```mocha``` client-runner. 1162 | * ```node``` - Runs Node.JS scripts 1163 | 1164 | Please see the above files in the source-code for details on how to implement these. Much of the common behavior and the communication is implemented in the ```client.js``` file. 1165 | 1166 | 1167 | ###Plugin Naming 1168 | The plugin module naming should follow the Grunt plugin naming convention. Plugins contributed by the community should have "contrib" in the module name (example: ```preceptor-contrib-hello-world```). Plugins that are supported by the Preceptor team will be named without the "contrib" keyword (example: ```preceptor-webdriver```). 1169 | 1170 | 1171 | ##API-Documentation 1172 | 1173 | Generate the documentation with following command: 1174 | ```shell 1175 | npm run docs 1176 | ``` 1177 | The documentation will be generated in the ```docs``` folder of the module root. 1178 | 1179 | ##Tests 1180 | 1181 | Run the tests with the following command: 1182 | ```shell 1183 | npm run test 1184 | ``` 1185 | The code-coverage will be written to the ```coverage``` folder in the module root. 1186 | 1187 | ##Third-party libraries 1188 | 1189 | The following third-party libraries are used by this module: 1190 | 1191 | ###Dependencies 1192 | * glob: https://github.com/isaacs/node-glob 1193 | * istanbul: https://github.com/gotwarlost/istanbul 1194 | * log4js: https://github.com/nomiddlename/log4js-node 1195 | * minimatch: https://github.com/isaacs/minimatch 1196 | * mkdirp: https://github.com/substack/node-mkdirp 1197 | * preceptor-core: https://github.com/yahoo/preceptor-core 1198 | * preceptor-reporter: https://github.com/yahoo/preceptor-reporter 1199 | * promise: https://github.com/then/promise 1200 | * underscore: http://underscorejs.org 1201 | * uuid: https://github.com/shtylman/node-uuid 1202 | 1203 | ###Dev-Dependencies 1204 | * chai: http://chaijs.com 1205 | * cucumber: http://github.com/cucumber/cucumber-js 1206 | * mocha: https://github.com/visionmedia/mocha 1207 | * cabbie: https://github.com/ForbesLindesay/cabbie 1208 | * kobold: https://github.com/yahoo/kobold 1209 | * preceptor-webdriver: https://github.com/yahoo/preceptor-webdriver 1210 | * yuidocjs: https://github.com/yui/yuidoc 1211 | 1212 | ##License 1213 | 1214 | The MIT License 1215 | 1216 | Copyright 2014 Yahoo Inc. 1217 | --------------------------------------------------------------------------------