/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Parameter builder.
*
*
A Testing Utility library that allows invoking a function using all its
* posible parameters.
* This file contains two classes the first being the container for the
* parameters thus called ParamSet and the second being an iterator that will
* be used to generate the different combinations thus called iterator.
* To use this utility we must do the following:
*
* - Create a ParamSet and fill the parameters in.
* - Execute our function through the ParamSet.execEach() method.
* - Create callbacks to control when the function finishes successfuly and
* when it fails due to an exception.
*
* In order to fill the ParamSet we need to take into account the order and
* position of the parameters as they will be consistently used by the
* ParamSet.
* Look at the following function definition:
* function doSomething(a, b, c)
* Let's assume we want to test this class, the first parameter receives a
* number, the parameter b receives a string, and the parameter c receives a
* string or nothing (it is optional).
* First we need to know wich position do parameters have. The position is a
* 0 based index that is just the order of the parameter declaration, that is
* parameter a is in position 0, parameter b is in position 1 and parameter c is
* in position 2.
* Now we need to fill the ParamSet.
*
* var paramSet = new ParamBuilder.ParamSet();
* paramSet.addParam(0, 20);
* paramSet.addParam(0, 99);
* paramSet.addParam(0, 0);
* paramSet.addParam(1, 'Some string');
* paramSet.addParam(1, 'Another string');
* paramSet.addParam(2, null, 'NULL');
* paramSet.addParam(2, 'The optional String');
*
*
* In this case the addParam function receives 3 parameters. First the
* position of the parameter and second the value, the order in wich the
* parameters are added defines the order in wich they will be iterated also,
* and this order is known as index.
* The index is a 0 based number that is dependent on the position, this means,
* there is an index per position. So in this case the parameter 'some string'
* is in position 1 and index 0, the parameter 20 is in position 0 index 0,
* the parameter 'The optional String' is in position 0 index 1.
* This is important since sometimes we want to put assertions or custom
* behaivor depending on the parameter sent, so using the indices rather than
* the values might be an easier and less expensive (comparing numbers instead
* of comparing objects) aproach.
* The third and optional parameter opt_label for the addParam function just
* gives an alias that will be used for logging purposes, It is posible to put
* any string there and it will be used in the getStringAt method instead of the
* actual object, This optional parameter does not have any impact in the
* invocation of the function or the generation of the iterator. In the example
* for the parameter in position 2, index 0 (null) it has a string alias of NULL
* this alias will only be used when logging, and it comes in handy when using
* large objects or complex structures as parameters as something more
* descriptive can be logged instead of a simple "object" or "function"
* string.
* Now to do the actual invocation it is only neccesary to do:
*
* paramSet.execEach(null, doSomething);
*
* Aditionally if we want to know the state of the function after invoked we can
* add two callbacks as parameters that will receive the result of the function
* or the exception, as well as the indices of the positions used, for example:
*
* paramSet.execEach(null, doSomething,
* function success(resultValue, indexArray, paramSet) {
* // The call succeded and we might have some result value
* // The callback code here
* },
* function errorHappened(error, indexArray, paramSet){
* // Something bad happened and an error was thrown.
* // The error callback code here
* });
*
* The interesting part comes in the indexArray which is formed of the indices
* from each parameter used. For example the first invocation will have an
* indexArray of [0, 0, 0] while the last will have [2, 1, 1].
* So going back to the concepts of position and index, an array like [1, 0, 1]
* means that for position 0 the index 1 was used, for position 1 the index 0
* was used, for position 2 index 1 was used. this translates in the following
* parameter values: 99, 'Some string', 'The optional String'.
* So the actual function call that will originate either of the callbacks was:
* doSomething(99, 'Some string', 'The optional String');
* Then from those indices we can know the values of the parameters used using
* the paramSet, or if we did the call we might as well use the indices to
* compare relevant data and do some interesting conditional behaivor.
* Aditionally some manual operations can be done over this ParamSet using
* the iterator, the possibilities can be for example dynamic object
* generation.
*/
// Namespace
var ParamBuilder = {};
/**
* Class ParamBuilder.ParamSet, Base class for creating sets of parameters,
* controls the generation of parameter combinations and stores its values and
* aliases (labels).
* Note that this ParamSet will not allow duplicates in the same position, this
* means that a parameter cannot be added twice in the same position but might
* be added in two different positions.
* @constructor
*/
ParamBuilder.ParamSet = function() {
/**
* A two-dimensional array that contains the parameter sets.
* @type {Array.>}
*/
this.params_ = [];
/**
* A two-dimensional array that stores the labels of the parameters. This is
* used in order to have aliases for the parameters that can be logged easily.
* @type {Array.>}
*/
this.labels_ = [];
};
/**
* Adds a parameter value to the given position.
* @param {number} position The position of the value.
* @param {*} value The actual value of the parameter that can be used latter on
* to invoke the function.
* @param {string} opt_label An optional label(alias) for this value.
* @return {number} The added parameter index or -1 if the parameter couldn't be
* added.
*/
ParamBuilder.ParamSet.prototype.addParam = function(position, value,
opt_label) {
if (!this.params_[position]) {
this.params_[position] = [];
this.labels_[position] = [];
}
if (this.hasParam(position, value)) {
return -1;
}
this.params_[position].push(value);
this.labels_[position].push(opt_label);
return this.params_[position].size - 1;
};
/**
* Adds the contents of the array as parameters for the specified position.
* If a value inside the array can't be added then none of the values are added.
* If the operation fails (returns false) then it is recomended adding manually
* (using addParam) the elements of the array.
* @param {number} position The position of the parameter in the function.
* @param {Array<*>} paramArray Array of parameters that will be added.
* @return {boolean} True if all of the contents of the array were added, false
* if they couldn't be added.
*/
ParamBuilder.ParamSet.prototype.addAll = function(position, paramArray) {
for (var i = 0; i < paramArray.length; i++) {
if (this.hasParam(0, paramArray[i])) {
return false;
}
}
for (var i = 0; i < paramArray.length; i++) {
this.addParam(position, paramArray[i]);
}
return true;
};
/**
* Sets a parameter in the given position.
* @param {number} position The position of the parameter.
* @param {number} index The index of the parameter.
* @param {*} value The value of the parameter.
* @param {string} opt_label An optional label(alias) for this value.
* @return {boolean} True if the value has been altered, false if not (an
* existing value was set).
*/
ParamBuilder.ParamSet.prototype.setParam = function(position, index, value,
opt_label) {
if (position >= this.params_.length || index >= this.params_[position].length
|| this.hasParam(position, value)) {
return false;
}
this.params_[position][index] = value;
this.labels_[position][index] = opt_label;
return true;
};
/**
* Removes the given value at the given position.
* @param {number} position The position where the value is going to be removed.
* @param {*} value The value of the element to be removed from the set.
* @return {boolean} True if the value was removed, false if not (it didn't
* exist)
*/
ParamBuilder.ParamSet.prototype.removeParam = function(position, value) {
return this.removeParamAt(position, this.indexOf(position, value));
};
/**
* Removes a parameter from a specific position and index.
* @param {number} position The position where the element is.
* @param {number} index The index of the element to be removed.
* @return {boolean} True if the value was removed, false if not (it didn't
* exist)
*/
ParamBuilder.ParamSet.prototype.removeParamAt = function(position, index) {
if (position < this.params_.length && index < this.params_[position].length
&& index >= 0 && position >= 0) {
this.params_[position].splice(index, 1);
this.labels_[position].splice(index, 1);
return true;
}
return false;
};
/**
* Gets all the values mapped to the given indices.
* @param {Array.} indexArray An array containing the indices of the
* parameters at their positions.
* @param {Array.<*>} opt_valueArray An optional array that will be used to
* store the values instead of creating a new array each time.
* @return {Array.<*>} An array containing the values of the parameters mapped
* to their respective indices.
*/
ParamBuilder.ParamSet.prototype.getValues = function(indexArray,
opt_valueArray) {
var paramValues = opt_valueArray || [];
for (var i = 0; i < indexArray.length; i++) {
paramValues[i] = this.getValueAt(i, indexArray[i]);
}
return paramValues;
};
/**
* Gets a string representation of the values in the given indices, this
* representation is similar of how the parameters are actually used, the
* resulting string is like: (param1, param2, param3, ...).
* @param {Array.} indexArray the array of the indices of the values
* inside this paramSet.
* @return {string} A single string representing the values of the parameters
*/
ParamBuilder.ParamSet.prototype.indicesToString = function(indexArray) {
return '(' + this.getStrings(indexArray).join(', ') + ')';
};
/**
* Gets an array containing the string representations of the values inside
* this paramSet at the given indices.
* @param {Array.} indexArray the array of the indices of the values
* inside this paramSet.
* @param {Array.} opt_stringArray An optional string array to store the
* labels of the parameters instead of creating a new one each time this
* function is called.
* @return {Array.} An array containing the string representations of
* the values inside this paramSet at the given indices.
*/
ParamBuilder.ParamSet.prototype.getStrings = function(indexArray,
opt_stringArray) {
var str = opt_stringArray || [];
for (var i = 0; i < indexArray.length; i++) {
str[i] = this.getStringAt(i, indexArray[i]);
}
return str;
};
/**
* Gets a single value at the given position and index.
* @param {number} position The given position to retrieve the value.
* @param {number} index The index of the value.
* @return {*} Gets a value stored in the specified position, index.
*/
ParamBuilder.ParamSet.prototype.getValueAt = function(position, index) {
return this.params_[position][index];
};
/**
* Gets a string representation of a value in the given position, index.
* @param {number} position The given position to retrieve the value.
* @param {number} index The index of the value.
* @return {string} The string representation of the value stored at a given
* position and index, if the value has a label(alias) assigned then that
* label is returned.
*/
ParamBuilder.ParamSet.prototype.getStringAt = function(position, index) {
if (this.labels_[position][index]) {
return this.labels_[position][index];
}
if (typeof(this.labels_[position][index]) == 'function') {
return 'function ' + this.labels_[position][index].name;
}
if (self.gadgets && gadgets.json && gadgets.json.stringify) {
return gadgets.json.stringify(this.params_[position][index]);
}
if (self.JSON) {
return JSON.stringify(this.params_[position][index]);
}
if (this.params_[position][index].toString) {
return this.params_[position][index].toString();
}
return this.params_[position][index];
};
/**
* Seeks the value in the given position.
* @param {number} position The position to search.
* @param {*} value The value to search.
* @return {boolean} True if the value was found in the position, false
* otherwise.
*/
ParamBuilder.ParamSet.prototype.hasParam = function(position, value) {
return this.indexOf(position, value) >= 0;
};
/**
* Gets the index of the value for the given position or -1 if it was not found.
* @param {number} position The position to search.
* @param {*} value The value to search for.
* @return {number} The index of the value or -1 if none was found.
*/
ParamBuilder.ParamSet.prototype.indexOf = function(position, value) {
// Old school approach is used since there might actually be a null inside the
// parameter collection.
var values = this.params_[position];
if (!values) {
return -1;
}
for (var i = 0; i < values.length; i++) {
if (values[i] == value) {
return i;
}
}
return -1;
};
/**
* Executes the given function for all the possible permutations of parameters.
* This function also has the ability to invoke two callbacks upon two possible
* events. When the function finishes without errors the opt_successCallback is
* called, this callback receives three parameters, the result of the
* invocation, the indexArray of the parameters and the ParamSet that was used.
* If there is an error this function invokes another callback
* opt_exceptionCallback, this receives three parameters as well, the error
* thrown, the indexArray of the parameters and the ParamSet that was used.
* @param {object} thisObj The object that will be used as a THIS reference
* inside the function call.
* @param {Function} funct The function to be invoked.
* @param {Function(*, Array, ParamBuilder.ParamSet)}
* opt_successCallback A callback invoked when the function ends,
* the parameters for the callback are: the result of the invocation, the
* indexArray and ParamSet that were used in the call.
* @param {Function(Error, Array, ParamBuilder.ParamSet)}
* opt_exceptionCallback A callback invoked when the function ends with an
* exception, the parameters for the callback are: the exception thrown,
* the indexArray and ParamSet that were used in the call.
*/
ParamBuilder.ParamSet.prototype.execEach = function(thisObj, funct,
opt_successCallback, opt_exceptionCallback) {
var iterator = this.iterator();
var indexArray = [];
var valuesArray = [];
while (iterator.next(indexArray)) {
var success = false;
var functionResult = null;
try {
functionResult = funct.apply(thisObj, this.getValues(indexArray,
valuesArray));
success = true;
} catch(exception) {
if (opt_exceptionCallback) {
opt_exceptionCallback(exception, indexArray, this);
}
}
if (opt_successCallback && success) {
opt_successCallback(functionResult, indexArray, this);
}
}
};
/**
* Gets an iterator to get all possible combinations over the lenght of all the
* sets inside each position of this ParamSet
* @return {ParamBuilder.ParamIterator_} an Iterator to get all possible
* combinations.
*/
ParamBuilder.ParamSet.prototype.iterator = function() {
var maxValues = [];
for (var i = 0; i < this.params_.length; i++) {
maxValues.push(this.params_[i].length - 1);
}
return new ParamBuilder.ParamIterator_(maxValues);
};
/**
* Class ParamBuilder.ParamIterator_ This class basically generates an array for
* every iteration containing an increasing counter value that increases from
* right to left to a maximum amount given in the constructor's parameter
* maxValues.
* For example to iterate through numbers 0 to 999 the maxValues array must
* contain [9, 9, 9]. This means the iterator will go through ALL the numbers
* of the last index of the array, 0 through 9, then it will increase
* the value of the following index (from right to left), and so on.
* So the progression we have at the end is as follows:
* [0, 0, 0] [0, 0, 1] [0, 0, 2] [0, 0, 3] [0, 0, 4] [0, 0, 5] ... [0, 0, 9]
* [0, 1, 0] [0, 1, 1] [0, 1, 2] [0, 1, 3] [0, 1, 4] [0, 1, 5] ... [0, 1, 9]
* [0, 2, 0] [0, 2, 1] [0, 2, 2] [0, 2, 3] [0, 2, 4] [0, 2, 5] ... [0, 2, 9]
* ...
* [0, 9, 0] [0, 9, 1] [0, 9, 2] [0, 9, 3] [0, 9, 4] [0, 9, 5] ... [0, 9, 9]
* [9, 0, 0] [9, 0, 1] [9, 0, 2] [9, 0, 3] [9, 0, 4] [9, 0, 5] ... [9, 0, 9]
* ...
* [9, 9, 0] [9, 9, 1] [9, 9, 2] [9, 9, 3] [9, 9, 4] [9, 9, 5] ... [9, 9, 9]
* While this might be achieved using a simple counter the real usefulness of
* this iterator is that it can calculate non-standard progressions with
* irregular max values, this means: instead of having maximum values of
* [9, 9, 9] we can have [4, 12, 3] and this will go through the values
* [0, 0, 0] to [4, 12, 3] increasing values from right to left when the
* rightmost value reaches 3, the second rightest reaches 12 and stop when the
* leftmost reaches 4 (and the others have reached their max as well).
* The resulting progression is:
* [0, 0, 0] [0, 0, 1] [0, 0, 2] [0, 0, 3]
* [0, 1, 0] [0, 1, 1] [0, 1, 2] [0, 1, 2]
* ...
* [0, 11, 0] [0, 11, 1] [0, 11, 2] [0, 11, 3]
* [0, 12, 0] [0, 12, 1] [0, 12, 2] [0, 12, 3]
* [1, 0, 0] [1, 0, 1] [1, 0, 2] [1, 0, 3]
* ...
* [4, 11, 0] [4, 11, 1] [4, 11, 2] [4, 11, 3]
* [4, 12, 0] [4, 12, 1] [4, 12, 2] [4, 12, 3]
* @param {Array.} maxValues The maximum values of the indices to go
* through.
*/
ParamBuilder.ParamIterator_ = function(maxValues) {
this.maxValues_ = maxValues;
this.currentIndices_ = [];
for (var i = 0; i < maxValues.length; i++) {
this.currentIndices_[i] = 0;
}
};
/**
* Gets the next element in the collection and advances the iteration pointer by
* one.
* @param {Array.} opt_array Optional array to store use instead of
* creating a new array.
* @return {Array.} An array containing the resulting index combination.
*/
ParamBuilder.ParamIterator_.prototype.next = function(opt_array) {
if (!this.hasNext()) {
return null;
}
var indices = opt_array || [];
for (var i = 0; i < this.currentIndices_.length; i++) {
indices[i] = this.currentIndices_[i];
}
// Move the pointer forward
var lastArrayIndex = this.currentIndices_.length - 1;
this.currentIndices_[lastArrayIndex]++;
for (var i = lastArrayIndex; i > 0; i--) {
if (this.currentIndices_[i] > this.maxValues_[i]) {
this.currentIndices_[i] = 0;
this.currentIndices_[i-1]++;
} else {
break;
}
}
return indices;
};
/**
* Checks if there are more elements remaining in the collection to be iterated
* and returns true if they are and false if there are no more elements ahead.
* @return {boolean} true if there are more elements, false if not.
*/
ParamBuilder.ParamIterator_.prototype.hasNext = function() {
return this.currentIndices_[0] <= this.maxValues_[0];
};