Browse Source

Merge branch 'feature/mongo_2.6.5_accumulators' of http://github.com/RiveraGroup/mungedb-aggregate into feature/mongo_2.6.5_accumulators

Kyle P Davis 11 năm trước cách đây
mục cha
commit
3a770a8da2

+ 29 - 33
lib/pipeline/accumulators/AddToSetAccumulator.js

@@ -7,53 +7,49 @@
  * @module mungedb-aggregate
  * @constructor
 **/
-var AddToSetAccumulator = module.exports = function AddToSetAccumulator(/* ctx */){
+var AddToSetAccumulator = module.exports = function AddToSetAccumulator(){
 	if (arguments.length !== 0) throw new Error("zero args expected");
-	this.set = [];
-	//this.itr = undefined; /* Shoudln't need an iterator for the set */
-	//this.ctx = undefined; /* Not using the context object currently as it is related to sharding */
+	this.reset();
 	base.call(this);
 }, klass = AddToSetAccumulator, Accumulator = require("./Accumulator"), base = Accumulator, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// NOTE: Skipping the create function, using the constructor instead
-
-// DEPENDENCIES
 var Value = require("../Value");
 
-
-// MEMBER FUNCTIONS
-
-proto.getOpName = function getOpName(){
-	return "$addToSet";
-};
-
-proto.getFactory = function getFactory(){
-	return klass;	// using the ctor rather than a separate .create() method
-};
-
-
-proto.contains = function contains(value) {
-	var set = this.set;
-	for (var i = 0, l = set.length; i < l; ++i) {
-		if (Value.compare(set[i], value) === 0) {
-			return true;
-		}
-	}
-	return false;
-};
-
 proto.processInternal = function processInternal(input, merging) {
-	if (! this.contains(input)) {
-		this.set.push(input);
+	if (!merging) {
+		if (input !== undefined) {
+			this.set[JSON.stringify(input)] = input;
+		}
+	} else {
+		// If we're merging, we need to take apart the arrays we
+		// receive and put their elements into the array we are collecting.
+		// If we didn't, then we'd get an array of arrays, with one array
+		// from each merge source.
+		if (!Array.isArray(input)) throw new Error("Assertion failure");
+
+		for (var i = 0, l = input.length; i < l; i++) {
+			this.set[JSON.stringify(input[i])] = input[i];
+		}
 	}
 };
 
 proto.getValue = function getValue(toBeMerged) {
-	return this.set;
+	var results = [];
+	for(var key in this.set){
+		// if(!Object.hasOwnProperty(this.set))
+		results.push(this.set[key]);
+	}
+	return results;
 };
 
 proto.reset = function reset() {
-	this.set = [];
+	this.set = {};
 };
 
+klass.create = function create() {
+	return new AddToSetAccumulator();
+};
 
+proto.getOpName = function getOpName() {
+	return "$addToSet";
+};

+ 29 - 33
lib/pipeline/accumulators/AvgAccumulator.js

@@ -8,57 +8,53 @@
  * @constructor
  **/
 var AvgAccumulator = module.exports = function AvgAccumulator(){
-	this.subTotalName = "subTotal";
-	this.countName = "count";
-	this.totalIsANumber = true;
-	this.total = 0;
-	this.count = 0;
+	this.reset();
 	base.call(this);
 }, klass = AvgAccumulator, Accumulator = require("./Accumulator"), base = Accumulator, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// NOTE: Skipping the create function, using the constructor instead
-
-// DEPENDENCIES
 var Value = require("../Value");
 
-// MEMBER FUNCTIONS
+var SUB_TOTAL_NAME = "subTotal";
+var COUNT_NAME = "count";
+
 proto.processInternal = function processInternal(input, merging) {
 	if (!merging) {
-		if (typeof input !== "number") {
-			return;
-		}
-		this.total += input;
-		this.count += 1;
+		// non numeric types have no impact on average
+		if (typeof input != "number") return;
+
+		this._total += input;
+		this._count += 1;
 	} else {
-		Value.verifyDocument(input);
-		this.total += input[this.subTotalName];
-		this.count += input[this.countName];
+		// We expect an object that contains both a subtotal and a count.
+		// This is what getValue(true) produced below.
+		if (!(input instanceof Object)) throw new Error("Assertion error");
+		this._total += input[SUB_TOTAL_NAME];
+		this._count += input[COUNT_NAME];
 	}
 };
 
-proto.getValue = function getValue(toBeMerged){
+klass.create = function create() {
+	return new AvgAccumulator();
+};
+
+proto.getValue = function getValue(toBeMerged) {
 	if (!toBeMerged) {
-		if (this.totalIsANumber && this.count > 0) {
-			return this.total / this.count;
-		} else if (this.count === 0) {
-			return 0;
-		} else {
-			throw new Error("$sum resulted in a non-numeric type");
-		}
+		if (this._count === 0)
+			return 0.0;
+		return this._total / this._count;
 	} else {
-		var ret = {};
-		ret[this.subTotalName] = this.total;
-		ret[this.countName] = this.count;
-
-		return ret;
+		var doc = {};
+		doc[SUB_TOTAL_NAME] = this._total;
+		doc[COUNT_NAME] = this._count;
+		return doc;
 	}
 };
 
 proto.reset = function reset() {
-	this.total = 0;
-	this.count = 0;
+	this._total = 0;
+	this._count = 0;
 };
 
-proto.getOpName = function getOpName(){
+proto.getOpName = function getOpName() {
 	return "$avg";
 };

+ 10 - 18
lib/pipeline/accumulators/FirstAccumulator.js

@@ -9,30 +9,15 @@
  **/
 var FirstAccumulator = module.exports = function FirstAccumulator(){
 	if (arguments.length !== 0) throw new Error("zero args expected");
+	this.reset();
 	base.call(this);
-	this._haveFirst = false;
-	this._first = undefined;
 }, klass = FirstAccumulator, base = require("./Accumulator"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// NOTE: Skipping the create function, using the constructor instead
-
-// MEMBER FUNCTIONS
-proto.getOpName = function getOpName(){
-	return "$first";
-};
-
-proto.getFactory = function getFactory(){
-	return klass;	// using the ctor rather than a separate .create() method
-};
-
-
 proto.processInternal = function processInternal(input, merging) {
-	/* only remember the first value seen */
+	// only remember the first value seen
 	if (!this._haveFirst) {
-		// can't use pValue.missing() since we want the first value even if missing
 		this._haveFirst = true;
 		this._first = input;
-		//this._memUsageBytes = sizeof(*this) + input.getApproximateSize() - sizeof(Value);
 	}
 };
 
@@ -43,5 +28,12 @@ proto.getValue = function getValue(toBeMerged) {
 proto.reset = function reset() {
 	this._haveFirst = false;
 	this._first = undefined;
-	this._memUsageBytes = 0;
+};
+
+klass.create = function create() {
+	return new FirstAccumulator();
+};
+
+proto.getOpName = function getOpName() {
+	return "$first";
 };

+ 16 - 13
lib/pipeline/accumulators/LastAccumulator.js

@@ -1,32 +1,35 @@
 "use strict";
 
-/** 
- * Constructor for LastAccumulator, wraps SingleValueAccumulator's constructor and finds the last document
+/**
+ * Accumulator for getting last value
  * @class LastAccumulator
  * @namespace mungedb-aggregate.pipeline.accumulators
  * @module mungedb-aggregate
  * @constructor
  **/
 var LastAccumulator = module.exports = function LastAccumulator(){
+	if (arguments.length !== 0) throw new Error("zero args expected");
+	this.reset();
 	base.call(this);
-	this.value = undefined;
 }, klass = LastAccumulator, base = require("./Accumulator"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// NOTE: Skipping the create function, using the constructor instead
+proto.processInternal = function processInternal(input, merging) {
+	// always remember the last value seen
+	this._last = input;
+};
 
-// MEMBER FUNCTIONS
-proto.processInternal = function processInternal(input, merging){
-	this.value = input;
+proto.getValue = function getValue(toBeMerged) {
+	return this._last;
 };
 
-proto.getValue = function getValue() {
-	return this.value;
+proto.reset = function reset() {
+	this._last = undefined;
 };
 
-proto.getOpName = function getOpName(){
-	return "$last";
+klass.create = function create() {
+	return new LastAccumulator();
 };
 
-proto.reset = function reset() {
-	this.value = undefined;
+proto.getOpName = function getOpName(){
+	return "$last";
 };

+ 25 - 31
lib/pipeline/accumulators/MinMaxAccumulator.js

@@ -1,55 +1,49 @@
 "use strict";
 
 /**
- * Constructor for MinMaxAccumulator, wraps SingleValueAccumulator's constructor and adds flag to track whether we have started or not
+ * Accumulator to get the min or max value
  * @class MinMaxAccumulator
  * @namespace mungedb-aggregate.pipeline.accumulators
  * @module mungedb-aggregate
  * @constructor
  **/
-var MinMaxAccumulator = module.exports = function MinMaxAccumulator(sense){
-	if (arguments.length > 1) throw new Error("expects a single value");
+var MinMaxAccumulator = module.exports = function MinMaxAccumulator(theSense){
+	if (arguments.length != 1) throw new Error("expects a single value");
+	this._sense = theSense; // 1 for min, -1 for max; used to "scale" comparison
 	base.call(this);
-	this.sense = sense; /* 1 for min, -1 for max; used to "scale" comparison */
-	if (this.sense !== 1 && this.sense !== -1) throw new Error("this should never happen");
+	if (this._sense !== 1 && this._sense !== -1) throw new Error("Assertion failure");
 }, klass = MinMaxAccumulator, base = require("./Accumulator"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// NOTE: Skipping the create function, using the constructor instead
-
-// DEPENDENCIES
 var Value = require("../Value");
 
-// MEMBER FUNCTIONS
-proto.getOpName = function getOpName(){
-	if (this.sense == 1) return "$min";
-	return "$max";
-};
-
-klass.createMin = function createMin(){
-	return new MinMaxAccumulator(1);
+proto.processInternal = function processInternal(input, merging) {
+	// nullish values should have no impact on result
+	if (!(input === undefined || input === null)) {
+		// compare with the current value; swap if appropriate
+		var cmp = Value.compare(this._val, input) * this._sense;
+		if (cmp > 0 || this._val === undefined) { // missing is lower than all other values
+			this._val = input;
+		}
+	}
 };
 
-klass.createMax = function createMax(){
-	return new MinMaxAccumulator(-1);
+proto.getValue = function getValue(toBeMerged) {
+	return this._val;
 };
 
 proto.reset = function reset() {
-	this.value = undefined;
+	this._val = undefined;
 };
 
-proto.getValue = function getValue(toBeMerged) {
-	return this.value;
+klass.createMin = function createMin(){
+	return new MinMaxAccumulator(1);
 };
 
-proto.processInternal = function processInternal(input, merging) {
-	// if this is the first value, just use it
-	if (!this.hasOwnProperty('value')) {
-		this.value = input;
-	} else {
-		// compare with the current value; swap if appropriate
-		var cmp = Value.compare(this.value, input) * this.sense;
-		if (cmp > 0) this.value = input;
-	}
+klass.createMax = function createMax(){
+	return new MinMaxAccumulator(-1);
+};
 
-	return this.value;
+proto.getOpName = function getOpName() {
+	if (this._sense == 1) return "$min";
+	return "$max";
 };

+ 20 - 25
lib/pipeline/accumulators/PushAccumulator.js

@@ -8,44 +8,39 @@
  * @constructor
  **/
 var PushAccumulator = module.exports = function PushAccumulator(){
+	if (arguments.length !== 0) throw new Error("zero args expected");
 	this.values = [];
 	base.call(this);
 }, klass = PushAccumulator, Accumulator = require("./Accumulator"), base = Accumulator, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// NOTE: Skipping the create function, using the constructor instead
-
-// MEMBER FUNCTIONS
-proto.getValue = function getValue(toBeMerged){
-	return this.values;
-};
-
-proto.getOpName = function getOpName(){
-	return "$push";
-};
-
-proto.getFactory = function getFactory(){
-	return klass;	// using the ctor rather than a separate .create() method
-};
-
-
 proto.processInternal = function processInternal(input, merging) {
 	if (!merging) {
 		if (input !== undefined) {
 			this.values.push(input);
-			//_memUsageBytes += input.getApproximateSize();
 		}
-	}
-	else {
+	} else {
 		// If we're merging, we need to take apart the arrays we
 		// receive and put their elements into the array we are collecting.
 		// If we didn't, then we'd get an array of arrays, with one array
 		// from each merge source.
-		if (!(input instanceof Array)) throw new Error("input is not an Array during merge in PushAccumulator:35");
-
-		this.values = this.values.concat(input);
+		if (!Array.isArray(input)) throw new Error("Assertion failure");
 
-		//for (size_t i=0; i < vec.size(); i++) {
-			//_memUsageBytes += vec[i].getApproximateSize();
-		//}
+		Array.prototype.push.apply(this.values, input);
 	}
 };
+
+proto.getValue = function getValue(toBeMerged) {
+	return this.values;
+};
+
+proto.reset = function reset() {
+	this.values = [];
+};
+
+klass.create = function create() {
+	return new PushAccumulator();
+};
+
+proto.getOpName = function getOpName() {
+	return "$push";
+};

+ 25 - 18
lib/pipeline/accumulators/SumAccumulator.js

@@ -1,37 +1,44 @@
 "use strict";
 
-/** 
- * Accumulator for summing a field across documents
+/**
+ * Accumulator for summing values
  * @class SumAccumulator
  * @namespace mungedb-aggregate.pipeline.accumulators
  * @module mungedb-aggregate
  * @constructor
  **/
-var SumAccumulator = module.exports = function SumAccumulator(){
-	this.total = 0;
-	this.count = 0;
-	this.totalIsANumber = true;
+var SumAccumulator = module.exports = function SumAccumulator() {
+	if (arguments.length !== 0) throw new Error("zero args expected");
+	this.reset();
 	base.call(this);
-}, klass = SumAccumulator, Accumulator = require("./Accumulator"), base = Accumulator, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
-
-// NOTE: Skipping the create function, using the constructor instead
+}, klass = SumAccumulator, base = require("./Accumulator"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// MEMBER FUNCTIONS
 proto.processInternal = function processInternal(input, merging) {
-	if(typeof input === "number"){ // do nothing with non-numeric types
-		this.totalIsANumber = true;
-		this.total += input;
+	// do nothing with non numeric types
+	if (typeof input !== "number"){
+		if (input !== undefined && input !== null) { //NOTE: DEVIATION FROM MONGO: minor fix for 0-like values
+			this.isNumber = false;
+		}
+		return;
 	}
-	this.count++;
+	this.total += input;
+};
 
-	return 0;
+klass.create = function create() {
+	return new SumAccumulator();
 };
 
-proto.getValue = function getValue(toBeMerged){
-	if (this.totalIsANumber) {
+proto.getValue = function getValue(toBeMerged) {
+	if (this.isNumber) {
 		return this.total;
+	} else {
+		throw new Error("$sum resulted in a non-numeric type; massert code 16000");
 	}
-	throw new Error("$sum resulted in a non-numeric type");
+};
+
+proto.reset = function reset() {
+	this.isNumber = true;
+	this.total = 0;
 };
 
 proto.getOpName = function getOpName(){

+ 68 - 71
test/lib/pipeline/accumulators/AddToSetAccumulator.js

@@ -2,95 +2,92 @@
 var assert = require("assert"),
 	AddToSetAccumulator = require("../../../../lib/pipeline/accumulators/AddToSetAccumulator");
 
-
-var createAccumulator = function createAccumulator() {
-	return new AddToSetAccumulator();
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+var testData = {
+	nil: null,
+	bF: false, bT: true,
+	numI: 123, numF: 123.456,
+	str: "TesT! mmm π",
+	obj: {foo:{bar:"baz"}},
+	arr: [1, 2, 3, [4, 5, 6]],
+	date: new Date(),
+	re: /foo/gi,
 };
 
 //TODO: refactor these test cases using Expression.parseOperand() or something because these could be a whole lot cleaner...
-module.exports = {
+exports.AddToSetAccumulator = {
 
-	"AddToSetAccumulator": {
+	".constructor()": {
 
-		"constructor()": {
+		"should create instance of Accumulator": function() {
+			assert(new AddToSetAccumulator() instanceof AddToSetAccumulator);
+		},
 
-			"should error if called with args": function testArgsGivenToCtor() {
-				assert.throws(function() {
-					new AddToSetAccumulator('arg');
-				});
-			},
+		"should error if called with args": function() {
+			assert.throws(function() {
+				new AddToSetAccumulator(123);
+			});
+		}
 
-			"should construct object with set property": function testCtorAssignsSet() {
-				var acc = new AddToSetAccumulator();
-				assert.notEqual(acc.set, null);
-				assert.notEqual(acc.set, undefined);
-			}
+	},
 
-		},
+	".create()": {
 
-		"#getFactory()": {
+		"should return an instance of the accumulator": function() {
+			assert(AddToSetAccumulator.create() instanceof AddToSetAccumulator);
+		}
+
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new AddToSetAccumulator().getFactory(), AddToSetAccumulator);
-			}
+	"#process()": {
 
+		"should add input to set": function() {
+			var acc = AddToSetAccumulator.create();
+			acc.process(testData);
+			assert.deepEqual(acc.getValue(), [testData]);
 		},
 
-		"#processInternal()" : {
-			"should add input to set": function testAddsToSet() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				var value = acc.getValue();
-				assert.deepEqual(JSON.stringify(value), JSON.stringify([5]));
-			}
+		"should add input iff not already in set": function() {
+			var acc = AddToSetAccumulator.create();
+			acc.process(testData);
+			acc.process(testData);
+			assert.deepEqual(acc.getValue(), [testData]);
+		},
 
+		"should merge input into set": function() {
+			var acc = AddToSetAccumulator.create();
+			acc.process(testData);
+			acc.process([testData, 42], true);
+			assert.deepEqual(acc.getValue(), [42, testData]);
 		},
 
-		"#getValue()": {
-
-			"should return empty array": function testEmptySet() {
-				var acc = new createAccumulator();
-				var value = acc.getValue();
-				assert.equal((value instanceof Array), true);
-				assert.equal(value.length, 0);
-			},
-
-			"should return array with one element that equals 5": function test5InSet() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				acc.processInternal(5);
-				var value = acc.getValue();
-				assert.deepEqual(JSON.stringify(value), JSON.stringify([5]));
-			},
-
-			"should produce value that is an array of multiple elements": function testMultipleItems() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				acc.processInternal({key: "value"});
-				var value = acc.getValue();
-				assert.deepEqual(JSON.stringify(value), JSON.stringify([5, {key: "value"}]));
-			},
-
-			"should return array with one element that is an object containing a key/value pair": function testKeyValue() {
-				var acc = createAccumulator();
-				acc.processInternal({key: "value"});
-				var value = acc.getValue();
-				assert.deepEqual(JSON.stringify(value), JSON.stringify([{key: "value"}]));
-			},
-
-			"should coalesce different instances of equivalent objects": function testGetValue_() {
-				var acc = createAccumulator();
-				acc.processInternal({key: "value"});
-				acc.processInternal({key: "value"});
-				var value = acc.getValue();
-				assert.deepEqual(JSON.stringify(value), JSON.stringify([{key: "value"}]));
-			}
+	},
 
-		}
+	"#getValue()": {
 
-	}
+		"should return empty set initially": function() {
+			var acc = new AddToSetAccumulator.create();
+			var value = acc.getValue();
+			assert.equal((value instanceof Array), true);
+			assert.equal(value.length, 0);
+		},
 
-};
+		"should return set of added items": function() {
+			var acc = AddToSetAccumulator.create(),
+				expected = [
+					42,
+					{foo:1, bar:2},
+					{bar:2, foo:1},
+					testData
+				];
+			expected.forEach(function(input){
+				acc.process(input);
+			});
+			assert.deepEqual(acc.getValue(), expected);
+		},
 
+	}
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+};

+ 192 - 73
test/lib/pipeline/accumulators/AvgAccumulator.js

@@ -2,111 +2,230 @@
 var assert = require("assert"),
 	AvgAccumulator = require("../../../../lib/pipeline/accumulators/AvgAccumulator");
 
-function createAccumulator(){
-	return new AvgAccumulator();
-}
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.AvgAccumulator = {
 
-	"AvgAccumulator": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new AvgAccumulator();
-				});
-			}
+	".constructor()": {
 
+		"should not throw Error when constructing without args": function() {
+			new AvgAccumulator();
 		},
 
-		"#getOpName()": {
+	},
+
+	"#process()": {
 
-			"should return the correct op name; $avg": function testOpName(){
-				assert.strictEqual(new AvgAccumulator().getOpName(), "$avg");
-			}
+		"should allow numbers": function() {
+			assert.doesNotThrow(function() {
+				var acc = AvgAccumulator.create();
+				acc.process(1);
+			});
+		},
 
+		"should ignore non-numbers": function() {
+			assert.doesNotThrow(function() {
+				var acc = AvgAccumulator.create();
+				acc.process(true);
+				acc.process("Foo");
+				acc.process(new Date());
+				acc.process({});
+				acc.process([]);
+			});
 		},
 
-		"#processInternal()": {
+		"router": {
 
-			"should evaluate no documents": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				assert.strictEqual(avgAccumulator.getValue(), 0);
+			"should handle result from one shard": function testOneShard() {
+				var acc = AvgAccumulator.create();
+				acc.process({subTotal:3.0, count:2}, true);
+				assert.deepEqual(acc.getValue(), 3.0 / 2);
 			},
 
-			"should evaluate one document with a field that is NaN": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(Number("foo"));
-				// NaN is unequal to itself
-				assert.notStrictEqual(avgAccumulator.getValue(), avgAccumulator.getValue());
+			"should handle result from two shards": function testTwoShards() {
+				var acc = AvgAccumulator.create();
+				acc.process({subTotal:6.0, count:1}, true);
+				acc.process({subTotal:5.0, count:2}, true);
+				assert.deepEqual(acc.getValue(), 11.0 / 3);
 			},
 
+		},
+
+	},
+
+	".create()": {
+
+		"should create an instance": function() {
+			assert(AvgAccumulator.create() instanceof AvgAccumulator);
+		},
+
+	},
+
+	"#getValue()": {
 
-			"should evaluate one document and avg it's value": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(5);
-				assert.strictEqual(avgAccumulator.getValue(), 5);
+		"should return 0 if no inputs evaluated": function testNoDocsEvaluated() {
+			var acc = AvgAccumulator.create();
+			assert.equal(acc.getValue(), 0);
+		},
+
+		"should return one int": function testOneInt() {
+			var acc = AvgAccumulator.create();
+			acc.process(3);
+			assert.equal(acc.getValue(), 3);
+		},
+
+		"should return one long": function testOneLong() {
+			var acc = AvgAccumulator.create();
+			acc.process(-4e24);
+			assert.equal(acc.getValue(), -4e24);
+		},
+
+		"should return one double": function testOneDouble() {
+			var acc = AvgAccumulator.create();
+			acc.process(22.6);
+			assert.equal(acc.getValue(), 22.6);
+		},
+
+		"should return avg for two ints": function testIntInt() {
+			var acc = AvgAccumulator.create();
+			acc.process(10);
+			acc.process(11);
+			assert.equal(acc.getValue(), 10.5);
+		},
+
+		"should return avg for int and double": function testIntDouble() {
+			var acc = AvgAccumulator.create();
+			acc.process(10);
+			acc.process(11.0);
+			assert.equal(acc.getValue(), 10.5);
+		},
 
+		"should return avg for two ints w/o overflow": function testIntIntNoOverflow() {
+			var acc = AvgAccumulator.create();
+			acc.process(32767);
+			acc.process(32767);
+			assert.equal(acc.getValue(), 32767);
+		},
+
+		"should return avg for two longs w/o overflow": function testLongLongOverflow() {
+			var acc = AvgAccumulator.create();
+			acc.process(2147483647);
+			acc.process(2147483647);
+			assert.equal(acc.getValue(), (2147483647 + 2147483647) / 2);
+		},
+
+		"shard": {
+
+			"should return avg info for int": function testShardInt() {
+				var acc = AvgAccumulator.create();
+				acc.process(3);
+				assert.deepEqual(acc.getValue(true), {subTotal:3.0, count:1});
 			},
 
+			"should return avg info for long": function testShardLong() {
+				var acc = AvgAccumulator.create();
+				acc.process(5);
+				assert.deepEqual(acc.getValue(true), {subTotal:5.0, count:1});
+			},
 
-			"should evaluate and avg two ints": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(5);
-				avgAccumulator.processInternal(7);
-				assert.strictEqual(avgAccumulator.getValue(), 6);
+			"should return avg info for double": function testShardDouble() {
+				var acc = AvgAccumulator.create();
+				acc.process(116.0);
+				assert.deepEqual(acc.getValue(true), {subTotal:116.0, count:1});
 			},
 
-			"should evaluate and avg two ints overflow": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(Number.MAX_VALUE);
-				avgAccumulator.processInternal(Number.MAX_VALUE);
-				assert.strictEqual(Number.isFinite(avgAccumulator.getValue()), false);
+			beforeEach: function() { // used in the tests below
+				this.getAvgValueFor = function(a, b) { // kind of like TwoOperandBase
+					var acc = AvgAccumulator.create();
+					for (var i = 0, l = arguments.length; i < l; i++) {
+						acc.process(arguments[i]);
+					}
+					return acc.getValue(true);
+				};
 			},
 
+			"should return avg info for two ints w/ overflow": function testShardIntIntOverflow() {
+				var operand1 = 32767,
+					operand2 = 3,
+					expected = {subTotal: 32767 + 3.0, count: 2};
+				assert.deepEqual(this.getAvgValueFor(operand1, operand2), expected);
+				assert.deepEqual(this.getAvgValueFor(operand2, operand1), expected);
+			},
 
-			"should evaluate and avg two negative ints": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(-5);
-				avgAccumulator.processInternal(-7);
-				assert.strictEqual(avgAccumulator.getValue(), -6);
+			"should return avg info for int and long": function testShardIntLong() {
+				var operand1 = 5,
+					operand2 = 3e24,
+					expected = {subTotal: 5 + 3e24, count: 2};
+				assert.deepEqual(this.getAvgValueFor(operand1, operand2), expected);
+				assert.deepEqual(this.getAvgValueFor(operand2, operand1), expected);
 			},
 
-//TODO Not sure how to do this in Javascript
-//			"should evaluate and avg two negative ints overflow": function testStuff(){
-//				var avgAccumulator = createAccumulator();
-//				avgAccumulator.processInternal(Number.MIN_VALUE);
-//				avgAccumulator.processInternal(7);
-//				assert.strictEqual(avgAccumulator.getValue(), Number.MAX_VALUE);
-//			},
-//
-
-			"should evaluate and avg int and float": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(8.5);
-				avgAccumulator.processInternal(7);
-				assert.strictEqual(avgAccumulator.getValue(), 7.75);
+			"should return avg info for int and double": function testShardIntDouble() {
+				var operand1 = 5,
+					operand2 = 6.2,
+					expected = {subTotal: 5 + 6.2, count: 2};
+				assert.deepEqual(this.getAvgValueFor(operand1, operand2), expected);
+				assert.deepEqual(this.getAvgValueFor(operand2, operand1), expected);
 			},
 
-			"should evaluate and avg one Number and a NaN sum to NaN": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(8);
-				avgAccumulator.processInternal(Number("bar"));
-				// NaN is unequal to itself
-				assert.notStrictEqual(avgAccumulator.getValue(), avgAccumulator.getValue());
+			"should return avg info for long and double": function testShardLongDouble() {
+				var operand1 = 5e24,
+					operand2 = 1.0,
+					expected = {subTotal: 5e24 + 1.0, count: 2};
+				assert.deepEqual(this.getAvgValueFor(operand1, operand2), expected);
+				assert.deepEqual(this.getAvgValueFor(operand2, operand1), expected);
 			},
 
-			"should evaluate and avg a null value to 0": function testStuff(){
-				var avgAccumulator = createAccumulator();
-				avgAccumulator.processInternal(null);
-				assert.strictEqual(avgAccumulator.getValue(), 0);
-			}
+			"should return avg info for int and long and double": function testShardIntLongDouble() {
+				var operand1 = 1,
+					operand2 = 2e24,
+					operand3 = 4.0,
+					expected = {subTotal: 1 + 2e24 + 4.0, count: 3};
+				assert.deepEqual(this.getAvgValueFor(operand1, operand2, operand3), expected);
+			},
 
+		},
+
+		"should handle NaN": function() {
+			var acc = AvgAccumulator.create();
+			acc.process(NaN);
+			acc.process(1);
+			assert(isNaN(acc.getValue()));
+			acc = AvgAccumulator.create();
+			acc.process(1);
+			acc.process(NaN);
+			assert(isNaN(acc.getValue()));
+		},
+
+		"should handle null as 0": function() {
+			var acc = AvgAccumulator.create();
+			acc.process(null);
+			assert.equal(acc.getValue(), 0);
 		}
 
-	}
+	},
 
-};
+	"#reset()": {
+
+		"should reset to zero": function() {
+			var acc = AvgAccumulator.create();
+			assert.equal(acc.getValue(), 0);
+			acc.process(123);
+			assert.notEqual(acc.getValue(), 0);
+			acc.reset();
+			assert.equal(acc.getValue(), 0);
+			assert.deepEqual(acc.getValue(true), {subTotal:0, count:0});
+		}
+
+	},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+	"#getOpName()": {
+
+		"should return the correct op name; $avg": function() {
+			assert.equal(new AvgAccumulator().getOpName(), "$avg");
+		}
+
+	},
+
+};

+ 77 - 55
test/lib/pipeline/accumulators/FirstAccumulator.js

@@ -2,77 +2,99 @@
 var assert = require("assert"),
 	FirstAccumulator = require("../../../../lib/pipeline/accumulators/FirstAccumulator");
 
-function createAccumulator(){
-	return new FirstAccumulator();
-}
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-module.exports = {
+exports.FirstAccumulator = {
 
-	"FirstAccumulator": {
+	".constructor()": {
 
-		"constructor()": {
+		"should create instance of Accumulator": function() {
+			assert(new FirstAccumulator() instanceof FirstAccumulator);
+		},
+
+		"should throw error if called with args": function() {
+			assert.throws(function() {
+				new FirstAccumulator(123);
+			});
+		},
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new FirstAccumulator();
-				});
-			}
+	},
 
+	".create()": {
+
+		"should return an instance of the accumulator": function() {
+			assert(FirstAccumulator.create() instanceof FirstAccumulator);
 		},
 
-		"#getOpName()": {
+	},
+
+	"#process()": {
 
-			"should return the correct op name; $first": function testOpName(){
-				assert.equal(new FirstAccumulator().getOpName(), "$first");
-			}
+		"should return undefined if no inputs evaluated": function testNone() {
+			var acc = FirstAccumulator.create();
+			assert.strictEqual(acc.getValue(), undefined);
+		},
 
+		"should return value for one input": function testOne() {
+			var acc = FirstAccumulator.create();
+			acc.process(5);
+			assert.strictEqual(acc.getValue(), 5);
 		},
 
-		"#getFactory()": {
+		"should return missing for one missing input": function testMissing() {
+			var acc = FirstAccumulator.create();
+			acc.process(undefined);
+			assert.strictEqual(acc.getValue(), undefined);
+		},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new FirstAccumulator().getFactory(), FirstAccumulator);
-			}
+		"should return first of two inputs": function testTwo() {
+			var acc = FirstAccumulator.create();
+			acc.process(5);
+			acc.process(7);
+			assert.strictEqual(acc.getValue(), 5);
+		},
 
+		"should return first of two inputs (even if first is missing)": function testFirstMissing() {
+			var acc = FirstAccumulator.create();
+			acc.process(undefined);
+			acc.process(7);
+			assert.strictEqual(acc.getValue(), undefined);
 		},
 
-		"#processInternal()": {
-
-			"The accumulator has no value": function none() {
-				// The accumulator returns no value in this case.
-				var acc = createAccumulator();
-				assert.ok(!acc.getValue());
-			},
-
-			"The accumulator uses processInternal on one input and retains its value": function one() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				assert.strictEqual(acc.getValue(), 5);
-			},
-
-			"The accumulator uses processInternal on one input with the field missing and retains undefined": function missing() {
-				var acc = createAccumulator();
-				acc.processInternal();
-				assert.strictEqual(acc.getValue(), undefined);
-			},
-
-			"The accumulator uses processInternal on two inputs and retains the value in the first": function two() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				acc.processInternal(7);
-				assert.strictEqual(acc.getValue(), 5);
-			},
-
-			"The accumulator uses processInternal on two inputs and retains the undefined value in the first": function firstMissing() {
-				var acc = createAccumulator();
-				acc.processInternal();
-				acc.processInternal(7);
-				assert.strictEqual(acc.getValue(), undefined);
-			}
+	},
+
+	"#getValue()": {
+
+		"should get value the same for shard and router": function() {
+			var acc = FirstAccumulator.create();
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
+			acc.process(123);
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
+		},
+
+	},
+
+	"#reset()": {
+
+		"should reset to missing": function() {
+			var acc = FirstAccumulator.create();
+			assert.strictEqual(acc.getValue(), undefined);
+			acc.process(123);
+			assert.notEqual(acc.getValue(), undefined);
+			acc.reset();
+			assert.strictEqual(acc.getValue(), undefined);
+			assert.strictEqual(acc.getValue(true), undefined);
 		}
 
-	}
+	},
 
-};
+	"#getOpName()": {
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"should return the correct op name; $first": function() {
+			assert.equal(new FirstAccumulator().getOpName(), "$first");
+		}
+
+	},
+
+};

+ 71 - 44
test/lib/pipeline/accumulators/LastAccumulator.js

@@ -2,73 +2,100 @@
 var assert = require("assert"),
 	LastAccumulator = require("../../../../lib/pipeline/accumulators/LastAccumulator");
 
-function createAccumulator(){
-	return new LastAccumulator();
-}
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
+exports.LastAccumulator = {
 
-module.exports = {
+	".constructor()": {
 
-	"LastAccumulator": {
+		"should create instance of Accumulator": function() {
+			assert(new LastAccumulator() instanceof LastAccumulator);
+		},
+
+		"should throw error if called with args": function() {
+			assert.throws(function() {
+				new LastAccumulator(123);
+			});
+		},
 
-		"constructor()": {
+	},
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new LastAccumulator();
-				});
-			}
+	".create()": {
 
+		"should return an instance of the accumulator": function() {
+			assert(LastAccumulator.create() instanceof LastAccumulator);
 		},
 
-		"#getOpName()": {
+	},
+
+	"#process()": {
+
+		"should return undefined if no inputs evaluated": function testNone() {
+			var acc = LastAccumulator.create();
+			assert.strictEqual(acc.getValue(), undefined);
+		},
 
-			"should return the correct op name; $last": function testOpName(){
-				assert.strictEqual(new LastAccumulator().getOpName(), "$last");
-			}
+		"should return value for one input": function testOne() {
+			var acc = LastAccumulator.create();
+			acc.process(5);
+			assert.strictEqual(acc.getValue(), 5);
+		},
 
+		"should return missing for one missing input": function testMissing() {
+			var acc = LastAccumulator.create();
+			acc.process(undefined);
+			assert.strictEqual(acc.getValue(), undefined);
 		},
 
-		"#processInternal()": {
+		"should return last of two inputs": function testTwo() {
+			var acc = LastAccumulator.create();
+			acc.process(5);
+			acc.process(7);
+			assert.strictEqual(acc.getValue(), 7);
+		},
 
-			"should evaluate no documents": function testStuff(){
-				var lastAccumulator = createAccumulator();
-				assert.strictEqual(lastAccumulator.getValue(), undefined);
-			},
+		"should return last of two inputs (even if last is missing)": function testFirstMissing() {
+			var acc = LastAccumulator.create();
+			acc.process(7);
+			acc.process(undefined);
+			assert.strictEqual(acc.getValue(), undefined);
+		},
 
+	},
 
-			"should evaluate one document and retains its value": function testStuff(){
-				var lastAccumulator = createAccumulator();
-				lastAccumulator.processInternal(5);
-				assert.strictEqual(lastAccumulator.getValue(), 5);
+	"#getValue()": {
 
-			},
+		"should get value the same for shard and router": function() {
+			var acc = LastAccumulator.create();
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
+			acc.process(123);
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
+		},
 
+	},
 
-			"should evaluate one document with the field missing retains undefined": function testStuff(){
-				var lastAccumulator = createAccumulator();
-				lastAccumulator.processInternal();
-				assert.strictEqual(lastAccumulator.getValue(), undefined);
-			},
+	"#reset()": {
 
+		"should reset to missing": function() {
+			var acc = LastAccumulator.create();
+			assert.strictEqual(acc.getValue(), undefined);
+			acc.process(123);
+			assert.notEqual(acc.getValue(), undefined);
+			acc.reset();
+			assert.strictEqual(acc.getValue(), undefined);
+			assert.strictEqual(acc.getValue(true), undefined);
+		},
 
-			"should evaluate two documents and retains the value in the last": function testStuff(){
-				var lastAccumulator = createAccumulator();
-				lastAccumulator.processInternal(5);
-				lastAccumulator.processInternal(7);
-				assert.strictEqual(lastAccumulator.getValue(), 7);
-			},
+	},
 
+	"#getOpName()": {
 
-			"should evaluate two documents and retains the undefined value in the last": function testStuff(){
-				var lastAccumulator = createAccumulator();
-				lastAccumulator.processInternal(5);
-				lastAccumulator.processInternal();
-				assert.strictEqual(lastAccumulator.getValue(), undefined);
-			}
-		}
+		"should return the correct op name; $last": function() {
+			assert.equal(new LastAccumulator().getOpName(), "$last");
+		},
 
-	}
+	},
 
 };
 

+ 0 - 78
test/lib/pipeline/accumulators/MaxAccumulator.js

@@ -1,78 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	MaxAccumulator = require("../../../../lib/pipeline/accumulators/MinMaxAccumulator");
-
-function createAccumulator(){
-	return MaxAccumulator.createMax();
-}
-
-
-module.exports = {
-
-	"MaxAccumulator": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args using createMax": function testConstructor(){
-				assert.doesNotThrow(function(){
-					MaxAccumulator.createMax();
-				});
-			},
-
-			"should throw Error when constructing without args using default constructor": function testConstructor(){
-				assert.throws(function(){
-					new MaxAccumulator();
-				});
-			}
-
-		},
-
-		"#getOpName()": {
-
-			"should return the correct op name; $max": function testOpName(){
-				var acc = createAccumulator();
-				assert.equal(acc.getOpName(), "$max");
-			}
-
-		},
-
-		"#processInternal()": {
-
-			"The accumulator evaluates no documents": function none() {
-				// The accumulator returns no value in this case.
-				var acc = createAccumulator();
-				assert.ok(!acc.getValue());
-			},
-
-			"The accumulator evaluates one document and retains its value": function one() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				assert.strictEqual(acc.getValue(), 5);
-			},
-
-			"The accumulator evaluates one document with the field missing retains undefined": function missing() {
-				var acc = createAccumulator();
-				acc.processInternal();
-				assert.strictEqual(acc.getValue(), undefined);
-			},
-
-			"The accumulator evaluates two documents and retains the maximum": function two() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				acc.processInternal(7);
-				assert.strictEqual(acc.getValue(), 7);
-			},
-
-			"The accumulator evaluates two documents and retains the defined value in the first": function lastMissing() {
-				var acc = createAccumulator();
-				acc.processInternal(7);
-				acc.processInternal();
-				assert.strictEqual(acc.getValue(), 7);
-			}
-		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 0 - 78
test/lib/pipeline/accumulators/MinAccumulator.js

@@ -1,78 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	MinAccumulator = require("../../../../lib/pipeline/accumulators/MinMaxAccumulator");
-
-function createAccumulator(){
-	return MinAccumulator.createMin();
-}
-
-
-module.exports = {
-
-	"MinAccumulator": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args using createMin": function testConstructor(){
-				assert.doesNotThrow(function(){
-					MinAccumulator.createMin();
-				});
-			},
-
-			"should throw Error when constructing without args using default constructor": function testConstructor(){
-				assert.throws(function(){
-					new MinAccumulator();
-				});
-			}
-
-		},
-
-		"#getOpName()": {
-
-			"should return the correct op name; $min": function testOpName(){
-				var acc = createAccumulator();
-				assert.equal(acc.getOpName(), "$min");
-			}
-
-		},
-
-		"#processInternal()": {
-
-			"The accumulator evaluates no documents": function none() {
-				// The accumulator returns no value in this case.
-				var acc = createAccumulator();
-				assert.ok(!acc.getValue());
-			},
-
-			"The accumulator evaluates one document and retains its value": function one() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				assert.strictEqual(acc.getValue(), 5);
-			},
-
-			"The accumulator evaluates one document with the field missing retains undefined": function missing() {
-				var acc = createAccumulator();
-				acc.processInternal();
-				assert.strictEqual(acc.getValue(), undefined);
-			},
-
-			"The accumulator evaluates two documents and retains the minimum": function two() {
-				var acc = createAccumulator();
-				acc.processInternal(5);
-				acc.processInternal(7);
-				assert.strictEqual(acc.getValue(), 5);
-			},
-
-			"The accumulator evaluates two documents and retains the undefined value in the last": function lastMissing() {
-				var acc = createAccumulator();
-				acc.processInternal(7);
-				acc.processInternal();
-				assert.strictEqual(acc.getValue(), undefined);
-			}
-		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 206 - 0
test/lib/pipeline/accumulators/MinMaxAccumulator.js

@@ -0,0 +1,206 @@
+"use strict";
+var assert = require("assert"),
+	MinMaxAccumulator = require("../../../../lib/pipeline/accumulators/MinMaxAccumulator");
+
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
+
+exports.MinMaxAccumulator = {
+
+	".constructor()": {
+
+		"should create instance of Accumulator": function() {
+			assert(MinMaxAccumulator.createMax() instanceof MinMaxAccumulator);
+		},
+
+		"should throw error if called without args": function() {
+			assert.throws(function() {
+				new MinMaxAccumulator();
+			});
+		},
+
+		"should create instance of Accumulator if called with valid sense": function() {
+			new MinMaxAccumulator(-1);
+			new MinMaxAccumulator(1);
+		},
+
+		"should throw error if called with invalid sense": function() {
+			assert.throws(function() {
+				new MinMaxAccumulator(0);
+			});
+		},
+
+	},
+
+	".createMin()": {
+
+		"should return an instance of the accumulator": function() {
+			var acc = MinMaxAccumulator.createMin();
+			assert(acc instanceof MinMaxAccumulator);
+			assert.strictEqual(acc._sense, 1);
+		},
+
+	},
+
+	".createMax()": {
+
+		"should return an instance of the accumulator": function() {
+			var acc = MinMaxAccumulator.createMax();
+			assert(acc instanceof MinMaxAccumulator);
+			assert.strictEqual(acc._sense, -1);
+		},
+
+	},
+
+	"#process()": {
+
+		"Min": {
+
+			"should return undefined if no inputs evaluated": function testNone() {
+				var acc = MinMaxAccumulator.createMin();
+				assert.strictEqual(acc.getValue(), undefined);
+			},
+
+			"should return value for one input": function testOne() {
+				var acc = MinMaxAccumulator.createMin();
+				acc.process(5);
+				assert.strictEqual(acc.getValue(), 5);
+			},
+
+			"should return missing for one missing input": function testMissing() {
+				var acc = MinMaxAccumulator.createMin();
+				acc.process();
+				assert.strictEqual(acc.getValue(), undefined);
+			},
+
+			"should return minimum of two inputs": function testTwo() {
+				var acc = MinMaxAccumulator.createMin();
+				acc.process(5);
+				acc.process(7);
+				assert.strictEqual(acc.getValue(), 5);
+			},
+
+			"should return minimum of two inputs (ignoring undefined once found)": function testLastMissing() {
+				var acc = MinMaxAccumulator.createMin();
+				acc.process(7);
+				acc.process(undefined);
+				assert.strictEqual(acc.getValue(), 7);
+			},
+
+		},
+
+		"Max": {
+
+			"should return undefined if no inputs evaluated": function testNone() {
+				var acc = MinMaxAccumulator.createMax();
+				assert.strictEqual(acc.getValue(), undefined);
+			},
+
+			"should return value for one input": function testOne() {
+				var acc = MinMaxAccumulator.createMax();
+				acc.process(5);
+				assert.strictEqual(acc.getValue(), 5);
+			},
+
+			"should return missing for one missing input": function testMissing() {
+				var acc = MinMaxAccumulator.createMax();
+				acc.process();
+				assert.strictEqual(acc.getValue(), undefined);
+			},
+
+			"should return maximum of two inputs": function testTwo() {
+				var acc = MinMaxAccumulator.createMax();
+				acc.process(5);
+				acc.process(7);
+				assert.strictEqual(acc.getValue(), 7);
+			},
+
+			"should return maximum of two inputs (ignoring undefined once found)": function testLastMissing() {
+				var acc = MinMaxAccumulator.createMax();
+				acc.process(7);
+				acc.process(undefined);
+				assert.strictEqual(acc.getValue(), 7);
+			},
+
+		},
+
+	},
+
+	"#getValue()": {
+
+		"Min": {
+
+			"should get value the same for shard and router": function() {
+				var acc = MinMaxAccumulator.createMin();
+				assert.strictEqual(acc.getValue(false), acc.getValue(true));
+				acc.process(123);
+				assert.strictEqual(acc.getValue(false), acc.getValue(true));
+			},
+
+		},
+
+		"Max": {
+
+			"should get value the same for shard and router": function() {
+				var acc = MinMaxAccumulator.createMax();
+				assert.strictEqual(acc.getValue(false), acc.getValue(true));
+				acc.process(123);
+				assert.strictEqual(acc.getValue(false), acc.getValue(true));
+			},
+
+		},
+
+	},
+
+	"#reset()": {
+
+		"Min": {
+
+			"should reset to missing": function() {
+				var acc = MinMaxAccumulator.createMin();
+				assert.strictEqual(acc.getValue(), undefined);
+				acc.process(123);
+				assert.notEqual(acc.getValue(), undefined);
+				acc.reset();
+				assert.strictEqual(acc.getValue(), undefined);
+				assert.strictEqual(acc.getValue(true), undefined);
+			},
+
+		},
+
+		"Max": {
+
+			"should reset to missing": function() {
+				var acc = MinMaxAccumulator.createMax();
+				assert.strictEqual(acc.getValue(), undefined);
+				acc.process(123);
+				assert.notEqual(acc.getValue(), undefined);
+				acc.reset();
+				assert.strictEqual(acc.getValue(), undefined);
+				assert.strictEqual(acc.getValue(true), undefined);
+			},
+
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"Min": {
+
+			"should return the correct op name; $min": function() {
+				assert.equal(MinMaxAccumulator.createMin().getOpName(), "$min");
+			},
+
+		},
+		"Max":{
+
+			"should return the correct op name; $max": function() {
+				assert.equal(MinMaxAccumulator.createMax().getOpName(), "$max");
+			},
+
+		},
+
+	},
+
+};

+ 97 - 77
test/lib/pipeline/accumulators/PushAccumulator.js

@@ -2,99 +2,119 @@
 var assert = require("assert"),
 	PushAccumulator = require("../../../../lib/pipeline/accumulators/PushAccumulator");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-function createAccumulator(){
-	return new PushAccumulator();
-}
 
-module.exports = {
+exports.PushAccumulator = {
 
-	"PushAccumulator": {
+	".constructor()": {
 
-		"constructor()": {
+		"should create instance of accumulator": function() {
+			assert(new PushAccumulator() instanceof PushAccumulator);
+		},
+
+		"should throw error if called with args": function() {
+			assert.throws(function() {
+				new PushAccumulator(123);
+			});
+		},
+
+	},
+
+	".create()": {
+
+		"should return an instance of the accumulator": function() {
+			assert(PushAccumulator.create() instanceof PushAccumulator);
+		},
+
+	},
+
+	"#process()": {
+
+		"should return empty array if no inputs evaluated": function() {
+			var acc = PushAccumulator.create();
+			assert.deepEqual(acc.getValue(), []);
+		},
+
+		"should return array of one value for one input": function() {
+			var acc = PushAccumulator.create();
+			acc.process(1);
+			assert.deepEqual(acc.getValue(), [1]);
+		},
+
+		"should return array of two values for two inputs": function() {
+			var acc = PushAccumulator.create();
+			acc.process(1);
+			acc.process(2);
+			assert.deepEqual(acc.getValue(), [1,2]);
+		},
+
+		"should return array of two values for two inputs (including null)": function() {
+			var acc = PushAccumulator.create();
+			acc.process(1);
+			acc.process(null);
+			assert.deepEqual(acc.getValue(), [1, null]);
+		},
+
+		"should return array of one value for two inputs if one is undefined": function() {
+			var acc = PushAccumulator.create();
+			acc.process(1);
+			acc.process(undefined);
+			assert.deepEqual(acc.getValue(), [1]);
+		},
+
+		"should return array of two values from two separate mergeable inputs": function() {
+			var acc = PushAccumulator.create();
+			acc.process([1], true);
+			acc.process([0], true);
+			assert.deepEqual(acc.getValue(), [1, 0]);
+		},
+
+		"should throw error if merging non-array": function() {
+			var acc = PushAccumulator.create();
+			assert.throws(function() {
+				acc.process(0, true);
+			});
+			assert.throws(function() {
+				acc.process("foo", true);
+			});
+		},
+
+	},
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new PushAccumulator();
-				});
-			}
+	"#getValue()": {
 
+		"should get value the same for shard and router": function() {
+			var acc = PushAccumulator.create();
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
+			acc.process(123);
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
 		},
 
-		"#getOpName()": {
+	},
 
-			"should return the correct op name; $push": function testOpName(){
-				assert.strictEqual(new PushAccumulator().getOpName(), "$push");
-			}
+	"#reset()": {
 
+		"should reset to empty array": function() {
+			var acc = PushAccumulator.create();
+			assert.deepEqual(acc.getValue(), []);
+			acc.process(123);
+			assert.notDeepEqual(acc.getValue(), []);
+			acc.reset();
+			assert.deepEqual(acc.getValue(), []);
+			assert.deepEqual(acc.getValue(true), []);
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new PushAccumulator().getFactory(), PushAccumulator);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $push": function(){
+			assert.strictEqual(new PushAccumulator().getOpName(), "$push");
 		},
 
-		"#processInternal()": {
-
-			"should processInternal no documents and return []": function testprocessInternal_None(){
-				var accumulator = createAccumulator();
-				assert.deepEqual(accumulator.getValue(), []);
-			},
-
-			"should processInternal a 1 and return [1]": function testprocessInternal_One(){
-				var accumulator = createAccumulator();
-				accumulator.processInternal(1);
-				assert.deepEqual(accumulator.getValue(), [1]);
-			},
-
-			"should processInternal a 1 and a 2 and return [1,2]": function testprocessInternal_OneTwo(){
-				var accumulator = createAccumulator();
-				accumulator.processInternal(1);
-				accumulator.processInternal(2);
-				assert.deepEqual(accumulator.getValue(), [1,2]);
-			},
-
-			"should processInternal a 1 and a null and return [1,null]": function testprocessInternal_OneNull(){
-				var accumulator = createAccumulator();
-				accumulator.processInternal(1);
-				accumulator.processInternal(null);
-				assert.deepEqual(accumulator.getValue(), [1, null]);
-			},
-
-			"should processInternal a 1 and an undefined and return [1]": function testprocessInternal_OneUndefined(){
-				var accumulator = createAccumulator();
-				accumulator.processInternal(1);
-				accumulator.processInternal(undefined);
-				assert.deepEqual(accumulator.getValue(), [1]);
-			},
-
-			"should processInternal a 1 and a 0 and return [1,0]": function testprocessInternal_OneZero(){
-				var accumulator = createAccumulator();
-				accumulator.processInternal(1);
-				accumulator.processInternal(0);
-				assert.deepEqual(accumulator.getValue(), [1, 0]);
-			},
-
-			"should processInternal a 1 and a [0] and return [1,0]": function testprocessInternal_OneArrayZeroMerging(){
-				var accumulator = createAccumulator();
-				accumulator.processInternal(1);
-				accumulator.processInternal([0], true);
-				assert.deepEqual(accumulator.getValue(), [1, 0]);
-			},
-
-			"should processInternal a 1 and a 0 and throw an error if merging": function testprocessInternal_OneZeroMerging(){
-				var accumulator = createAccumulator();
-				accumulator.processInternal(1);
-				assert.throws(function() {
-					accumulator.processInternal(0, true);
-				});
-			}
-		}
-
-	}
+	},
 
 };
 

+ 240 - 78
test/lib/pipeline/accumulators/SumAccumulator.js

@@ -2,113 +2,275 @@
 var assert = require("assert"),
 	SumAccumulator = require("../../../../lib/pipeline/accumulators/SumAccumulator");
 
+// Mocha one-liner to make these tests self-hosted
+if(!module.parent)return(require.cache[__filename]=null,(new(require("mocha"))({ui:"exports",reporter:"spec",grep:process.env.TEST_GREP})).addFile(__filename).run(process.exit));
 
-function createAccumulator(){
-	return new SumAccumulator();
-}
+exports.SumAccumulator = {
 
+	".constructor()": {
 
-module.exports = {
+		"should create instance of Accumulator": function() {
+			assert(new SumAccumulator() instanceof SumAccumulator);
+		},
+
+		"should throw error if called with args": function() {
+			assert.throws(function() {
+				new SumAccumulator(123);
+			});
+		},
+
+	},
+
+	".create()": {
+
+		"should return an instance of the accumulator": function() {
+			assert(SumAccumulator.create() instanceof SumAccumulator);
+		},
+
+	},
+
+	"#process()": {
+
+		"should return 0 if no inputs evaluated": function testNone() {
+			var acc = SumAccumulator.create();
+			assert.strictEqual(acc.getValue(), 0);
+		},
+
+		"should return value for one int input": function testOneInt() {
+			var acc = SumAccumulator.create();
+			acc.process(5);
+			assert.strictEqual(acc.getValue(), 5);
+		},
+
+		"should return value for one long input": function testOneLong() {
+			var acc = SumAccumulator.create();
+			acc.process(6e24);
+			assert.strictEqual(acc.getValue(), 6e24);
+		},
+
+		"should return value for one large long input": function testOneLargeLong() {
+			var acc = SumAccumulator.create();
+			acc.process(6e42);
+			assert.strictEqual(acc.getValue(), 6e42);
+		},
+
+		"should return value for one double input": function testOneDouble() {
+			var acc = SumAccumulator.create();
+			acc.process(7.0);
+			assert.strictEqual(acc.getValue(), 7.0);
+		},
+
+		"should return value for one fractional double input": function testNanDouble() {
+			var acc = SumAccumulator.create();
+			acc.process(NaN);
+			assert.notEqual(acc.getValue(), acc.getValue()); // NaN is unequal to itself.
+		},
+
+		beforeEach: function() { // used in the tests below
+			this.getSumValueFor = function(first, second) { // kind of like TwoOperandBase
+				var acc = SumAccumulator.create();
+				for (var i = 0, l = arguments.length; i < l; i++) {
+					acc.process(arguments[i]);
+				}
+				return acc.getValue();
+			};
+		},
+
+		"should return sum for two ints": function testIntInt() {
+			var summand1 = 4,
+				summand2 = 5,
+				expected = 9;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
+
+		"should return sum for two ints (overflow)": function testIntIntOverflow() {
+			var summand1 = 32767,
+				summand2 = 10,
+				expected = 32767 + 10;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
+
+		"should return sum for two ints (negative overflow)": function testIntIntNegativeOverflow() {
+			var summand1 = 32767,
+				summand2 = -10,
+				expected = 32767 + -10;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
-	"SumAccumulator": {
+		"should return sum for int and long": function testIntLong() {
+			var summand1 = 4,
+				summand2 = 5e24,
+				expected = 4 + 5e24;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
-		"constructor()": {
+		"should return sum for max int and long (no int overflow)": function testIntLongNoIntOverflow() {
+			var summand1 = 32767,
+				summand2 = 1e24,
+				expected = 32767 + 1e24;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
+
+		"should return sum for int and max long (long overflow)": function testIntLongLongOverflow() {
+			var summand1 = 1,
+				summand2 = 9223372036854775807,
+				expected = 1 + 9223372036854775807;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
+
+		"should return sum for long and long": function testLongLong() {
+			var summand1 = 4e24,
+				summand2 = 5e24,
+				expected = 4e24 + 5e24;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
+
+		"should return sum for max long and max long (overflow)": function testLongLongOverflow() {
+			var summand1 = 9223372036854775807,
+				summand2 = 9223372036854775807,
+				expected = 9223372036854775807 + 9223372036854775807;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
+
+		"should return sum for int and double": function testIntDouble() {
+			var summand1 = 4,
+				summand2 = 5.5,
+				expected = 9.5;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new SumAccumulator();
-				});
-			}
+		"should return sum for int and NaN as NaN": function testIntNanDouble() {
+			var summand1 = 4,
+				summand2 = NaN,
+				expected = NaN;
+			assert(isNaN(this.getSumValueFor(summand1, summand2)));
+			assert(isNaN(this.getSumValueFor(summand2, summand1)));
+		},
 
+		"should return sum for int and double (no int overflow)": function testIntDoubleNoIntOverflow() {
+			var summand1 = 32767,
+				summand2 = 1.0,
+				expected = 32767 + 1.0;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
 		},
 
-		"#getOpName()": {
+		"should return sum for long and double": function testLongDouble() {
+			var summand1 = 4e24,
+				summand2 = 5.5,
+				expected = 4e24 + 5.5;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
-			"should return the correct op name; $sum": function testOpName(){
-				assert.strictEqual(new SumAccumulator().getOpName(), "$sum");
-			}
+		"should return sum for max long and double (no long overflow)": function testLongDoubleNoLongOverflow() {
+			var summand1 = 9223372036854775807,
+				summand2 = 1.0,
+				expected = 9223372036854775807 + 1.0;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
+		"should return sum for double and double": function testDoubleDouble() {
+			var summand1 = 2.5,
+				summand2 = 5.5,
+				expected = 8.0;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
 		},
 
-		"#processInternal()": {
+		"should return sum for double and double (overflow)": function testDoubleDoubleOverflow() {
+			var summand1 = Number.MAX_VALUE,
+				summand2 = Number.MAX_VALUE,
+				expected = Infinity;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
-			"should evaluate no documents": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				assert.strictEqual(sumAccumulator.getValue(), 0);
-			},
+		"should return sum for int and long and double": function testIntLongDouble() {
+			assert.strictEqual(this.getSumValueFor(5, 99, 0.2), 104.2);
+		},
 
-			"should evaluate one document with a field that is NaN": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(Number("foo"));
-				// NaN is unequal to itself
-				assert.notStrictEqual(sumAccumulator.getValue(), sumAccumulator.getValue());
-			},
+		"should return sum for a negative value": function testNegative() {
+			var summand1 = 5,
+				summand2 = -8.8,
+				expected = 5 - 8.8;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
+		"should return sum for long and negative int": function testLongIntNegative() {
+			var summand1 = 5e24,
+				summand2 = -6,
+				expected = 5e24 - 6;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
-			"should evaluate one document and sum it's value": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(5);
-				assert.strictEqual(sumAccumulator.getValue(), 5);
+		"should return sum for int and null": function testIntNull() {
+			var summand1 = 5,
+				summand2 = null,
+				expected = 5;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
-			},
+		"should return sum for int and undefined": function testIntUndefined() {
+			var summand1 = 9,
+				summand2, // = undefined,
+				expected = 9;
+			assert.strictEqual(this.getSumValueFor(summand1, summand2), expected);
+			assert.strictEqual(this.getSumValueFor(summand2, summand1), expected);
+		},
 
+		"should return sum for long long max and long long max and 1": function testNoOverflowBeforeDouble() {
+			var actual = this.getSumValueFor(9223372036854775807, 9223372036854775807, 1.0),
+				expected = 9223372036854775807 + 9223372036854775807;
+			assert.strictEqual(actual, expected);
+		},
 
-			"should evaluate and sum two ints": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(5);
-				sumAccumulator.processInternal(7);
-				assert.strictEqual(sumAccumulator.getValue(), 12);
-			},
+	},
 
-			"should evaluate and sum two ints overflow": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(Number.MAX_VALUE);
-				sumAccumulator.processInternal(Number.MAX_VALUE);
-				assert.strictEqual(Number.isFinite(sumAccumulator.getValue()), false);
-			},
+	"#getValue()": {
 
+		"should get value the same for shard and router": function() {
+			var acc = SumAccumulator.create();
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
+			acc.process(123);
+			assert.strictEqual(acc.getValue(false), acc.getValue(true));
+		},
 
-			"should evaluate and sum two negative ints": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(-5);
-				sumAccumulator.processInternal(-7);
-				assert.strictEqual(sumAccumulator.getValue(), -12);
-			},
+	},
 
-//TODO Not sure how to do this in Javascript
-//			"should evaluate and sum two negative ints overflow": function testStuff(){
-//				var sumAccumulator = createAccumulator();
-//				sumAccumulator.processInternal({b:Number.MIN_VALUE});
-//				sumAccumulator.processInternal({b:7});
-//				assert.strictEqual(sumAccumulator.getValue(), Number.MAX_VALUE);
-//			},
-//
+	"#reset()": {
 
-			"should evaluate and sum int and float": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(8.5);
-				sumAccumulator.processInternal(7);
-				assert.strictEqual(sumAccumulator.getValue(), 15.5);
-			},
+		"should reset to 0": function() {
+			var acc = SumAccumulator.create();
+			assert.strictEqual(acc.getValue(), 0);
+			acc.process(123);
+			assert.notEqual(acc.getValue(), 0);
+			acc.reset();
+			assert.strictEqual(acc.getValue(), 0);
+			assert.strictEqual(acc.getValue(true), 0);
+		},
 
-			"should evaluate and sum one Number and a NaN sum to NaN": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(8);
-				sumAccumulator.processInternal(Number("bar"));
-				// NaN is unequal to itself
-				assert.notStrictEqual(sumAccumulator.getValue(), sumAccumulator.getValue());
-			},
+	},
 
-			"should evaluate and sum a null value to 0": function testStuff(){
-				var sumAccumulator = createAccumulator();
-				sumAccumulator.processInternal(null);
-				assert.strictEqual(sumAccumulator.getValue(), 0);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $sum": function() {
+			assert.equal(SumAccumulator.create().getOpName(), "$sum");
 		}
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);