Files
elasticsearch-js/test/integration/yaml_suite/yaml_doc.js
Spencer Alger dcc4a3747f Updated the API to the latest version and fixed an issue with the test runner,
which was confused by parameters that were in the URL, but not in the params list.
2014-04-15 08:30:42 -07:00

542 lines
15 KiB
JavaScript

/**
* Class to wrap a single document from a yaml test file
*
* @constructor
* @class YamlDoc
* @param actions {Array} - The array of actions directly from the Yaml file
*/
module.exports = YamlDoc;
var _ = require('../../../src/lib/utils');
var expect = require('expect.js');
var clientManager = require('./client_manager');
var implementedFeatures = ['gtelte', 'regex'];
/**
* The version that ES is running, in comparable string form XXX-XXX-XXX, fetched when needed
* @type {String}
*/
var ES_VERSION = null;
// core expression for finding a version
var versionExp = '([\\d\\.]*\\d)(?:\\.\\w+)?';
/**
* Regular Expression to extract a version number from a string
* @type {RegExp}
*/
var versionRE = new RegExp(versionExp);
/**
* Regular Expression to extract a version range from a string
* @type {RegExp}
*/
var versionRangeRE = new RegExp(versionExp + '\\s*\\-\\s*' + versionExp);
/**
* Fetches the client.info, and parses out the version number to a comparable string
* @param done {Function} - callback
*/
function getVersionFromES(done) {
clientManager.get().info({}, function (err, resp) {
if (err) {
throw new Error('unable to get info about ES');
}
expect(resp.version.number).to.match(versionRE);
ES_VERSION = versionToComparableString(versionRE.exec(resp.version.number)[1]);
done();
});
}
/**
* Transform x.x.x into xxx.xxx.xxx, striping off any text at the end like beta or pre-alpha35
*
* @param {String} version - Version number represented as a string
* @return {String} - Version number represented as three numbers, seperated by -, all numbers are
* padded with 0 and will be three characters long so the strings can be compared.
*/
function versionToComparableString(version) {
var parts = _.map(version.split('.'), function (part) {
part = '' + _.parseInt(part);
return (new Array(4 - part.length)).join('0') + part;
});
while (parts.length < 3) {
parts.push('000');
}
return parts.join('-');
}
/**
* Compare a version range to the ES_VERSION, determining if the current version
* falls within the range.
*
* @param {String} rangeString - a string representing two version numbers seperated by a "-"
* @return {Boolean} - is the current version within the range (inclusive)
*/
function rangeMatchesCurrentVersion(rangeString, done) {
function doWork() {
expect(rangeString).to.match(versionRangeRE);
var range = versionRangeRE.exec(rangeString);
range = _.map(_.last(range, 2), versionToComparableString);
done(ES_VERSION >= range[0] && ES_VERSION <= range[1]);
}
if (!ES_VERSION) {
getVersionFromES(doWork);
} else {
doWork();
}
}
// empty all of the indices in ES please
function clearIndices(done) {
clientManager.get().indices.delete({
index: '*',
ignore: 404
}, done);
}
function YamlDoc(doc, file) {
var self = this;
self.file = file;
self.description = _.keys(doc).shift();
self._stash = {};
self._last_requests_response = null;
// setup the actions, creating a bound and testable method for each
self._actions = _.map(self.flattenTestActions(doc[self.description]), function (action, i) {
// get the method that will do the action
var method = self['do_' + action.name];
// check that it's a function
expect(method || 'YamlDoc#' + action.name).to.be.a('function');
if (_.isPlainObject(action.args)) {
action.name += '(' + JSON.stringify(action.args) + ')';
} else if (action.args) {
action.name += '(' + action.args + ')';
}
// wrap in a check for skipping
action.bound = _.bind(method, self, action.args);
// create a function that can be passed to mocha or async
action.testable = function (_cb) {
function done(err) {
process.nextTick(function () {
if (err) {
err.message += ' in ' + action.name;
}
_cb(err);
});
}
if (self.skipping || self.file.skipping) {
return done();
}
if (method.length > 1) {
action.bound(done);
} else {
try {
action.bound();
process.nextTick(done);
} catch (err) {
done(err);
}
}
};
return action;
});
}
YamlDoc.prototype = {
/**
* convert tests actions
* from: [ {name:args, name:args}, {name:args}, ... ]
* to: [ {name:'', args:'' }, {name:'', args:''} ]
* so it's easier to work with
* @param {ArrayOfObjects} config - Actions to be taken as defined in the yaml specs
*/
flattenTestActions: function (config) {
// creates [ [ {name:"", args:"" }, ... ], ... ]
// from [ {name:args, name:args}, {name:args} ]
var actionSets = _.map(config, function (set) {
return _.map(_.pairs(set), function (pair) {
return { name: pair[0], args: pair[1] };
});
});
// do a single level flatten, merge=ing the nested arrays from step one
// into a master array, creating an array of action objects
return _.reduce(actionSets, function (note, set) {
return note.concat(set);
}, []);
},
/**
* Itterate over each of the actions, provides the testable function, and a name/description.
* return a litteral false to stop itterating
* @param {Function} ittr - The function to call for each action.
* @return {undefined}
*/
each: function (ittr) {
for (var i = 0; i < this._actions.length; i++) {
if (ittr(this._actions[i].testable, this._actions[i].name) === false) {
break;
}
}
},
/**
* Get a value from the last response, using dot-notation
*
* Example
* ===
*
* get '_source.tags.1'
*
* from {
* _source: {
* tags: [
* 'one',
* 'two'
* ]
* }
* }
*
* returns 'two'
*
* @param {string} path - The dot-notation path to the value needed.
* @return {*} - The value requested, or undefined if it was not found
*/
get: function (path, from) {
var log = process.env.LOG_GETS && !from ? console.log.bind(console) : function () {};
var i;
if (!from) {
if (path[0] === '$') {
from = this._stash;
path = path.substring(1);
} else {
from = this._last_requests_response;
}
}
log('getting', path, 'from', from);
var steps = _.map(path ? path.replace(/\\\./g, '\uffff').split('.') : [], function (step) {
return step.replace(/\uffff/g, '.');
});
var remainingSteps;
for (i = 0; from != null && i < steps.length; i++) {
if (from[steps[i]] === void 0) {
remainingSteps = steps.slice(i).join('.').replace(/\\\./g, '.');
from = from[remainingSteps];
break;
} else {
from = from[steps[i]];
}
}
log('found', typeof from !== 'function' ? from : 'function');
return from;
},
/**
* Do a skip operation, setting the skipping flag to true if the version matches
* the range defined in args.version
*
* @param args
* @param done
*/
do_skip: function (args, done) {
if (args.version) {
return rangeMatchesCurrentVersion(args.version, _.bind(function (match) {
if (match) {
if (this.description === 'setup') {
this.file.skipping = true;
// console.log('skipping this file' + (args.reason ? ' because ' + args.reason : ''));
} else {
this.skipping = true;
// console.log('skipping the rest of this doc' + (args.reason ? ' because ' + args.reason : ''));
}
} else {
this.skipping = false;
this.file.skipping = false;
}
done();
}, this));
}
if (args.features) {
var features = Array.isArray(args.features) ? args.features : [args.features];
var notImplemented = _.difference(features, implementedFeatures);
if (notImplemented.length) {
if (this.description === 'setup') {
this.file.skipping = true;
console.log('skipping this file because ' + notImplemented.join(' & ') + ' are not implemented');
} else {
this.skipping = true;
console.log('skipping the rest of this doc because ' + notImplemented.join(' & ') + ' are not implemented');
}
}
return done();
}
},
/**
* Do a request, as outlined in the args
*
* @param {[type]} args [description]
* @param {Function} done [description]
* @return {[type]} [description]
*/
do_do: function (args, done) {
var catcher;
// resolve the catch arg to a value used for matching once the request is complete
switch (args.catch) {
case void 0:
catcher = null;
break;
case 'missing':
catcher = 404;
break;
case 'conflict':
catcher = 409;
break;
case 'forbidden':
catcher = 403;
break;
case 'request':
catcher = /.*/;
break;
case 'param':
catcher = TypeError;
break;
default:
catcher = args.catch.match(/^\/(.*)\/$/);
if (catcher) {
catcher = new RegExp(catcher[1]);
}
}
delete args.catch;
var client = clientManager.get();
var action = _.keys(args).pop();
var clientActionName = _.map(action.split('.'), _.camelCase).join('.');
var clientAction = this.get(clientActionName, client);
var params = _.transform(args[action], function (params, val, name) {
var camelName = _.camelCase(name);
// search through the params and url peices to find this param name
var paramName = name;
var spec = clientAction && clientAction.spec;
var knownParam = spec && spec.params && spec.params[camelName];
var knownUrlParam = spec && !knownParam && !!_.find(spec.url ? [spec.url] : spec.urls, function (url) {
if ((url.opt && url.opt[camelName]) || (url.req && url.req[camelName])) {
return true;
}
});
// if we do know this param name, use the camelCase verison
if (knownParam || knownUrlParam) {
paramName = camelName;
}
// for ercursively traversing the params to replace '$stashed' vars
var transformObject = function (vals, val, i) {
switch (typeof val) {
case 'string':
val = (val[0] === '$') ? this.get(val) : val;
break;
case 'object':
val = _.transform(val, transformObject);
}
vals[i] = val;
}.bind(this);
// start with the initial param, only traverse traversables
switch (typeof val) {
case 'string':
val = (val[0] === '$') ? this.get(val) : val;
break;
case 'object':
val = _.transform(val, transformObject);
break;
}
params[paramName] = val;
}, {}, this);
expect(clientAction || clientActionName).to.be.a('function');
if (_.isNumeric(catcher)) {
params.ignore = _.union(params.ignore || [], [catcher]);
catcher = null;
}
var cb = _.bind(function (error, body, status) {
this._last_requests_response = body;
if (error) {
if (catcher) {
if (catcher instanceof RegExp) {
// error message should match the regexp
expect(error.message).to.match(catcher);
error = null;
} else if (typeof catcher === 'function') {
// error should be an instance of
expect(error).to.be.a(catcher);
error = null;
} else {
return done(new Error('Invalid catcher ' + catcher));
}
} else {
return done(error);
}
}
done(error);
}, this);
clientAction.call(client, params, cb);
},
/**
* Set a value from the respose into the stash
*
* Example
* ====
* { _id: id } # stash the value of `response._id` as `id`
*
* @param {Object} args - The object set to the "set" key in the test
* @return {undefined}
*/
do_set: function (args) {
_.forOwn(args, function (name, path) {
this._stash[name] = this.get(path);
}, this);
},
/**
* Test that the specified path exists in the response and has a
* true value (eg. not 0, false, undefined, null or the empty string)
*
* @param {string} path - Path to the response value to test
* @return {undefined}
*/
do_is_true: function (path) {
expect(Boolean(this.get(path))).to.be(true, 'path: ' + path);
},
/**
* Test that the specified path exists in the response and has a
* false value (eg. 0, false, undefined, null or the empty string)
*
* @param {string} path - Path to the response value to test
* @return {undefined}
*/
do_is_false: function (path) {
expect(Boolean(this.get(path))).to.be(false, 'path: ' + path);
},
/**
* Test that the response field (arg key) matches the value specified
*
* @param {Object} args - Hash of fields->values that need to be checked
* @return {undefined}
*/
do_match: function (args) {
_.forOwn(args, function (val, path) {
var isRef = _.isString(val) && val[0] === '$';
var isRE = _.isString(val) && val[0] === '/' && path[path.length - 1] === '/';
if (isRef) {
val = this.get(val === '$body' ? '' : val);
} else if (isRE) {
val = new RegExp(val);
} else {
val = this.get(path);
}
var assert = expect(val).to[isRE ? 'match' : 'eql'](val, 'path: ' + path);
}, this);
},
/**
* Test that the response field (arg key) is less than the value specified
*
* @param {Object} args - Hash of fields->values that need to be checked
* @return {undefined}
*/
do_lt: function (args) {
_.forOwn(args, function (num, path) {
expect(this.get(path)).to.be.below(num, 'path: ' + path);
}, this);
},
/**
* Test that the response field (arg key) is less than the value specified
*
* @param {Object} args - Hash of fields->values that need to be checked
* @return {undefined}
*/
do_lte: function (args) {
_.forOwn(args, function (num, path) {
expect(this.get(path) <= num).to.be.ok('path: ' + path);
}, this);
},
/**
* Test that the response field (arg key) is greater than the value specified
*
* @param {Object} args - Hash of fields->values that need to be checked
* @return {undefined}
*/
do_gt: function (args) {
_.forOwn(args, function (num, path) {
expect(this.get(path)).to.be.above(num, 'path: ' + path);
}, this);
},
/**
* Test that the response field (arg key) is greater than the value specified
*
* @param {Object} args - Hash of fields->values that need to be checked
* @return {undefined}
*/
do_gte: function (args) {
_.forOwn(args, function (num, path) {
expect(this.get(path) >= num).to.be.ok('path: ' + path);
}, this);
},
/**
* Test that the response field (arg key) has a length equal to that specified.
* For object values, checks the length of the keys.
*
* @param {Object} args - Hash of fields->values that need to be checked
* @return {undefined}
*/
do_length: function (args) {
_.forOwn(args, function (len, path) {
expect(_.size(this.get(path))).to.eql(len, 'path: ' + path);
}, this);
}
};