Files
elasticsearch-js/test/integration/yaml_suite/yaml_doc.js
Spencer Alger 96b44ebf8b Merging spenceralger:travis_and_coveralls. Summary of changes:
- removed several unneeded devDeps
- removed old get_spec.js script
- the client's ping method will now send back true as the body when the ping
  succceeds, and false when it does not. When the ping fails, the error will
  still be sent back and the connection's status will still be set to "dead".
- All of the client's methods now have a spec property, which will provide the
  JSON spec used to run that method.
- The yaml test runner will only camelCase param names that are documented, uses
  the client's method's new spec property
- Trace log events will now have their proper original query string parameters
- The "tracer" logger will now write to elasticsearch-tracer.log by default, and
  will truncate the file if it already exists.
- When running the integration tests, the client will now use a tracer logger which
  writes to stderr. The default level is "warning", but with the VERBOSE environment
  var it becomes "trace" and the logger will write to it's default file
- Added .idea to the .gitignore, it was being published to NPM
- Cleanup of the grunt tasks. Consilidated several tiny files into seperate moderately sized ones.
2013-12-18 13:21:40 -07:00

449 lines
12 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 should = require('should');
var clientManager = require('./client_manager');
/**
* 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');
}
should(resp.version.number).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() {
should(rangeString).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
should(method).have.type('function');
if (_.isPlainObject(action.args)) {
action.name += ' ' + _.keys(action.args).join(', ');
} 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
action.testable = function (done) {
if (self.skipping || self.file.skipping) {
return done();
}
if (method.length > 1) {
action.bound(done);
} else {
action.bound();
done();
}
};
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) {
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));
},
/**
* 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 = Object.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);
// undocumented params should be passed through as-is
var paramName = name;
if (clientAction && clientAction.spec && clientAction.spec.params && clientAction.spec.params[camelName]) {
paramName = camelName;
}
params[paramName] = (typeof val === 'string' && val[0] === '$') ? this.get(val) : val;
}, {}, this);
should(clientAction || clientActionName).have.type('function');
if (typeof clientAction === '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
should(error.message).match(catcher);
error = null;
} else if (typeof catcher === 'function') {
// error should be an instance of
should(error).be.an.instanceOf(catcher);
error = null;
} else {
return done(new Error('Invalid catcher ' + catcher));
}
} else {
return done(error);
}
}
done(error);
}, this);
clientAction.call(client, params, cb);
} else {
done(new Error('stepped in do_do, did not find a function'));
}
},
/**
* 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) {
should(Boolean(this.get(path))).equal(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) {
should(Boolean(this.get(path))).equal(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) {
if (val[0] === '$') {
val = this.get(val);
}
should(this.get(path)).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) {
should(this.get(path)).be.below(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_gt: function (args) {
_.forOwn(args, function (num, path) {
should(this.get(path)).be.above(num, '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) {
should(_.size(this.get(path))).eql(len, 'path: ' + path);
}, this);
}
};