Skip to content

Commit

Permalink
Add support for version ranges and aliases
Browse files Browse the repository at this point in the history
  • Loading branch information
Paul Shannon authored and jason0x43 committed Jun 28, 2016
1 parent 951e016 commit ab9a245
Show file tree
Hide file tree
Showing 7 changed files with 660 additions and 138 deletions.
138 changes: 74 additions & 64 deletions lib/executors/Runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ define([
'../ProxiedSession',
'../Suite',
'../util',
'../resolveEnvironments',
'require'
], function (
lang,
Expand All @@ -23,6 +24,7 @@ define([
ProxiedSession,
Suite,
util,
resolveEnvironments,
require
) {
/**
Expand Down Expand Up @@ -122,8 +124,10 @@ define([
}

function loadTestModules() {
self.suites = self._createSuites(config, self.tunnel, self.preExecutor.getArguments());
return self._loadTestModules(config.functionalSuites);
return self._createSuites(config, self.tunnel, self.preExecutor.getArguments()).then(function (suites) {
self.suites = suites;
return self._loadTestModules(config.functionalSuites);
});
}

function startTunnel() {
Expand Down Expand Up @@ -168,7 +172,7 @@ define([
* @param {Configuration} config Intern configuration.
* @param {module:digdug/Tunnel} tunnel A Dig Dug tunnel.
* @param {Object} overrides Overrides to the user configuration provided via command-line.
* @returns {Suite[]} An array of root suites.
* @returns {Promise<Suite[]>} An array of root suites.
*/
_createSuites: function (config, tunnel, overrides) {
var proxy = this.proxy;
Expand All @@ -178,73 +182,79 @@ define([
});
server.sessionConstructor = ProxiedSession;

return util.flattenEnvironments(config.capabilities, config.environments).map(function (environmentType) {
var suite = new Suite({
name: String(environmentType),
reporterManager: reporterManager,
publishAfterSetup: true,
grep: config.grep,
bail: config.bail,
timeout: config.defaultTimeout,
setup: function () {
return util.retry(function () {
return server.createSession(environmentType);
}, config.environmentRetries).then(function (session) {
session.coverageEnabled = config.excludeInstrumentation !== true;
session.coverageVariable = config.instrumenterOptions.coverageVariable;
session.proxyUrl = config.proxyUrl;
session.proxyBasePathLength = config.basePath.length;
session.reporterManager = reporterManager;

var command = new Command(session);
command.environmentType = new EnvironmentType(session.capabilities);

suite.remote = command;
// TODO: Document or remove sessionStart/sessionEnd.
return reporterManager.emit('sessionStart', command);
});
},
teardown: function () {
var remote = this.remote;

function endSession() {
return reporterManager.emit('sessionEnd', remote).then(function () {
return tunnel.sendJobState(remote.session.sessionId, {
success: suite.numFailedTests === 0 && !suite.error
});
return tunnel.getEnvironments().then(function (tunnelEnvironments) {
return resolveEnvironments(
config.capabilities,
config.environments,
tunnelEnvironments
).map(function (environmentType) {
var suite = new Suite({
name: String(environmentType),
reporterManager: reporterManager,
publishAfterSetup: true,
grep: config.grep,
bail: config.bail,
timeout: config.defaultTimeout,
setup: function () {
return util.retry(function () {
return server.createSession(environmentType);
}, config.environmentRetries).then(function (session) {
session.coverageEnabled = config.excludeInstrumentation !== true;
session.coverageVariable = config.instrumenterOptions.coverageVariable;
session.proxyUrl = config.proxyUrl;
session.proxyBasePathLength = config.basePath.length;
session.reporterManager = reporterManager;

var command = new Command(session);
command.environmentType = new EnvironmentType(session.capabilities);

suite.remote = command;
// TODO: Document or remove sessionStart/sessionEnd.
return reporterManager.emit('sessionStart', command);
});
}

if (remote) {
if (
config.leaveRemoteOpen === true ||
(config.leaveRemoteOpen === 'fail' && this.numFailedTests > 0)
) {
return endSession();
},
teardown: function () {
var remote = this.remote;

function endSession() {
return reporterManager.emit('sessionEnd', remote).then(function () {
return tunnel.sendJobState(remote.session.sessionId, {
success: suite.numFailedTests === 0 && !suite.error
});
});
}

return remote.quit().finally(endSession);
if (remote) {
if (
config.leaveRemoteOpen === true ||
(config.leaveRemoteOpen === 'fail' && this.numFailedTests > 0)
) {
return endSession();
}

return remote.quit().finally(endSession);
}
}
}
});
});

// The `suites` flag specified on the command-line as an empty string will just get converted to an
// empty array in the client, which means we can skip the client tests entirely. Otherwise, if no
// suites were specified on the command-line, we rely on the existence of `config.suites` to decide
// whether or not to client suites. If `config.suites` is truthy, it may be an empty array on the
// Node.js side but could be a populated array when it gets to the browser side (conditional based
// on environment), so we require users to explicitly set it to a falsy value to assure the test
// system that it should not run the client
if (config.suites) {
suite.tests.push(new ClientSuite({
args: overrides,
config: config,
parent: suite,
proxy: proxy
}));
}
// The `suites` flag specified on the command-line as an empty string will just get converted to an
// empty array in the client, which means we can skip the client tests entirely. Otherwise, if no
// suites were specified on the command-line, we rely on the existence of `config.suites` to decide
// whether or not to client suites. If `config.suites` is truthy, it may be an empty array on the
// Node.js side but could be a populated array when it gets to the browser side (conditional based
// on environment), so we require users to explicitly set it to a falsy value to assure the test
// system that it should not run the client
if (config.suites) {
suite.tests.push(new ClientSuite({
args: overrides,
config: config,
parent: suite,
proxy: proxy
}));
}

return suite;
return suite;
});
});
},

Expand Down
209 changes: 209 additions & 0 deletions lib/resolveEnvironments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
define([
'dojo/lang',
'./EnvironmentType'
], function (lang, EnvironmentType) {
/**
* A comparator for sorting numbers in ascending order
*/
function ascendingNumbers(a, b) {
return a - b;
}

/**
* Expands a range of versions using available environments
*/
function expandVersionRange(left, right, availableVersions) {
left = Number(left);
right = Number(right);
if (availableVersions.indexOf(left) === -1 || availableVersions.indexOf(right) === -1) {
throw new Error('The version range ' + left + '..' + right + ' is unavailable');
}
return availableVersions.filter(function (version) {
return version >= left && version <= right;
});
}

/**
* Resolves a version alias from a list of available versions.
*
* Assumes availableVersions is sorted in ascending order. Acceptable versions are:
*
* - {number}
* - '{number}'
* - 'latest'
* - 'latest-{number}'
*
* @returns {number}
*/
function resolveVersionAlias(version, availableVersions) {
var pieces = version.split('-');
if (pieces.length > 2) {
throw new Error('Invalid alias syntax "' + version + '"');
}

pieces = pieces.map(function (piece) {
return piece.trim();
});

if (
(pieces.length === 2 && (pieces[0] !== 'latest' || isNaN(pieces[1]))) ||
(pieces.length === 1 && isNaN(pieces[0] && pieces[0] !== 'latest'))
) {
throw new Error('invalid alias syntax "' + version + '"');
}

if (pieces[0] === 'latest') {
var offset = pieces.length === 2 ? Number(pieces[1]) : 0;
if (offset > availableVersions.length) {
var message = 'Can\'t get ' + version + '; ' + availableVersions.length + ' version';
message += (availableVersions.length !== 1 ? 's are' : ' is') + ' available';
throw new Error(message);
}

return availableVersions[availableVersions.length - 1 - offset];
}
else {
return Number(pieces[0]);
}
}

/**
* Splits a version into one or two version strings using the '..' delimiter
*
* @returns {string[]}
*/
function splitVersions(versions) {
versions = versions.split('..');
if (versions.length > 2) {
throw new Error('Invalid version syntax');
}

return versions.map(function (version) {
return version.trim();
});
}

/**
* Get a list of versions from a list of available environments filtered by the current environment.
*
* @param {Object} environment the environment for which versions should match
* @param {Object[]} available a list of available environments
* @returns {number[]} a list of version numbers from available filtered by the current environment
*/
function getVersions(environment, available) {
var versions = {};

available.filter(function (availableEnvironment) {
// Return true if there are no mismatching keys
return !Object.keys(environment).filter(function (key) {
return key !== 'version';
}).some(function (key) {
return (key in availableEnvironment) && availableEnvironment[key] !== environment[key];
});
}).forEach(function (environment) {
versions[environment.version] = true;
});

return Object.keys(versions).map(function (version) {
return Number(version);
}).sort(ascendingNumbers);
}

/**
* Resolves version aliases (e.g. latest, latest - 1) and version ranges (e.g. 36 .. latest or latest - 3 .. latest)
* using the environment list returned by tunnel#getEnvironments().
*
* @param {Object} environment an object with an optional version property
* @param {Object[]} available a list of enviornment available on the target service
* @returns {Object} the environment with resolved version aliases
*/
function resolveVersions(environment, available) {
var versions = environment.version;
available = available || [];

if (versions && isNaN(versions)) {
var availableVersions = getVersions(environment, available);

versions = splitVersions(versions).map(function (version) {
return resolveVersionAlias(version, availableVersions);
});

if (versions.length === 2) {
if (versions[0] > versions[1]) {
throw new Error('Invalid range [' + versions + '], must be in ascending order');
}

versions = expandVersionRange(versions[0], versions[1], availableVersions);
}
}

return versions;
}

/**
* Builds permutations of an object by flattening properties holding array values into a collection of objects
* representing all combinations of objects for all arrays in the object.
*
* @param base {Object} a base set of properties applied to each source
* @param sources {Array.<Object>} a list of sources to flatten
* @return {Object[]} a flattened collection of sources
*/
function createPermutations(base, sources) {
// If no expansion sources were given, the set of permutations consists of just the base
if (!sources || sources.length === 0) {
return [ lang.mixin({}, base) ];
}

// Expand the permutation set for each source
return sources.map(function (source) {
return Object.keys(source).reduce(function (permutations, key) {
if (Array.isArray(source[key])) {
// For array values, create a copy of the permutation set for each array item, then use the
// combination of these copies as the new value of `permutations`
permutations = source[key].map(function (value) {
return permutations.map(function (permutation) {
var clone = lang.mixin({}, permutation);
clone[key] = value;
return clone;
});
}).reduce(function (newPermutations, keyPermutations) {
return newPermutations.concat(keyPermutations);
}, []);
}
else {
// For simple values, add the value to all current permutations
permutations.forEach(function (permutation) {
permutation[key] = source[key];
});
}
return permutations;
}, [ lang.mixin({}, base) ]);
}).reduce(function (newPermutations, sourcePermutations) {
return newPermutations.concat(sourcePermutations);
}, []);
}

/**
* Resolves a collection of Intern test environments to a list of service environments
*
* @param {Object} capabilities a base set of capabilities for all environments
* @param {Object[]} environments a list of user-requested enviromnents
* @param {Object[]?} available a list of available environments
* @returns {EnvironmentType} a list of flattened service environments
*/
function resolveEnvironments(capabilities, environments, available) {
environments = createPermutations(capabilities, environments);

// Expand any version ranges or aliases in the environments.
environments.forEach(function (environment) {
environment.version = resolveVersions(environment, available);
});

// Perform a second round of permuting to handle any expanded version ranges
return createPermutations({}, environments).map(function (environment) {
return new EnvironmentType(environment);
});
}

return resolveEnvironments;
});
Loading

0 comments on commit ab9a245

Please sign in to comment.