From 77393d08343ef16ddc2b8042e187c9d68fe7bf2f Mon Sep 17 00:00:00 2001 From: Walker Date: Thu, 6 Feb 2014 17:32:36 -0600 Subject: [PATCH] feat(runner/launcher): major runner updates to allow multiple capabilities Adding simultaneous runner capability (grid-style), refactoring launch/runner init system, and abstracting out configParser module. --- lib/cli.js | 39 +-- lib/configParser.js | 239 ++++++++++++++++ lib/driverProviders/chrome.dp.js | 64 +++++ lib/driverProviders/hosted.dp.js | 64 +++++ lib/driverProviders/local.dp.js | 114 ++++++++ lib/driverProviders/sauce.dp.js | 93 +++++++ lib/launcher.js | 117 ++++++++ lib/runner.js | 458 ++----------------------------- lib/testRunner.js | 456 ++++++++++++++++++++++++++++++ package.json | 3 +- 10 files changed, 1172 insertions(+), 475 deletions(-) create mode 100644 lib/configParser.js create mode 100644 lib/driverProviders/chrome.dp.js create mode 100644 lib/driverProviders/hosted.dp.js create mode 100644 lib/driverProviders/local.dp.js create mode 100644 lib/driverProviders/sauce.dp.js create mode 100644 lib/launcher.js create mode 100644 lib/testRunner.js diff --git a/lib/cli.js b/lib/cli.js index 8cf9dc439..54a88d2bc 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -24,7 +24,7 @@ try { var util = require('util'); var path = require('path'); -var runner = require('./runner.js'); +var child = require('child_process'); var argv = require('optimist'). usage('Usage: protractor [options] [configFile]\n' + 'The [options] object will override values from the config file.\n' + @@ -67,22 +67,6 @@ if (argv.version) { process.exit(0); } - -// Any file names should be resolved relative to the current working directory. -var processFilePatterns = function(list) { - var patterns = list.split(','); - patterns.forEach(function(spec, index, arr) { - arr[index] = path.resolve(process.cwd(), spec); - }); - return patterns; -} -if (argv.specs) { - argv.specs = processFilePatterns(argv.specs); -} -if (argv.exclude) { - argv.exclude = processFilePatterns(argv.exclude); -} - // WebDriver capabilities properties require dot notation. var flattenObject = function(obj) { var prefix = arguments[1] || ''; @@ -93,26 +77,11 @@ var flattenObject = function(obj) { } } return out; -} +}; if (argv.capabilities) { argv.capabilities = flattenObject(argv.capabilities); } -['seleniumServerJar', 'chromeDriver', 'onPrepare'].forEach(function(name) { - if (argv[name]) { - argv[name] = path.resolve(process.cwd(), argv[name]); - } -}); - -var configFilename = argv._[0]; -if (configFilename) { - var configPath = path.resolve(process.cwd(), configFilename); - var config = require(configPath).config; - config.configDir = path.dirname(configPath); - runner.addConfig(config); -} - -runner.addConfig(argv); - -runner.runOnce(); +// Run the launcher +require('./launcher').init(argv); diff --git a/lib/configParser.js b/lib/configParser.js new file mode 100644 index 000000000..a638d675e --- /dev/null +++ b/lib/configParser.js @@ -0,0 +1,239 @@ +var path = require('path'), + glob = require('glob'), + config_ = { + configDir: './', + jasmineNodeOpts: {} + }, + //this allows for ease of maintaining public apis of the config while still + // allowing for easy variable renames or future config api changes while + // supported backwards compatibility + configMap_ = { + + //structure is internal name -> supported config apis + 'specs': ['specs'], + 'exclude': ['exclude'], + 'capabilities': ['capabilities'], + 'seleniumHost': ['seleniumAddress'], + 'rootElement': ['rootElement'], + 'baseUrl': ['baseUrl'], + 'timeout': ['allScriptsTimeout'], + 'browserParams': ['params'], + 'framework': ['framework'], + 'jasmineOpts': ['jasmineNodeOpts'], + 'mochaOpts': ['mochaOpts'], + 'seleniumLocal.jar': ['seleniumServerJar'], + 'seleniumLocal.args': ['seleniumArgs'], + 'seleniumLocal.port': ['seleniumPort'], + 'sauceAccount.user': ['sauceUser'], + 'sauceAccount.key': ['sauceKey'], + 'chromeDriver': ['chromeDriver'], + 'chromeOnly': ['chromeOnly'], + 'configDir': ['configDir'], + 'cucumberOpts.require': ['cucumberOpts.require'], + 'cucumberOpts.format': ['cucumberOpts.format'], + 'cucumberOpts.tags': ['cucumberOpts.tags'] + }; + +/** + * Merge config objects together. + * + * @private + * @param {Object} into + * @param {Object} from + * + * @return {Object} The 'into' config. + */ +var merge_ = function(into, from) { + for (var key in from) { + if (into[key] instanceof Object && !(into[key] instanceof Array)) { + merge_(into[key], from[key]); + } else { + into[key] = from[key]; + } + } + return into; +}; + +/** + * Resolve a list of file patterns into a list of individual file paths. + * + * @param {Array/String} patterns + * @param {Boolean} opt_omitWarnings whether to omit did not match warnings + * + * @return {Array} The resolved file paths. + */ +var resolveFilePatterns = function(patterns, opt_omitWarnings) { + var resolvedFiles = []; + + patterns = (typeof patterns === 'string') ? + [patterns] : patterns; + + if (patterns) { + for (var i = 0; i < patterns.length; ++i) { + var matches = glob.sync(patterns[i], {cwd: config_.configDir}); + if (!matches.length && !opt_omitWarnings) { + util.puts('Warning: pattern ' + patterns[i] + ' did not match any files.'); + } + for (var j = 0; j < matches.length; ++j) { + resolvedFiles.push(path.resolve(config_.configDir, matches[j])); + } + } + } + return resolvedFiles; +}; + +/** + * Helper to resolve file pattern strings relative to the cwd + * + * @private + * @param {Array} list + */ +var processFilePatterns_ = function(list) { + var patterns = list.split(','); + patterns.forEach(function(spec, index, arr) { + arr[index] = path.resolve(process.cwd(), spec); + }); + return patterns; +}; + +/** + * Add the options in the parameter config to this runner instance. + * + * @private + * @param {Object} additionalConfig + */ +var addConfig_ = function(additionalConfig) { + // All filepaths should be kept relative to the current config location. + // This will not affect absolute paths. + ['seleniumServerJar', 'chromeDriver', 'onPrepare'].forEach(function(name) { + if (additionalConfig[name] && additionalConfig.configDir && + typeof additionalConfig[name] === 'string') { + additionalConfig[name] = + path.resolve(additionalConfig.configDir, additionalConfig[name]); + } + }); + + // Make sure they're not trying to add in deprecated config vals + if (additionalConfig.jasmineNodeOpts && + additionalConfig.jasmineNodeOpts.specFolders) { + throw new Error('Using config.jasmineNodeOpts.specFolders is deprecated ' + + 'since Protractor 0.6.0. Please switch to config.specs.'); + } + merge_(config_,additionalConfig); +}; + + +/** + * Merges in passed in configuration data with existing class defaults + * @public + * @param {Object} config - A set of properties collected that will be merged + * with AbstractTestRunner defaults + */ +var loadConfig = function(configObj, configToLoad) { + + if (!configToLoad || !configObj) { + return; + } + + /* helper to set the correct value for string dot notation */ + function setConfig_(obj, str, val) { + str = str.split('.'); + while (str.length > 1) { + obj = obj[str.shift()]; + } + obj[str.shift()] = val; + } + + /* helper to retrieve the correct value for string dot notation */ + function getConfig_(obj, str) { + var arr = str.split("."); + while(arr.length && (obj = obj[arr.shift()])); + return obj; + } + + /* helper to determine whether a config value is empty based on type */ + function isEmpty_(val) { + return ( val !== null && + val !== '' && + val !== undefined && + !(val instanceof Array && + !val.length) && + !(val instanceof Object && + !Object.keys(val).length) + ); + } + + //object definition driven merging + var key,configDef,configAlias,i; + for (key in configMap_) { + + configDef = configMap_[key]; + for (i=0; i} A promise which resolves to - * the value of the selenium address that will be used. - */ -var setUpSelenium = function() { - // TODO: This should not be tied to the webdriver promise loop, it should use - // another promise system instead. - var deferred = webdriver.promise.defer(); - - if (config.sauceUser && config.sauceKey) { - sauceAccount = new SauceLabs({ - username: config.sauceUser, - password: config.sauceKey - }); + if (config.onCleanup) { + testRunner.registerTestCleaners(config.onCleanUp); } - var defaultChromedriver; - if (config.chromeDriver) { - if (!fs.existsSync(config.chromeDriver)) { - if (fs.existsSync(config.chromeDriver + '.exe')) { - config.chromeDriver += '.exe'; - } else { - throw 'Could not find chromedriver at ' + config.chromeDriver; - } - } - } else { - defaultChromedriver = path.resolve( - __dirname, - '../selenium/chromedriver'); - if (fs.existsSync(defaultChromedriver)) { - config.chromeDriver = defaultChromedriver; - } else if (fs.existsSync(defaultChromedriver + '.exe')) { - config.chromeDriver = defaultChromedriver + '.exe'; - } - } - - // Priority - // 1) if chromeOnly, use that - // 2) if seleniumAddress is given, use that - // 3) if a sauceAccount is given, use that. - // 4) if a seleniumServerJar is specified, use that - // 5) try to find the seleniumServerJar in protractor/selenium - if (config.chromeOnly) { - util.puts('Using ChromeDriver directly...'); - deferred.fulfill(null); - } else if (config.seleniumAddress) { - util.puts('Using the selenium server at ' + config.seleniumAddress); - deferred.fulfill(config.seleniumAddress); - } else if (sauceAccount) { - config.capabilities.username = config.sauceUser; - config.capabilities.accessKey = config.sauceKey; - if (!config.jasmineNodeOpts.defaultTimeoutInterval) { - config.jasmineNodeOpts.defaultTimeoutInterval = 30 * 1000; - } - config.seleniumAddress = 'http://' + config.sauceUser + ':' + - config.sauceKey + '@ondemand.saucelabs.com:80/wd/hub'; - - util.puts('Using SauceLabs selenium server at ' + config.seleniumAddress); - deferred.fulfill(config.seleniumAddress); - } else { - util.puts('Starting selenium standalone server...'); - - if (!config.seleniumServerJar) { - // Try to use the default location. - var defaultStandalone = path.resolve(__dirname, - '../selenium/selenium-server-standalone-' + - require('../package.json').webdriverVersions.selenium + '.jar'); - if (!fs.existsSync(defaultStandalone)) { - throw new Error('Unable to start selenium. ' + - 'You must specify either a seleniumAddress, ' + - 'seleniumServerJar, or saucelabs account, or use webdriver-manager.'); - } else { - config.seleniumServerJar = defaultStandalone; - } - } else if (!fs.existsSync(config.seleniumServerJar)) { - throw new Error('there\'s no selenium server jar at the specified ' + - 'location. Do you have the correct version?'); - } - - if (config.chromeDriver) { - config.seleniumArgs.push( - '-Dwebdriver.chrome.driver=' + config.chromeDriver); - } - - server = new remote.SeleniumServer(config.seleniumServerJar, { - args: config.seleniumArgs, - port: config.seleniumPort - }); - - server.start().then(function(url) { - util.puts('Selenium standalone server started at ' + url); - config.seleniumAddress = server.address(); - deferred.fulfill(config.seleniumAddress); - }); - } - - return deferred.promise; + testRunner.run(); }; -/** - * Resolve a list of file patterns into a list of individual file paths. - * - * @param {array} patterns - * @param {boolean} opt_omitWarnings whether to omit did not match warnings - * - * @return {array} The resolved file paths. - */ -var resolveFilePatterns = function(patterns, opt_omitWarnings) { - var resolvedFiles = []; - - if (patterns) { - for (var i = 0; i < patterns.length; ++i) { - var matches = glob.sync(patterns[i], {cwd: config.configDir}); - if (!matches.length && !opt_omitWarnings) { - util.puts('Warning: pattern ' + patterns[i] + ' did not match any files.'); - } - for (var j = 0; j < matches.length; ++j) { - resolvedFiles.push(path.resolve(config.configDir, matches[j])); - } - } - } - return resolvedFiles; -}; - -/** - * Set up webdriver and run the tests. Note that due to the current setup of - * loading Jasmine and the test specs, this should only be run once. - * - * @return {webdriver.promise.Promise} A promise that will resolve - * when the test run is finished. - */ -var runTests = function() { - if (config.jasmineNodeOpts.specFolders) { - throw new Error('Using config.jasmineNodeOpts.specFolders is deprecated ' + - 'since Protractor 0.6.0. Please switch to config.specs.'); - } - - var resolvedExcludes = resolveFilePatterns(config.exclude, true); - var resolvedSpecs = resolveFilePatterns(config.specs).filter(function (path) { - return resolvedExcludes.indexOf(path) < 0; - }); - - if (!resolvedSpecs.length) { - throw new Error('Spec patterns did not match any files.'); - } - if (config.cucumberOpts && config.cucumberOpts.require) { - var cucumberRequire = config.cucumberOpts.require; - cucumberRequire = typeof cucumberRequire === 'string' ? - [cucumberRequire] : cucumberRequire; - var cucumberResolvedRequire = resolveFilePatterns(cucumberRequire); - } - - // TODO: This should not be tied to the webdriver promise loop, it should use - // another promise system instead. - var runDeferred = webdriver.promise.defer(); - - if (config.chromeOnly) { - var service = new chrome.ServiceBuilder(config.chromeDriver).build(); - driver = chrome.createDriver( - new webdriver.Capabilities(config.capabilities), service); - } else { - driver = new webdriver.Builder(). - usingServer(config.seleniumAddress). - withCapabilities(config.capabilities).build(); - } - - driver.getSession().then(function(session) { - driver.manage().timeouts().setScriptTimeout(config.allScriptsTimeout); - - sessionId = session.getId(); - - var browser = protractor.wrapDriver( - driver, - config.baseUrl, - config.rootElement); - browser.params = config.params; - - protractor.setInstance(browser); - - // Export protractor to the global namespace to be used in tests. - global.protractor = protractor; - global.browser = browser; - global.$ = browser.$; - global.$$ = browser.$$; - global.element = browser.element; - global.by = global.By = protractor.By; - - // Do the framework setup here so that jasmine and mocha globals are - // available to the onPrepare function. - var minijn, mocha, cucumber; - if (config.framework === 'jasmine') { - minijn = require('minijasminenode'); - require('../jasminewd'); - minijn.addSpecs(resolvedSpecs); - } else if (config.framework === 'mocha') { - var Mocha = require('mocha'); - - mocha = new Mocha(config.mochaOpts); - - resolvedSpecs.forEach(function(file) { - mocha.addFile(file); - }); - - // Mocha doesn't set up the ui until the pre-require event, so - // wait until then to load mocha-webdriver adapters as well. - mocha.suite.on('pre-require', function() { - var mochaAdapters = require('selenium-webdriver/testing'); - global.after = mochaAdapters.after; - global.afterEach = mochaAdapters.afterEach; - global.before = mochaAdapters.before; - global.beforeEach = mochaAdapters.beforeEach; +// Merge in config file options +argv = JSON.parse(process.env.optimistArgs); +configParser.addFileConfig(argv._[0]); +configParser.addArgvConfig(argv); +config = configParser.getConfig(); - global.it = mochaAdapters.it; - global.it.only = global.iit = mochaAdapters.it.only; - global.it.skip = global.xit = mochaAdapters.xit; - }); +//Grab capability to run from launcher +config.capabilities = JSON.parse(process.env.capability); - mocha.loadFiles(); - } else if (config.framework === 'cucumber') { - var Cucumber = require('cucumber'); - var execOptions = ['node', 'node_modules/.bin/cucumber-js']; - execOptions = execOptions.concat(resolvedSpecs); - - if (config.cucumberOpts) { - if (cucumberResolvedRequire && cucumberResolvedRequire.length) { - execOptions.push('-r'); - execOptions = execOptions.concat(cucumberResolvedRequire); - } - - if (config.cucumberOpts.tags) { - execOptions.push('-t'); - execOptions.push(config.cucumberOpts.tags); - } - - if (config.cucumberOpts.format) { - execOptions.push('-f'); - execOptions.push(config.cucumberOpts.format); - } - } - - cucumber = Cucumber.Cli(execOptions); - } else { - throw 'config.framework ' + config.framework + - ' is not a valid framework.'; - } - - // Let the configuration configure the protractor instance before running - // the tests. - webdriver.promise.controlFlow().execute(function() { - if (config.onPrepare) { - if (typeof config.onPrepare === 'function') { - config.onPrepare(); - } else if (typeof config.onPrepare === 'string') { - require(path.resolve(config.configDir, config.onPrepare)); - } else { - throw 'config.onPrepare must be a string or function'; - } - } - }).then(function() { - var options = config.jasmineNodeOpts; - var originalOnComplete = options.onComplete; - options.onComplete = function(runner, log) { - if (originalOnComplete) { - originalOnComplete(runner, log); - } - driver.quit().then(function() { - runDeferred.fulfill(runner); - }); - }; - - if (config.framework === 'jasmine') { - minijn.executeSpecs(options); - } else if (config.framework === 'mocha') { - mocha.run(function(failures) { - // Warning: hack to make it have the same signature as Jasmine 1.3.1. - if (originalOnComplete) { - originalOnComplete(); - } - driver.quit().then(function() { - runDeferred.fulfill({ - results: function() { - return { - failedCount: failures - }; - } - }); - }); - }); - } else if (config.framework === 'cucumber') { - cucumber.run(function(succeeded) { - // Warning: hack to make it have the same signature as Jasmine 1.3.1. - if (originalOnComplete) { - originalOnComplete(); - } - driver.quit().then(function() { - runDeferred.fulfill({ - results: function() { - return { - failedCount: succeeded ? 0 : 1 - }; - } - }); - }); - }); - } - }); - }); - - return runDeferred.promise; -}; - - -/** - * Run Protractor once. - */ -var runOnce = function() { - var specs = config.specs; - if (!specs || specs.length === 0) { - util.puts('No spec files found'); - process.exit(0); - } - - return setUpSelenium().then(function() { - // cleanUp must be registered directly onto runTests, not onto - // the chained promise, so that cleanUp is still called in case of a - // timeout error. Timeout errors need to clear the control flow, which - // would mess up chaining promises. - return runTests().then(cleanUp); - }); -}; +init(); -exports.addConfig = addConfig; -exports.runOnce = runOnce; diff --git a/lib/testRunner.js b/lib/testRunner.js new file mode 100644 index 000000000..c40233da5 --- /dev/null +++ b/lib/testRunner.js @@ -0,0 +1,456 @@ +var protractor = require(__dirname+'/protractor.js'), + webdriver = require('selenium-webdriver'), + configParser = require(__dirname+'/configParser'), + path = require('path'), + util = require('util'), + fs = require('fs'), + q = require('q'); + +/* + * This object serves as the primarily responsible party for starting the + * execution of a test run and triggering setup, teardown, managing config, + * etc through its various dependencies. + */ +function TestRunner(config) { + + this.config_= { + specs: [], + capabilities: { + browserName: 'chrome' + }, + seleniumHost: 'http://localhost:4444/wd/hub', + rootElement: 'body', + baseUrl: 'http://localhost', + timeout: 11000, + browserParams: {}, + framework: 'jasmine', + jasmineOpts: { + isVerbose: false, + showColors: true, + includeStackTrace: true, + stackFilter: protractor.filterStackTrace, + defaultTimeoutInterval: (30 * 1000) + }, + cucumberOpts: {}, + mochaOpts: { + ui: 'bdd', + reporter: 'list' + }, + seleniumLocal: { + jar: null, + args: [], + port: null + }, + sauceAccount: {}, + chromeDriver: null, + configDir: './' + }; + + //properties set up internally + this.preparers_ = []; + this.cleaners_ = []; + this.driverprovider_ = undefined; + + // Init + configParser.loadConfig(this.config_,config); + this.loadDriverProvider_(config); +} + + +/** + * Sets up chromeDriver property. Attempts to locate a default + * if none is provided + * @private + */ +TestRunner.prototype.setupChromeDriver_ = function() { + var defaultChromedriver, + userSpecified = !!(this.config_.chromeDriver); + + //use default if none was provided + defaultChromedriver = (this.config_.chromeDriver) ? + this.config_.chromeDriver : + path.resolve(__dirname,'../../selenium/chromedriver'); + + //check if file exists, if not try .exe or fail accordingly + if (!fs.existsSync(defaultChromedriver)) { + + defaultChromedriver+='.exe'; + //throw error if the client specified conf chromedriver and its not found + if (!fs.existsSync(defaultChromedriver) && userSpecified) { + throw new Error('Could not find chromedriver at ' + + defaultChromedriver); + } + } + this.config_.chromeDriver = defaultChromedriver; +}; + + +/** + * Responsible for executing the testRunner's test cases through Jasmine + * @private + * @param {Array} specs - Array of Directory Path Strings + * @param deferred - the deferred object that we're going to resolve when + * Jasmine is done + */ +TestRunner.prototype.runJasmine_ = function(specs, callbackFn) { + + //jasmine setup + var minijn = require('minijasminenode'), + self = this; + + //inject on complete within flow and handle jasmine execution + require('../jasminewd'); + webdriver.promise.controlFlow().execute(function() { + self.runTestPreparers_(); + }).then(function() { + var opt = self.config_.jasmineOpts; + opt._originalOnComplete = self.config_.onComplete; + opt.onComplete = function(runner, log) { + if (opt._originalOnComplete) { + opt._originalOnComplete(runner, log); + } + callbackFn(runner); + }; + + minijn.addSpecs(specs); + minijn.executeSpecs(opt); + }); +}; + + +/** + * Responsible for executing the testRunner's test cases through Mocha + * @private + * @param {Array} specs - Array of Directory Path Strings + * @param deferred - the deferred object that we're going to resolve + * when Mocha is done + */ +TestRunner.prototype.runMocha_ = function(specs, callbackFn) { + + var Mocha = require('mocha'), + mocha = new Mocha(this.config_.mochaOpts), + self = this; + + + // Mocha doesn't set up the ui until the pre-require event, so + // wait until then to load mocha-webdriver adapters as well. + mocha.suite.on('pre-require', function() { + var mochaAdapters = require('selenium-webdriver/testing'); + global.after = mochaAdapters.after; + global.afterEach = mochaAdapters.afterEach; + global.before = mochaAdapters.before; + global.beforeEach = mochaAdapters.beforeEach; + + global.it = mochaAdapters.it; + global.it.only = global.iit = mochaAdapters.it.only; + global.it.skip = global.xit = mochaAdapters.xit; + }); + + mocha.loadFiles(); + + webdriver.promise.controlFlow().execute(function() { + self.runTestPreparers_(); + }).then(function() { + + specs.forEach(function(file) { + mocha.addFile(file); + }); + + mocha.run(function(failures) { + if (self.config_.onComplete) { + self.config_.onComplete(); + } + var resolvedObj = { + results: function() { + return { + failedCount: failures + }; + } + }; + + self.driverprovider_.getDriver().quit().then(function() { + callbackFn(resolvedObj); + }); + }); + }); +}; + + +TestRunner.prototype.runCucumber_ = function(specs, callbackFn) { + var Cucumber = require('cucumber'), + self = this, + execOptions = ['node', 'node_modules/.bin/cucumber-js'], + cucumberResolvedRequire; + + //Set up exec options for Cucumber + execOptions = execOptions.concat(specs); + if (self.config_.cucumberOpts) { + + //Process Cucumber Require param + if (self.config_.cucumberOpts.require) { + cucumberResolvedRequire = + configParser.resolveFilePatterns(self.config_.cucumberOpts.require); + if (cucumberResolvedRequire && cucumberResolvedRequire.length) { + execOptions.push('-r'); + execOptions = execOptions.concat(cucumberResolvedRequire); + } + } + + //Process Cucumber Tag param + if (self.config_.cucumberOpts.tags) { + execOptions.push('-t'); + execOptions.push(self.config_.cucumberOpts.tags); + } + + //Process Cucumber Format param + if (self.config_.cucumberOpts.format) { + execOptions.push('-f'); + execOptions.push(self.config_.cucumberOpts.format); + } + } + cucumber = Cucumber.Cli(execOptions); + + webdriver.promise.controlFlow().execute(function() { + self.runTestPreparers_(); + }).then(function() { + + cucumber.run(function(succeeded) { + if (self.config_.onComplete) { + self.config_.onComplete(); + } + var resolvedObj = { + results: function() { + return { + failedCount: succeeded ? 0 : 1 + }; + } + }; + self.driverprovider_.getDriver().quit().then(function() { + callbackFn(resolvedObj); + }); + }); + }); +}; + + +/** + * Internal helper for abstraction of polymorphic filenameOrFn properties + * @private + * @param {Array} source - the Object Array that we'll be iterating through + * as we evaluate whether to require or execute each item. + */ +TestRunner.prototype.runFilenameOrFn_ = function(source) { + var i, filenameOrFn; + for (i=0; i=3.1.14", "adm-zip": ">=0.4.2", - "optimist": "~0.6.0" + "optimist": "~0.6.0", + "q": "1.0.0" }, "devDependencies": { "expect.js": "~0.2.0",