%PDF- %PDF-
| Direktori : /home/infra/fusioninventory/lib/lazy.js-0.5.1/spec/support/ |
| Current File : //home/infra/fusioninventory/lib/lazy.js-0.5.1/spec/support/spec_helper.js |
(function(context) {
// If this is Node, then we're running jasmine-node, which will load this file
// first (so we need to require Lazy right here right now).
if (typeof require === 'function') {
context.Lazy = require('../../lazy.node.js');
// Also need to load this for a nicer jasmine async interface
// (see https://github.com/derickbailey/jasmine.async).
context.AsyncSpec = require('jasmine-async')(context.jasmine);
// ...and also need this... at least until I refactor it on out of here.
require('./person.js');
}
/**
* Tests many requirements of a sequence in one fell swoop. (See
* comprehensiveTestCase for more details.) Also verifies that aliases
* delegate properly.
*
* @param {string} name The name of the method under test.
* @param {Object} options A whole bunch of configuration options specifying
* what should be tested. Here are the important ones:
*
* {
* cases: [
* {
* input: (the object, e.g., an array, to serve as the underlying
* source of the sequence),
* params: (the parameters to pass to the method, called on a
* sequence based on the underlying source),
* result: (the expected result of applying this method to the
* sequence, after calling .value())
* },
* ...
* ],
*
* aliases: (an array of other names this method can be called by),
* arrayLike: (whether the result should provide indexed access),
* supportsAsync: (just what it sounds like)
* }
*/
context.comprehensiveSequenceTest = function(name, options) {
var cases = options.cases;
Lazy(cases).each(function(testCase) {
comprehensiveTestCase(name, testCase, options);
});
var aliases = options.aliases || [];
Lazy(aliases).each(function(alias) {
describe('#' + alias, function() {
it('is an alias for #' + name, function() {
var verifyDelegation = function(sequence) {
spyOn(sequence, name).andCallThrough();
iterate(sequence[alias].apply(sequence, cases[0].params));
expect(sequence[name]).toHaveBeenCalled();
};
verifyDelegation(Lazy(cases[0].input));
verifyDelegation(Lazy(cases[0].input).map(Lazy.identity));
verifyDelegation(Lazy(cases[0].input).filter(alwaysTrue));
});
});
});
};
/**
* Verifies the following for a given sequence method, for a specified case
* (input/output):
*
* - the actual sequence behavior (result matches expected output)
* - consistent behavior among different base sequence types (e.g., wrapped
* array, array-like, and vanilla Sequence)
* - true laziness (does not iterate until `each` is called)
* - support for early termination
* - support for async iteration
*/
function comprehensiveTestCase(name, testCase, options) {
var label = '#' + name;
if (testCase.label) {
label += ' (' + testCase.label + ')';
}
describe(label, function() {
var monitor, sequence;
beforeEach(function() {
monitor = createMonitor(testCase.input);
sequence = Lazy(monitor);
});
function getResult() {
return sequence[name].apply(sequence, testCase.params || []);
}
function iterateResult() {
return iterate(getResult());
}
function assertResult() {
expect(getResult()).toComprise(testCase.result);
}
var sequenceTypes = [
{
label: 'an ArrayWrapper',
transform: function() { return sequence; }
},
{
label: 'an ArrayLikeSequence',
transform: function() { return sequence.map(Lazy.identity); }
},
{
label: 'an ordinary sequence',
transform: function() { return sequence.filter(alwaysTrue); },
arrayLike: false
}
];
Lazy(sequenceTypes).each(function(sequenceType) {
describe('for ' + sequenceType.label, function() {
beforeEach(function() {
sequence = sequenceType.transform();
});
it('works as expected', function() {
assertResult();
});
it('is actually lazy', function() {
getResult();
expect(monitor.accessCount()).toBe(0);
});
it('supports early termination', function() {
expect(getResult().take(2)).toComprise(testCase.result.slice(0, 2));
});
// For something like Lazy([...]).take(N), we only need to access N
// elements; however, some sequence types may require > N accesses
// to produce N results. An obvious example is #filter.
if (!lookupValue('skipAccessCounts', [sequenceType, options])) {
it('accesses the minimum number of elements from the source', function() {
var expectedAccessCount = testCase.accessCountForTake2 || 2;
iterate(getResult().take(2));
expect(monitor.accessCount()).toEqual(expectedAccessCount);
});
}
it('passes along the index with each element during iteration', function() {
indexes = getResult().map(function(e, i) { return i; }).toArray();
expect(indexes).toComprise(Lazy.range(indexes.length));
});
describe('each', function() {
it('returns true if the entire sequence is iterated', function() {
var result = iterateResult();
expect(result).toBe(true);
});
it('returns false if iteration is terminated early', function() {
var result = getResult().each(alwaysFalse);
expect(result).toBe(false);
});
it('returns false if the last iteration returns false', function() {
var length = getResult().value().length;
var result = getResult().each(function(e, i) {
if (i === length - 1) {
return false;
}
});
expect(result).toBe(false);
});
});
describe('indexed access', function() {
it('is supported', function() {
expect(getResult().get(1)).toEqual(testCase.result[1]);
});
if (lookupValue('arrayLike', [sequenceType, options])) {
it('does not invoke full iteration', function() {
getResult().get(1);
expect(monitor.accessCount()).toEqual(1);
});
}
});
if (lookupValue('supportsAsync', [sequenceType, options])) {
describe('async iteration', function() {
var async = new AsyncSpec(this);
function getAsyncResult() {
return getResult().async();
}
// Currently this tests if blah().async() works.
// TODO: First, think about whether async().blah() should work.
// TODO: IF it should work, then make it work (better)!
async.it('is supported', function(done) {
getAsyncResult().toArray().onComplete(function(result) {
expect(result).toEqual(testCase.result);
done();
});
});
async.it('supports early termination', function(done) {
var expectedAccessCount = testCase.accessCountForTake2 || 2;
getAsyncResult().take(2).toArray().onComplete(function(result) {
expect(result).toEqual(testCase.result.slice(0, 2));
done();
});
});
});
}
});
});
});
}
/**
* Takes an object (e.g. an array) and returns a copy of that object that
* monitors its properties so that it can tell when one has been accessed.
* This is useful for tests that want to ensure certain elements of an array
* haven't been looked at.
*/
function createMonitor(target) {
var monitor = Lazy.clone(target),
accesses = {};
function monitorProperty(property) {
Object.defineProperty(monitor, property, {
get: function() {
accesses[property] = true;
return target[property];
}
});
}
Lazy(target).each(function(value, property) {
monitorProperty(property);
});
monitor.accessCount = function() {
return Object.keys(accesses).length;
};
monitor.accessedAt = function(property) {
return !!accesses[property];
};
return monitor;
}
/**
* Forces iteration over a sequence.
*/
function iterate(sequence) {
return sequence.each(Lazy.noop);
}
/**
* Given the name of a property, iterates over a list of objects until finding
* one with the given property. Returns the first value found.
*
* This is to allow the options supplied to a call to
* #comprehensiveSequenceTest to include properties at the top level, and also
* to override those at the case level.
*/
function lookupValue(property, objects) {
for (var i = 0; i < objects.length; ++i) {
if (property in objects[i]) {
return objects[i][property];
}
}
}
// This is basically to allow verifying that certain methods don't create
// intermediate arrays. Not sure if it's really sensible to test this; but
// anyway, that's the purpose.
context.arraysCreated = 0;
var originalToArray = Lazy.Sequence.prototype.toArray;
Lazy.Sequence.prototype.toArray = function() {
var result = originalToArray.apply(this);
arraysCreated += 1;
return result;
};
beforeEach(function() {
var people = [
context.david = new Person("David", 63, "M"),
context.mary = new Person("Mary", 62, "F"),
context.lauren = new Person("Lauren", 32, "F"),
context.adam = new Person("Adam", 30, "M"),
context.daniel = new Person("Daniel", 28, "M"),
context.happy = new Person("Happy", 25, "F")
];
context.people = people.slice(0);
var personsAccessed = [
false,
false,
false,
false,
false,
false
];
context.personsAccessed = function() {
return Lazy(personsAccessed).compact().value().length;
};
Lazy.range(context.people.length).forEach(function(index) {
Object.defineProperty(context.people, index, {
get: function() {
personsAccessed[index] = true;
return people[index];
}
});
});
Person.reset(people);
arraysCreated = 0;
});
beforeEach(function() {
this.addMatchers({
toComprise: function(elements) {
var actual = this.actual;
if (actual instanceof Lazy.Sequence) {
actual = actual.value();
}
if (elements instanceof Lazy.Sequence) {
elements = elements.value();
}
expect(actual).toEqual(elements);
return true;
},
toBeInstanceOf: function(type) {
var actual = this.actual;
this.message = function() {
return 'Expected ' + actual + ' to be a ' + (type.name || type);
};
return actual instanceof type;
},
toPassToEach: function(argumentIndex, expectedValues) {
var i = 0;
this.actual.each(function() {
expect(arguments[argumentIndex]).toEqual(expectedValues[i++]);
});
return true;
}
});
});
/**
* Populates a collection.
*/
context.populate = function(collection, contents) {
if (collection instanceof Array) {
for (var i = 0, len = contents.length; i < len; ++i) {
collection.push(contents[i]);
}
return;
}
for (var key in contents) {
collection[key] = contents[key];
}
};
// --------------------------------------------------------------------------
// I think most of the stuff below here is deprecated. It's more specialized
// stuff duplicating what comprehensiveSequenceTest provides.
// --------------------------------------------------------------------------
context.ensureLaziness = function(action) {
it("doesn't eagerly iterate the collection", function() {
action();
expect(Person.accesses).toBe(0);
});
};
// Example usage:
// createAsyncTest('blah', {
// getSequence: function() { return Lazy([1, 2, 3]); },
// expected: [1, 2, 3]
// });
context.createAsyncTest = function(description, options) {
it(description, function() {
performAsyncSteps(options);
});
};
context.performAsyncSteps = function(options) {
var results = [];
// This can be a function, in case what we want to expect is not defined at the time
// createAsyncTest is called.
var expected = typeof options.expected === "function" ?
options.expected() :
options.expected;
runs(function() {
options.getSequence().each(function(e) { results.push(e); });
// Should not yet be populated.
expect(results.length).toBe(0);
});
waitsFor(function() {
return results.length === expected.length;
});
runs(function() {
expect(results).toEqual(expected);
});
if (options.additionalExpectations) {
runs(options.additionalExpectations);
}
};
context.testAllSequenceTypes = function(description, array, expectation) {
it(description + " for a wrapped array", function() {
var arrayWrapper = Lazy(array);
expectation(arrayWrapper);
});
it(description + " for an indexed sequence", function() {
var indexedSequence = Lazy(array).map(Lazy.identity);
expectation(indexedSequence);
});
it(description + " for a non-indexed sequence", function() {
var nonIndexedSequence = Lazy(array).filter(alwaysTrue);
expectation(nonIndexedSequence);
});
};
// ----- Helpers, to make specs more concise -----
context.add = function(x, y) { return x + y; };
context.increment = function(x) { return x + 1; };
context.isEven = function(x) { return x % 2 === 0; };
context.alwaysTrue = function(x) { return true; };
context.alwaysFalse = function(x) { return false; };
// ----- Specifically for spies -----
context.toBeCalled = function(callback) {
return function() { return callback.callCount > 0; };
};
context.toBePopulated = function(collection, length) {
return function() {
if (!collection) {
return false;
}
var size = typeof collection.length === 'number' ?
collection.length :
Object.keys(collection).length;
if (length) {
return size === length;
}
return size > 0;
};
};
}(typeof global !== 'undefined' ? global : window));