浏览代码

Merge branch 'feature/mongo_2.6.5_expressions' into feature/mongo_2.6.5_documentSource

Patrick Rigney 11 年之前
父节点
当前提交
05df538d9e
共有 52 个文件被更改,包括 4557 次插入2484 次删除
  1. 115 101
      lib/pipeline/Value.js
  2. 88 0
      lib/pipeline/ValueSet.js
  3. 33 41
      lib/pipeline/accumulators/Accumulator.js
  4. 27 36
      lib/pipeline/accumulators/AddToSetAccumulator.js
  5. 29 33
      lib/pipeline/accumulators/AvgAccumulator.js
  6. 10 18
      lib/pipeline/accumulators/FirstAccumulator.js
  7. 16 13
      lib/pipeline/accumulators/LastAccumulator.js
  8. 25 31
      lib/pipeline/accumulators/MinMaxAccumulator.js
  9. 20 25
      lib/pipeline/accumulators/PushAccumulator.js
  10. 25 18
      lib/pipeline/accumulators/SumAccumulator.js
  11. 10 32
      lib/pipeline/expressions/AllElementsTrueExpression.js
  12. 6 4
      lib/pipeline/expressions/AndExpression.js
  13. 12 29
      lib/pipeline/expressions/AnyElementTrueExpression.js
  14. 25 24
      lib/pipeline/expressions/ConcatExpression.js
  15. 6 12
      lib/pipeline/expressions/Expression.js
  16. 5 4
      lib/pipeline/expressions/FieldPathExpression.js
  17. 62 59
      lib/pipeline/expressions/MapExpression.js
  18. 30 23
      lib/pipeline/expressions/MultiplyExpression.js
  19. 33 33
      lib/pipeline/expressions/SetDifferenceExpression.js
  20. 33 20
      lib/pipeline/expressions/SetEqualsExpression.js
  21. 43 22
      lib/pipeline/expressions/SetIntersectionExpression.js
  22. 69 61
      lib/pipeline/expressions/SetIsSubsetExpression.js
  23. 25 27
      lib/pipeline/expressions/SetUnionExpression.js
  24. 9 17
      lib/pipeline/expressions/ToLowerExpression.js
  25. 9 17
      lib/pipeline/expressions/ToUpperExpression.js
  26. 94 74
      lib/pipeline/expressions/Variables.js
  27. 8 8
      lib/pipeline/expressions/VariablesIdGenerator.js
  28. 26 23
      lib/pipeline/expressions/VariablesParseState.js
  29. 68 71
      test/lib/pipeline/accumulators/AddToSetAccumulator.js
  30. 192 73
      test/lib/pipeline/accumulators/AvgAccumulator.js
  31. 77 55
      test/lib/pipeline/accumulators/FirstAccumulator.js
  32. 71 44
      test/lib/pipeline/accumulators/LastAccumulator.js
  33. 0 78
      test/lib/pipeline/accumulators/MaxAccumulator.js
  34. 0 78
      test/lib/pipeline/accumulators/MinAccumulator.js
  35. 206 0
      test/lib/pipeline/accumulators/MinMaxAccumulator.js
  36. 97 77
      test/lib/pipeline/accumulators/PushAccumulator.js
  37. 240 78
      test/lib/pipeline/accumulators/SumAccumulator.js
  38. 142 107
      test/lib/pipeline/expressions/AllElementsTrueExpression.js
  39. 99 27
      test/lib/pipeline/expressions/AndExpression_test.js
  40. 142 96
      test/lib/pipeline/expressions/AnyElementTrueExpression.js
  41. 63 43
      test/lib/pipeline/expressions/ConcatExpression_test.js
  42. 115 0
      test/lib/pipeline/expressions/MapExpression_test.js
  43. 91 30
      test/lib/pipeline/expressions/MultiplyExpression_test.js
  44. 321 83
      test/lib/pipeline/expressions/SetDifferenceExpression.js
  45. 321 83
      test/lib/pipeline/expressions/SetEqualsExpression.js
  46. 59 0
      test/lib/pipeline/expressions/SetExpectedResultBase.js
  47. 319 111
      test/lib/pipeline/expressions/SetIntersectionExpression.js
  48. 321 94
      test/lib/pipeline/expressions/SetIsSubsetExpression.js
  49. 319 113
      test/lib/pipeline/expressions/SetUnionExpression.js
  50. 74 46
      test/lib/pipeline/expressions/ToLowerExpression_test.js
  51. 85 57
      test/lib/pipeline/expressions/ToUpperExpression_test.js
  52. 242 235
      test/lib/pipeline/expressions/Variables.js

+ 115 - 101
lib/pipeline/Value.js

@@ -8,10 +8,10 @@
  * @constructor
  **/
 var Value = module.exports = function Value(){
-	if(this.constructor == Value) throw new Error("Never create instances of this! Use the static helpers only.");
-}, klass = Value, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+	if(this.constructor === Value) throw new Error("Never create instances of this! Use the static helpers only.");
+}, klass = Value;
 
-var Document;  // loaded lazily below //TODO: a dirty hack; need to investigate and clean up
+var Document; // loaded lazily below //TODO: a dirty hack; need to investigate and clean up
 
 //SKIPPED: ValueStorage -- probably not required; use JSON?
 //SKIPPED: createIntOrLong -- not required; use Number
@@ -22,18 +22,19 @@ var Document;  // loaded lazily below //TODO: a dirty hack; need to investigate
 //SKIPPED: addToBsonArray -- not required; use arr.push(<val>)
 
 /** Coerce a value to a bool using BSONElement::trueValue() rules.
- * Some types unsupported.  SERVER-6120
+ * Some types unsupported. SERVER-6120
  * @method coerceToBool
  * @static
  */
 klass.coerceToBool = function coerceToBool(value) {
-	if (typeof(value) == "string") return true;
+	if (typeof value === "string") return true;
 	return !!value;	// including null or undefined
 };
 
-/** Coercion operators to extract values with fuzzy type logic.
- *  These currently assert if called on an unconvertible type.
- *  TODO: decided how to handle unsupported types.
+/**
+ * Coercion operators to extract values with fuzzy type logic.
+ * These currently assert if called on an unconvertible type.
+ * TODO: decided how to handle unsupported types.
  */
 klass.coerceToWholeNumber = function coerceToInt(value) {
 	return klass.coerceToNumber(value) | 0;
@@ -42,36 +43,29 @@ klass.coerceToInt = klass.coerceToWholeNumber;
 klass.coerceToLong = klass.coerceToWholeNumber;
 klass.coerceToNumber = function coerceToNumber(value) {
 	if (value === null) return 0;
-	switch (typeof(value)) {
-	case "undefined":
-		return 0;
-	case "number":
-		return value;
-	case "object":
-		switch (value.constructor.name) {
-			case "Long":
-				return parseInt(value.toString(), 10);
-			case "Double":
-				return parseFloat(value.value, 10);
-			default:
-				throw new Error("can't convert from BSON type " + value.constructor.name + " to int; codes 16003, 16004, 16005");
-		}
-		return value;
-	default:
-		throw new Error("can't convert from BSON type " + typeof(value) + " to int; codes 16003, 16004, 16005");
+	switch (Value.getType(value)) {
+		case "undefined":
+			return 0;
+		case "number":
+			return value;
+		case "Long":
+			return parseInt(value.toString(), 10);
+		case "Double":
+			return parseFloat(value.value, 10);
+		default:
+			throw new Error("can't convert from BSON type " + Value.getType(value) + " to int; codes 16003, 16004, 16005");
 	}
 };
 klass.coerceToDouble = klass.coerceToNumber;
 klass.coerceToDate = function coerceToDate(value) {
 	if (value instanceof Date) return value;
-	throw new Error("can't convert from BSON type " + typeof(value) + " to Date; uassert code 16006");
+	throw new Error("can't convert from BSON type " + Value.getType(value) + " to Date; uassert code 16006");
 };
 //SKIPPED: coerceToTimeT -- not required; just use Date
 //SKIPPED: coerceToTm -- not required; just use Date
 //SKIPPED: tmToISODateString -- not required; just use Date
 klass.coerceToString = function coerceToString(value) {
-	var type = typeof(value);
-	if (type == "object") type = value === null ? "null" : value.constructor.name;
+	var type = Value.getType(value);
 	switch (type) {
 		//TODO: BSON numbers?
 		case "number":
@@ -91,7 +85,7 @@ klass.coerceToString = function coerceToString(value) {
 			return "";
 
 		default:
-			throw new Error("can't convert from BSON type " + typeof(value) + " to String; uassert code 16007");
+			throw new Error("can't convert from BSON type " + Value.getType(value) + " to String; uassert code 16007");
 	}
 };
 //SKIPPED: coerceToTimestamp
@@ -101,8 +95,16 @@ klass.coerceToString = function coerceToString(value) {
  * @method cmp
  * @static
  */
-klass.cmp = function cmp(l, r){
-	return l < r ? -1 : l > r ? 1 : 0;
+var cmp = klass.cmp = function cmp(left, right){
+	// The following is lifted directly from compareElementValues
+	// to ensure identical handling of NaN
+	if (left < right)
+		return -1;
+	if (left === right)
+		return 0;
+	if (isNaN(left))
+		return isNaN(right) ? 0 : -1;
+	return 1;
 };
 
 /** Compare two Values.
@@ -112,74 +114,84 @@ klass.cmp = function cmp(l, r){
  * Warning: may return values other than -1, 0, or 1
  */
 klass.compare = function compare(l, r) {
-	//NOTE: deviation from mongo code: we have to do some coercing for null "types" because of javascript
-	var lt = l === null ? "null" : typeof(l),
-		rt = r === null ? "null" : typeof(r),
+	var lType = Value.getType(l),
+		rType = Value.getType(r),
 		ret;
 
-	// NOTE: deviation from mongo code: javascript types do not work quite the same, so for proper results we always canonicalize, and we don't need the "speed" hack
-	ret = (klass.cmp(klass.canonicalize(l), klass.canonicalize(r)));
+	ret = lType === rType ?
+	 	0 // fast-path common case
+		: cmp(klass.canonicalize(l), klass.canonicalize(r));
 
-	if(ret !== 0) return ret;
+	if(ret !== 0)
+		return ret;
 
-	// Numbers
-	if (lt === "number" && rt === "number"){
-		//NOTE: deviation from Mongo code: they handle NaN a bit differently
-		if (isNaN(l)) return isNaN(r) ? 0 : -1;
-		if (isNaN(r)) return 1;
-		return klass.cmp(l,r);
-	}
-	// Compare MinKey and MaxKey cases
-	if (l instanceof Object && ["MinKey", "MaxKey"].indexOf(l.constructor.name) !== -1) {
-		if (l.constructor.name == r.constructor.name) {
-			return 0;
-		} else if (l.constructor.name === "MinKey") {
-			return -1;
-		} else {
-			return 1; // Must be MaxKey, which is greater than everything but MaxKey (which r cannot be)
-		}
-	}
-	// hack: These should really get converted to their BSON type ids and then compared, we use int vs object in queries
-	if (lt === "number" && rt === "object"){
-		return -1;
-	} else if (lt === "object" && rt === "number") {
-		return 1;
-	}
 	// CW TODO for now, only compare like values
-	if (lt !== rt) throw new Error("can't compare values of BSON types [" + lt + " " + l.constructor.name + "] and [" + rt + ":" + r.constructor.name + "]; code 16016");
-	// Compare everything else
-	switch (lt) {
-	case "number":
-		throw new Error("number types should have been handled earlier!");
-	case "string":
-		return klass.cmp(l, r);
-	case "boolean":
-		return l == r ? 0 : l ? 1 : -1;
-	case "undefined": //NOTE: deviation from mongo code: we are comparing null to null or undefined to undefined (otherwise the ret stuff above would have caught it)
-	case "null":
-		return 0;
-	case "object":
-		if (l instanceof Array) {
-			for (var i = 0, ll = l.length, rl = r.length; true ; ++i) {
-				if (i > ll) {
-					if (i > rl) return 0; // arrays are same length
-					return -1; // left array is shorter
-				}
-				if (i > rl) return 1; // right array is shorter
-				var cmp = Value.compare(l[i], r[i]);
-				if (cmp !== 0) return cmp;
+	if (lType !== rType)
+		throw new Error("can't compare values of BSON types [" + lType + "] and [" + rType + "]; code 16016");
+
+	switch (lType) {
+		// Order of types is the same as in compareElementValues() to make it easier to verify
+
+		// These are valueless types
+		//SKIPPED: case "EOO":
+		case "undefined":
+		case "null":
+		//SKIPPED: case "jstNULL":
+		case "MaxKey":
+		case "MinKey":
+			return ret;
+
+		case "boolean":
+			return l - r;
+
+		// WARNING: Timestamp and Date have same canonical type, but compare differently.
+		// Maintaining behavior from normal BSON.
+		//SKIPPED: case "Timestamp": //unsigned-----//TODO: handle case for bson.Timestamp()
+		case "Date": // signed
+			return cmp(l.getTime(), r.getTime());
+
+        // Numbers should compare by equivalence even if different types
+		case "number":
+			return cmp(l, r);
+
+        //SKIPPED: case "jstOID":----//TODO: handle case for bson.ObjectID()
+
+        case "Code":
+        case "Symbol":
+        case "string":
+			l = String(l);
+			r = String(r);
+			return l < r ? -1 : l > r ? 1 : 0;
+
+		case "Object":
+			if (Document === undefined) Document = require("./Document");	//TODO: a dirty hack; need to investigate and clean up
+			return Document.compare(l, r);
+
+		case "Array":
+			var lArr = l,
+				rArr = r;
+
+			var elems = Math.min(lArr.length, rArr.length);
+			for (var i = 0; i < elems; i++) {
+				// compare the two corresponding elements
+				ret = Value.compare(lArr[i], rArr[i]);
+				if (ret !== 0)
+					return ret;
 			}
+			// if we get here we are either equal or one is prefix of the other
+			return cmp(lArr.length, rArr.length);
 
-			throw new Error("logic error in Value.compare for Array types!");
-		}
-		if (l instanceof Date) return klass.cmp(l,r);
-		if (l instanceof RegExp) return klass.cmp(l,r);
-		if (Document === undefined) Document = require("./Document");	//TODO: a dirty hack; need to investigate and clean up
-		return Document.compare(l, r);
-	default:
-		throw new Error("unhandled left hand type:" + lt);
-	}
+		//SKIPPED: case "DBRef":-----//TODO: handle case for bson.DBRef()
+		//SKIPPED: case "BinData":-----//TODO: handle case for bson.BinData()
 
+		case "RegExp": // same as String in this impl but keeping order same as compareElementValues
+			l = String(l);
+			r = String(r);
+			return l < r ? -1 : l > r ? 1 : 0;
+
+		//SKIPPED: case "CodeWScope":-----//TODO: handle case for bson.CodeWScope()
+	}
+	throw new Error("Assertion failure");
 };
 
 //SKIPPED: hash_combine
@@ -201,22 +213,25 @@ klass.consume = function consume(consumed) {
 };
 
 //NOTE: DEVIATION FROM MONGO: many of these do not apply or are inlined (code where relevant)
-// missing(val):  val == undefined
-// nullish(val):  val == null || val == undefined
-// numeric(val):  typeof val == "number"
+// missing(val): val === undefined
+// nullish(val): val === null || val === undefined
+// numeric(val): typeof val === "number"
 klass.getType = function getType(v) {
 	var t = typeof v;
-	if (t == "object") t = (v === null ? "null" : v.constructor.name || t);
-	return t;
+	if (t !== "object")
+		return t;
+	if (v === null)
+		return "null";
+	return v.constructor.name || t;
 };
 // getArrayLength(arr): arr.length
-// getString(val): val.toString()   //NOTE: same for getStringData(val) I think
+// getString(val): val.toString() //NOTE: same for getStringData(val) I think
 // getOid
 // getBool
 // getDate
 // getTimestamp
-// getRegex(re):  re.source
-// getRegexFlags(re):  re.toString().slice(-re.toString().lastIndexOf('/') + 2)
+// getRegex(re): re.source
+// getRegexFlags(re): re.toString().slice(-re.toString().lastIndexOf('/') + 2)
 // getSymbol
 // getCode
 // getInt
@@ -225,8 +240,7 @@ klass.getType = function getType(v) {
 
 // from bsontypes
 klass.canonicalize = function canonicalize(x) {
-	var xType = typeof(x);
-	if (xType == "object") xType = x === null ? "null" : x.constructor.name;
+	var xType = Value.getType(x);
 	switch (xType) {
 		case "MinKey":
 			return -1;

+ 88 - 0
lib/pipeline/ValueSet.js

@@ -0,0 +1,88 @@
+"use strict";
+
+/**
+ * A set of values (i.e., `typedef unordered_set<Value, Value::Hash> ValueSet;`)
+ * @class ValueSet
+ * @namespace mungedb-aggregate.pipeline.expressions
+ * @module mungedb-aggregate
+ * @constructor
+ */
+var ValueSet = module.exports = function ValueSet(vals) {
+	this.set = {};
+	if (vals instanceof Array)
+		this.insertRange(vals);
+}, klass = ValueSet, proto = klass.prototype;
+
+proto._getKey = JSON.stringify;
+
+proto.hasKey = function hasKey(key) {
+	return key in this.set;
+};
+//SKIPPED: proto.count -- use hasKey instead
+
+proto.has = function has(val) {
+	return this._getKey(val) in this.set;
+};
+
+proto.insert = function insert(val) {
+	var valKey = this._getKey(val);
+	if (!this.hasKey(valKey)) {
+		this.set[valKey] = val;
+		return valKey;
+	}
+	return undefined;
+};
+
+proto.insertRange = function insertRange(vals) {
+	var results = [];
+	for (var i = 0, l = vals.length; i < l; i++)
+		results.push(this.insert(vals[i]));
+	return results;
+};
+
+proto.equals = function equals(other) {
+	for (var key in this.set) {
+		if (!other.hasKey(key))
+			return false;
+	}
+	for (var otherKey in other.set) {
+		if (!this.hasKey(otherKey))
+			return false;
+	}
+	return true;
+};
+
+proto.values = function values() {
+	var vals = [];
+	for (var key in this.set)
+		vals.push(this.set[key]);
+	return vals;
+};
+
+proto.size = function values() {
+	var n = 0;
+	for (var key in this.set) //jshint ignore:line
+		n++;
+	return n;
+};
+
+proto.swap = function swap(other) {
+	var tmp = this.set;
+	this.set = other.set;
+	other.set = tmp;
+};
+
+proto.eraseKey = function eraseKey(key) {
+	delete this.set[key];
+};
+
+proto.erase = function erase(val) {
+	var key = this._getKey(val);
+	this.eraseKey(key);
+};
+
+proto.empty = function empty() {
+	for (var key in this.set) //jshint ignore:line
+		return false;
+	return true;
+};

+ 33 - 41
lib/pipeline/accumulators/Accumulator.js

@@ -1,8 +1,7 @@
 "use strict";
 
 /**
- * A base class for all pipeline accumulators. Uses NaryExpression as a base class.
- *
+ * A base class for all pipeline accumulators.
  * @class Accumulator
  * @namespace mungedb-aggregate.pipeline.accumulators
  * @module mungedb-aggregate
@@ -10,66 +9,59 @@
  **/
 var Accumulator = module.exports = function Accumulator(){
 	if (arguments.length !== 0) throw new Error("zero args expected");
-	this._memUsageBytes = 0;
 	base.call(this);
 }, klass = Accumulator, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
-// var Value = require("../Value"),
-
-proto.memUsageForSorter = function memUsageForSorter() {
-	return this._memUsageBytes;
-};
-
-proto.getFactory = function getFactory(){
-	return klass;	// using the ctor rather than a separate .create() method
-};
-
 /** Process input and update internal state.
- * merging should be true when processing outputs from getValue(true).
+ *  merging should be true when processing outputs from getValue(true).
+ *  @method process
+ *  @param input {Value}
+ *  @param merging {Boolean}
  */
-proto.process = function process(input, merging){
+proto.process = function process(input, merging) {
 	this.processInternal(input, merging);
 };
 
-proto.toJSON = function toJSON(isExpressionRequired){
-	var rep = {};
-	rep[this.getOpName()] = this.operands[0].toJSON(isExpressionRequired);
-	return rep;
+/** Marks the end of the evaluate() phase and return accumulated result.
+ *  toBeMerged should be true when the outputs will be merged by process().
+ *  @method getValue
+ *  @param toBeMerged {Boolean}
+ *  @return {Value}
+ */
+proto.getValue = function getValue(toBeMerged) {
+	throw new Error("You need to define this function on your accumulator");
 };
 
 /**
- * If this function is not overridden in the sub classes,
- * then throw an error
- *
- **/
+ * The name of the op as used in a serialization of the pipeline.
+ * @method getOpName
+ * @return {String}
+ */
 proto.getOpName = function getOpName() {
 	throw new Error("You need to define this function on your accumulator");
 };
 
+//NOTE: DEVIATION FROM MONGO: not implementing this
+//int memUsageForSorter() const {}
+
 /**
- * If this function is not overridden in the sub classes,
- * then throw an error
- *
- **/
-proto.getValue = function getValue(toBeMerged) {
+ * Reset this accumulator to a fresh state ready to receive input.
+ * @method reset
+ */
+proto.reset = function reset() {
 	throw new Error("You need to define this function on your accumulator");
 };
 
 /**
- * If this function is not overridden in the sub classes,
- * then throw an error
- *
- **/
+ * Update subclass's internal state based on input
+ * @method processInternal
+ * @param input {Value}
+ * @param merging {Boolean}
+ */
 proto.processInternal = function processInternal(input, merging) {
 	throw new Error("You need to define this function on your accumulator");
 };
 
-/**
- * If this function is not overridden in the sub classes,
- * then throw an error
- *
- **/
-proto.reset = function reset() {
-	throw new Error("You need to define this function on your accumulator");
-};
+//NOTE: DEVIATION FROM MONGO: not implementing this
+// /// subclasses are expected to update this as necessary
+// int _memUsageBytes;

+ 27 - 36
lib/pipeline/accumulators/AddToSetAccumulator.js

@@ -6,54 +6,45 @@
  * @namespace mungedb-aggregate.pipeline.accumulators
  * @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;
-};
+var ValueSet = require("../ValueSet");
 
 proto.processInternal = function processInternal(input, merging) {
-	if (! this.contains(input)) {
-		this.set.push(input);
+	if (!merging) {
+		if (input !== undefined) {
+			this.set.insert(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.insert(input[i]);
+		}
 	}
 };
 
-proto.getValue = function getValue(toBeMerged) {
-	return this.set;
+proto.getValue = function getValue(toBeMerged) { //jshint ignore:line
+	return this.set.values();
 };
 
 proto.reset = function reset() {
-	this.set = [];
+	this.set = new ValueSet();
 };
 
+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(){

+ 10 - 32
lib/pipeline/expressions/AllElementsTrueExpression.js

@@ -6,50 +6,28 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var AllElementsTrueExpression = module.exports = function AllElementsTrueExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-},
-	klass = AllElementsTrueExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = AllElementsTrueExpression, base = require("./FixedArityExpressionT")(AllElementsTrueExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	CoerceToBoolExpression = require("./CoerceToBoolExpression"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$allElementsTrue";
-};
-
-/**
- * Takes an array of one or more numbers and returns true if all.
- * @method @evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var arr = this.operands[0].evaluateInternal(vars);
-
-	if (!(arr instanceof Array)) throw new Error("uassert 17040: argument of " + this.getOpName() + "  must be an array, but is " + typeof arr);
-
-	//Deviation from mongo. This is needed so that empty array is handled properly.
-	if (arr.length === 0){
-		return false;
-	}
-
-	for (var i = 0, n = arr.length; i < n; ++i) {
-		if (! Value.coerceToBool(arr[i])){
+	if (!(arr instanceof Array)) throw new Error(this.getOpName() + "'s argument must be an array, but is " + Value.getType(arr) + "; uassert code 17040");
+	for (var i = 0, l = arr.length; i < l; ++i) {
+		if (!Value.coerceToBool(arr[i])) {
 			return false;
 		}
 	}
 	return true;
 };
 
-/** Register Expression */
 Expression.registerExpression("$allElementsTrue", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$allElementsTrue";
+};

+ 6 - 4
lib/pipeline/expressions/AndExpression.js

@@ -12,9 +12,9 @@
  * @constructor
  **/
 var AndExpression = module.exports = function AndExpression() {
-//	if (arguments.length !== 0) throw new Error("zero args expected");
+	if (arguments.length !== 0) throw new Error("zero args expected");
 	base.call(this);
-}, klass = AndExpression, base = require("./VariadicExpressionT")(AndExpression), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
+}, klass = AndExpression, base = require("./VariadicExpressionT")(klass), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
 // DEPENDENCIES
 var Value = require("../Value"),
@@ -35,11 +35,13 @@ proto.getOpName = function getOpName() {
 proto.evaluateInternal = function evaluateInternal(vars) {
 	for (var i = 0, n = this.operands.length; i < n; ++i) {
 		var value = this.operands[i].evaluateInternal(vars);
-		if (!Value.coerceToBool()) return false;
+		if (!Value.coerceToBool(value)) return false;
 	}
 	return true;
 };
 
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() { return true; };
+
 proto.optimize = function optimize() {
 	var expr = base.prototype.optimize.call(this); //optimize the conjunction as much as possible
 
@@ -55,7 +57,7 @@ proto.optimize = function optimize() {
 	if (!(lastExpr instanceof ConstantExpression)) return expr;
 
 	// Evaluate and coerce the last argument to a boolean.  If it's false, then we can replace this entire expression.
-	var last = Value.coerceToBool(lastExpr.evaluate());
+	var last = Value.coerceToBool(lastExpr.evaluateInternal());
 	if (!last) return new ConstantExpression(false);
 
 	// If we got here, the final operand was true, so we don't need it anymore.

+ 12 - 29
lib/pipeline/expressions/AnyElementTrueExpression.js

@@ -6,45 +6,28 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var AnyElementTrueExpression = module.exports = function AnyElementTrueExpression(){
+ */
+var AnyElementTrueExpression = module.exports = function AnyElementTrueExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-},
-	klass = AnyElementTrueExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype,{
-		constructor:{
-			value:klass
-		}
-	});
+}, klass = AnyElementTrueExpression, base = require("./FixedArityExpressionT")(AnyElementTrueExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-	return "$anyElementTrue";
-};
-
-/**
- * Takes an array of one or more numbers and returns true if any.
- * @method @evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var arr = this.operands[0].evaluateInternal(vars);
-	if (!(arr instanceof Array)) {
-		throw new Error("uassert 17041: $anyElementTrue's " +
-						"argument must be an array, but is " +
-						typeof arr);
-	}
-	for (var i=0, n=arr.length; i<n; ++i) {
-		if (Value.coerceToBool(arr[i]))
+	if (!(arr instanceof Array)) throw new Error(this.getOpName() + "'s argument must be an array, but is " + Value.getType(arr) + "; uassert code 17041");
+	for (var i = 0, l = arr.length; i < l; ++i) {
+		if (Value.coerceToBool(arr[i])) {
 			return true;
+		}
 	}
 	return false;
 };
 
-/** Register Expression */
 Expression.registerExpression("$anyElementTrue",base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$anyElementTrue";
+};

+ 25 - 24
lib/pipeline/expressions/ConcatExpression.js

@@ -1,41 +1,42 @@
 "use strict";
 
-var Expression = require("./Expression");
-
 /**
  * Creates an expression that concatenates a set of string operands.
  * @class ConcatExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var ConcatExpression = module.exports = function ConcatExpression(){
 	if (arguments.length !== 0) throw new Error("zero args expected");
 	base.call(this);
 }, klass = ConcatExpression, base = require("./VariadicExpressionT")(ConcatExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
-var Value = require("../Value");
-var Expression = require("./Expression");
-
-// PROTOTYPE MEMBERS
-klass.opName = "$concat";
-proto.getOpName = function getOpName(){
-	return klass.opName;
-};
+var Value = require("../Value"),
+	Expression = require("./Expression");
 
-/**
- * Concats a string of values together.
- * @method evaluate
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-    return this.operands.map(function(x) {
-		var y = x.evaluateInternal(vars);
-		if(typeof(y) !== "string") {
-	    	throw new Error("$concat only supports strings - 16702");
-		}
-	return y;
-    }).join("");
+	var n = this.operands.length;
+
+	var result = "";
+	for (var i = 0; i < n; ++i) {
+		var val = this.operands[i].evaluateInternal(vars);
+
+		if (val === undefined || val === null)
+			return null;
+
+		if (typeof val !== "string")
+			throw new Error(this.getOpName() + " only supports strings, not " +
+				Value.getType(val) + "; uassert code 16702");
+
+		result += val;
+	}
+
+	return result;
 };
 
-Expression.registerExpression(klass.opName, base.parse);
+Expression.registerExpression("$concat", base.parse);
+
+proto.getOpName = function getOpName(){
+	return "$concat";
+};

+ 6 - 12
lib/pipeline/expressions/Expression.js

@@ -49,12 +49,7 @@ var ObjectCtx = Expression.ObjectCtx = (function() {
 		for (var k in opts) { // assign all given opts to self so long as they were part of klass.prototype as undefined properties
 			if (opts.hasOwnProperty(k) && proto.hasOwnProperty(k) && proto[k] === undefined) this[k] = opts[k];
 		}
-	}, base = Object,
-		proto = klass.prototype = Object.create(base.prototype, {
-			constructor: {
-				value: klass
-			}
-		});
+	}, proto = klass.prototype;
 
 	// PROTOTYPE MEMBERS
 	proto.isDocumentOk =
@@ -183,9 +178,8 @@ klass.expressionParserMap = {};
  * REGISTER_EXPRESSION("$foo", ExpressionFoo::parse);
  */
 klass.registerExpression = function registerExpression(key, parserFunc) {
-	if (key in klass.expressionParserMap) {
+	if (key in klass.expressionParserMap)
 		throw new Error("Duplicate expression (" + key + ") detected; massert code 17064");
-	}
 	klass.expressionParserMap[key] = parserFunc;
 	return 1;
 };
@@ -272,7 +266,7 @@ proto.optimize = function optimize() {
  *             where {a:1} inclusion objects aren't allowed, they get
  *             NULL.
  */
-proto.addDependencies = function addDependencies(deps, path) {
+proto.addDependencies = function addDependencies(deps, path) { //jshint ignore:line
 	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
 };
 
@@ -290,7 +284,7 @@ proto.isSimple = function isSimple() {
  * If explain is false, returns a Value parsable by parseOperand().
  * @method serialize
  */
-proto.serialize = function serialize(explain) {
+proto.serialize = function serialize(explain) { //jshint ignore:line
 	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
 };
 
@@ -304,7 +298,7 @@ proto.serialize = function serialize(explain) {
  * @param vars
  */
 proto.evaluate = function evaluate(vars) {
-	if (!(vars instanceof Variables)) vars = new Variables(0, vars); /// Evaluate expression with specified inputs and return result. (only used by tests)
+	if (vars instanceof Object && vars.constructor === Object) vars = new Variables(0, vars); /// Evaluate expression with specified inputs and return result. (only used by tests)
 	return this.evaluateInternal(vars);
 };
 
@@ -327,7 +321,7 @@ klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
  * @method evaluate
  * @returns the computed value
  */
-proto.evaluateInternal = function evaluateInternal(vars) {
+proto.evaluateInternal = function evaluateInternal(vars) { //jshint ignore:line
 	throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
 };
 

+ 5 - 4
lib/pipeline/expressions/FieldPathExpression.js

@@ -2,7 +2,6 @@
 
 var Expression = require("./Expression"),
     Variables = require("./Variables"),
-    Value = require("../Value"),
     FieldPath = require("../FieldPath");
 
 /**
@@ -53,11 +52,13 @@ klass.parse = function parse(raw, vps) {
     if (raw.length < 2) throw new Error("'$' by itself is not a valid FieldPath; uassert code 16872"); // need at least "$" and either "$" or a field name
     if (raw[1] === "$") {
         var fieldPath = raw.substr(2), // strip off $$
-            varName = fieldPath.substr(0, fieldPath.indexOf("."));
+            dotIndex = fieldPath.indexOf("."),
+            varName = fieldPath.substr(0, dotIndex !== -1 ? dotIndex : fieldPath.length);
         Variables.uassertValidNameForUserRead(varName);
-        return new FieldPathExpression(raw.slice(2), vps.getVariableName(varName));
+        return new FieldPathExpression(fieldPath, vps.getVariable(varName));
     } else {
-        return new FieldPathExpression("CURRENT." + raw.substr(1), vps.getVariable("CURRENT"));
+        return new FieldPathExpression("CURRENT." + raw.substr(1), // strip the "$" prefix
+            vps.getVariable("CURRENT"));
     }
 };
 

+ 62 - 59
lib/pipeline/expressions/MapExpression.js

@@ -1,107 +1,110 @@
 "use strict";
 
 var MapExpression = module.exports = function MapExpression(varName, varId, input, each){
-	if (arguments.length !== 4) throw new Error("Four args expected");
+	if (arguments.length !== 4) throw new Error(klass.name + ": args expected: varName, varId, input, each");
 	this._varName = varName;
 	this._varId = varId;
 	this._input = input;
 	this._each = each;
 }, klass = MapExpression, Expression = require("./Expression"), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
-var Variables = require("./Variables"),
-	VariablesParseState = require("./VariablesParseState");
+var Value = require("../Value"),
+	Variables = require("./Variables");
 
-// PROTOTYPE MEMBERS
+klass.parse = function parse(expr, vpsIn) {
 
+	// if (!(exprFieldName)) throw new Error("Assertion failure"); //NOTE: DEVIATION FROM MONGO: we do not have exprFieldName here
 
-klass.parse = function parse(expr, vpsIn){
-	if(!("$map" in expr)) {
-		throw new Error("Tried to create a $let with something other than let. Looks like your parse map went all funny.");
+	if (Value.getType(expr) !== "Object") {
+		throw new Error("$map only supports an object as it's argument; uassert code 16878");
 	}
 
-	if(typeof(expr.$map) !== 'object' || (expr.$map instanceof Array)) {
-		throw new Error("$map only supports an object as it's argument:16878");
+	// "in" must be parsed after "as" regardless of BSON order
+	var inputElem,
+		asElem,
+		inElem,
+		args = expr;
+	for (var argFieldName in args) {
+		var arg = args[argFieldName];
+		if (argFieldName === "input") {
+			inputElem = arg;
+		} else if (argFieldName === "as") {
+			asElem = arg;
+		} else if (argFieldName === "in") {
+			inElem = arg;
+		} else {
+			throw new Error("Unrecognized parameter to $map: " + argFieldName + "; uassert code 16879");
+		}
 	}
 
-	var args = expr.$map,
-		inputElem = args.input,
-		inElem = args['in'],
-		asElem = args.as;
-
-	if(!inputElem) {
-		throw new Error("Missing 'input' parameter to $map: 16880");
-	}
-	if(!asElem) {
-		throw new Error("Missing 'as' parameter to $map: 16881");
-	}
-	if(!inElem) {
-		throw new Error("Missing 'in' parameter to $let: 16882");
-	}
+	if (!inputElem) throw new Error("Missing 'input' parameter to $map; uassert code 16880");
+	if (!asElem) throw new Error("Missing 'as' parameter to $map; uassert code 16881");
+	if (!inElem) throw new Error("Missing 'in' parameter to $map; uassert code 16882");
 
+	// parse "input"
+	var input = Expression.parseOperand(inputElem, vpsIn); // only has outer vars
 
-	if(Object.keys(args).length > 3) {
-		var bogus = Object.keys(args).filter(function(x) {return !(x === 'in' || x === 'as' || x === 'input');});
-		throw new Error("Unrecognized parameter to $map: " + bogus.join(",") + "- 16879");
-	}
-
-	var input = Expression.parseOperand(inputElem, vpsIn);
-
-	var vpsSub = new VariablesParseState(vpsIn),
+	// parse "as"
+	var vpsSub = vpsIn, // vpsSub gets our vars, vpsIn doesn't.
 		varName = asElem;
-
 	Variables.uassertValidNameForUserWrite(varName);
 	var varId = vpsSub.defineVariable(varName);
 
-	var invert = Expression.parseOperand(inElem, vpsSub);
+	// parse "in"
+	var inExpr = Expression.parseOperand(inElem, vpsSub); // has access to map variable
 
-	return new MapExpression(varName, varId, input, invert);
+	return new MapExpression(varName, varId, input, inExpr);
 };
 
-
 proto.optimize = function optimize() {
+	// TODO handle when _input is constant
 	this._input = this._input.optimize();
 	this._each = this._each.optimize();
 	return this;
 };
 
 proto.serialize = function serialize(explain) {
-	return {$map: {input:this._input.serialize(explain),
-				   as: this._varName,
-				   'in': this._each.serialize(explain)}};
+	return {
+		$map: {
+			input: this._input.serialize(explain),
+			as: this._varName,
+			in : this._each.serialize(explain)
+		}
+	};
 };
 
 proto.evaluateInternal = function evaluateInternal(vars) {
+	// guaranteed at parse time that this isn't using our _varId
 	var inputVal = this._input.evaluateInternal(vars);
-	if( inputVal === null) {
+	if (inputVal === null || inputVal === undefined)
 		return null;
-	}
 
-	if(!(inputVal instanceof Array)) {
-		throw new Error("Input to $map must be an Array, not a ____ 16883");
+	if (!(inputVal instanceof Array)){
+		throw new Error("input to $map must be an Array not " +
+			Value.getType(inputVal) + "; uassert code 16883");
 	}
 
-	if(inputVal.length === 0) {
-		return [];
-	}
+	if (inputVal.length === 0)
+		return inputVal;
+
+	var output = new Array(inputVal.length);
+	for (var i = 0, l = inputVal.length; i < l; i++) {
+		vars.setValue(this._varId, inputVal[i]);
 
-	// Diverge from Mongo source here, as Javascript has a builtin map operator.
-	return inputVal.map(function(x) {
-	   vars.setValue(this._varId, x);
-	   var toInsert = this._each.evaluateInternal(vars);
-	   if(toInsert === undefined) {
-		   toInsert = null;
-	   }
+		var toInsert = this._each.evaluateInternal(vars);
+		if (toInsert === undefined)
+			toInsert = null; // can't insert missing values into array
 
-	   return toInsert;
-   });
+		output[i] = toInsert;
+	}
+
+	return output;
 };
 
-proto.addDependencies = function addDependencies(deps, path){
-	this._input.addDependencies(deps, path);
-	this._each.addDependencies(deps, path);
+proto.addDependencies = function addDependencies(deps, path) { //jshint ignore:line
+	this._input.addDependencies(deps);
+	this._each.addDependencies(deps);
 	return deps;
 };
 
-
 Expression.registerExpression("$map", klass.parse);

+ 30 - 23
lib/pipeline/expressions/MultiplyExpression.js

@@ -7,36 +7,43 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var MultiplyExpression = module.exports = function MultiplyExpression(){
-	//if (arguments.length !== 0) throw new Error("Zero args expected");
+if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
 }, klass = MultiplyExpression, base = require("./VariadicExpressionT")(MultiplyExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
- Expression = require("./Expression");
+	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-klass.opName = "$multiply";
-proto.getOpName = function getOpName(){
-	return klass.opName;
-};
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var product = 1; //NOTE: DEVIATION FROM MONGO: no need to track narrowest so just use one var
 
-/**
- * Takes an array of one or more numbers and multiples them, returning the resulting product.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars){
-	var product = 1;
-	for(var i = 0, n = this.operands.length; i < n; ++i){
-		var value = this.operands[i].evaluateInternal(vars);
-		if(value instanceof Date) throw new Error("$multiply does not support dates; code 16375");
-		product *= Value.coerceToDouble(value);
+	var n = this.operands.length;
+	for (var i = 0; i < n; ++i) {
+		var val = this.operands[i].evaluateInternal(vars);
+
+		if (typeof val === "number") {
+			product *= Value.coerceToDouble(val);
+		} else if (val === undefined || val === null) {
+			return null;
+		} else {
+			throw new Error("$multiply only supports numeric types, not " +
+			 	Value.getType(val) + "; uasserted code 16555");
+		}
 	}
-	if(typeof(product) != "number") throw new Error("$multiply resulted in a non-numeric type; code 16418");
-	return product;
+
+	if (typeof product === "number")
+		return product;
+	throw new Error("$multiply resulted in a non-numeric type; massert code 16418");
 };
 
-/** Register Expression */
-Expression.registerExpression(klass.opName, base.parse);
+Expression.registerExpression("$multiply", base.parse);
+
+proto.getOpName = function getOpName(){
+	return "$multiply";
+};
+
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return true;
+};

+ 33 - 33
lib/pipeline/expressions/SetDifferenceExpression.js

@@ -1,52 +1,52 @@
 "use strict";
 
 /**
- * A $setdifference pipeline expression.
- * @see evaluateInternal
+ * A $setDifference pipeline expression.
  * @class SetDifferenceExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SetDifferenceExpression = module.exports = function SetDifferenceExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SetDifferenceExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SetDifferenceExpression, base = require("./FixedArityExpressionT")(SetDifferenceExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	Expression = require("./Expression");
+	Expression = require("./Expression"),
+	ValueSet = require("../ValueSet");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$setdifference";
-};
-
-/**
- * Takes 2 arrays. Assigns the second array to the first array.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var array1 = this.operands[0].evaluateInternal(vars),
-		array2 = this.operands[1].evaluateInternal(vars);
-	if (array1 instanceof Array) throw new Error(this.getOpName() + ": object 1 must be an array");
-	if (array2 instanceof Array) throw new Error(this.getOpName() + ": object 2 must be an array");
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
 
-	var returnVec = [];
+	if (lhs === undefined || lhs === null || rhs === undefined || rhs === null) {
+		return null;
+	}
 
-	array1.forEach(function(key) {
-		if (-1 === array2.indexOf(key)) {
-			returnVec.push(key);
+	if (!(lhs instanceof Array))
+		throw new Error("both operands of " + this.getOpName() + " must be arrays. First " +
+			"argument is of type: " + Value.getType(lhs) + "; uassert code 17048");
+	if (!(rhs instanceof Array))
+		throw new Error("both operands of " + this.getOpName() + " must be arrays. Second " +
+			"argument is of type: " + Value.getType(rhs) + "; uassert code 17049");
+
+	var rhsSet = new ValueSet(rhs),
+		lhsArray = lhs,
+		returnVec = [];
+	for (var i = 0, l = lhsArray.length; i < l; ++i) {
+		// rhsSet serves the dual role of filtering out elements that were originally present
+		// in RHS and of eleminating duplicates from LHS
+		var it = lhsArray[i];
+		if (rhsSet.insert(it) !== undefined) {
+			returnVec.push(it);
 		}
-	}, this);
+	}
 	return returnVec;
 };
 
-/** Register Expression */
-Expression.registerExpression("$setdifference", base.parse);
+Expression.registerExpression("$setDifference", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$setDifference";
+};

+ 33 - 20
lib/pipeline/expressions/SetEqualsExpression.js

@@ -2,37 +2,50 @@
 
 /**
  * A $setequals pipeline expression.
- * @see evaluateInternal
  * @class SetEqualsExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SetEqualsExpression = module.exports = function SetEqualsExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SetEqualsExpression, base = require("./NaryBaseExpressionT")(SetEqualsExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = SetEqualsExpression, base = require("./VariadicExpressionT")(SetEqualsExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	Expression = require("./Expression");
+	Expression = require("./Expression"),
+	ValueSet = require("../ValueSet");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$setequals";
+proto.validateArguments = function validateArguments(args) {
+	if (args.length < 2)
+		throw new Error(this.getOpName() + " needs at least two arguments had: " +
+			args.length + "; uassert code 17045");
 };
 
-/**
- * Takes 2 arrays. Assigns the second array to the first array.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var array1 = this.operands[0].evaluateInternal(vars),
-		array2 = this.operands[1].evaluateInternal(vars);
-	if (array1 instanceof Array) throw new Error(this.getOpName() + ": object 1 must be an array");
-	if (array2 instanceof Array) throw new Error(this.getOpName() + ": object 2 must be an array");
-	array1 = array2;
-	return array1;
+	var n = this.operands.length,
+		lhs;
+
+	for (var i = 0; i < n; i++) {
+		var nextEntry = this.operands[i].evaluateInternal(vars);
+		if (!(nextEntry instanceof Array))
+			throw new Error("All operands of " + this.getOpName() +" must be arrays. One " +
+				"argument is of type: " + Value.getType(nextEntry) + "; uassert code 17044");
+
+		if (i === 0) {
+			lhs = new ValueSet(nextEntry);
+		} else {
+			var rhs = new ValueSet(nextEntry);
+			if (!lhs.equals(rhs)) {
+				return false;
+			}
+		}
+	}
+	return true;
 };
 
-/** Register Expression */
-Expression.registerExpression("$setequals", base.parse);
+Expression.registerExpression("$setEquals", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$setEquals";
+};

+ 43 - 22
lib/pipeline/expressions/SetIntersectionExpression.js

@@ -2,39 +2,60 @@
 
 /**
  * A $setintersection pipeline expression.
- * @see evaluateInternal
  * @class SetIntersectionExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SetIntersectionExpression = module.exports = function SetIntersectionExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SetIntersectionExpression, base = require("./NaryBaseExpressionT")(SetIntersectionExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = SetIntersectionExpression, base = require("./VariadicExpressionT")(SetIntersectionExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	Expression = require("./Expression");
+	Expression = require("./Expression"),
+	ValueSet = require("../ValueSet");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$setIntersection";
-};
-
-/**
- * Takes 2 objects. Returns the intersects of the objects.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var object1 = this.operands[0].evaluateInternal(vars),
-		object2 = this.operands[1].evaluateInternal(vars);
-	if (object1 instanceof Array) throw new Error(this.getOpName() + ": object 1 must be an object");
-	if (object2 instanceof Array) throw new Error(this.getOpName() + ": object 2 must be an object");
+	var n = this.operands.length,
+		currentIntersection = new ValueSet();
+	for (var i = 0; i < n; i++){
+		var nextEntry = this.operands[i].evaluateInternal(vars);
+		if (nextEntry === undefined || nextEntry === null){
+			return null;
+		}
+		if (!(nextEntry instanceof Array))
+			 throw new Error("All operands of " + this.getOpName() + "must be arrays. One " +
+				"argument is of type: " + Value.getType(nextEntry) + "; uassert code 17047");
 
-	var result = object1.filter(function(n) {
-		return object2.indexOf(n) > -1;
-	});
+		if (i === 0){
+			currentIntersection.insertRange(nextEntry);
+		} else {
+			var nextSet = new ValueSet(nextEntry);
+			if (currentIntersection.size() > nextSet.size()) {
+				// to iterate over whichever is the smaller set
+				nextSet.swap(currentIntersection);
+			}
+			for (var itKey in currentIntersection.set) {
+				if (!nextSet.hasKey(itKey)) {
+					currentIntersection.eraseKey(itKey);
+				}
+			}
+		}
+		if (currentIntersection.empty()) {
+			break;
+		}
+	}
+	var result = currentIntersection.values();
+	return result;
 };
 
-/** Register Expression */
 Expression.registerExpression("$setIntersection", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$setIntersection";
+};
+
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return true;
+};

+ 69 - 61
lib/pipeline/expressions/SetIsSubsetExpression.js

@@ -7,82 +7,90 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SetIsSubsetExpression = module.exports = function SetIsSubsetExpression() {
-	if (arguments.length !== 2) throw new Error("two args expected");
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SetIsSubsetExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SetIsSubsetExpression, base = require("./FixedArityExpressionT")(SetIsSubsetExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	Expression = require("./Expression");
-
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$setissubset";
-};
+	Expression = require("./Expression"),
+	NaryExpression = require("./NaryExpression"),
+	ConstantExpression = require("./ConstantExpression"),
+	ValueSet = require("../ValueSet");
+
+function setIsSubsetHelper(lhs, rhs) { //NOTE: vector<Value> &lhs, ValueSet &rhs
+	// do not shortcircuit when lhs.size() > rhs.size()
+	// because lhs can have redundant entries
+	for (var i = 0; i < lhs.length; i++) {
+		if (!rhs.has(lhs[i])) {
+			return false;
+		}
+	}
+	return true;
+}
 
-proto.optimize = function optimize(cachedRhsSet, operands) {
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
 
-// This optimize needs to be done, eventually
+	if (!(lhs instanceof Array))
+		throw new Error("both operands of " + this.getOpName() + ": must be arrays. First " +
+			"argument is of type " + Value.getType(lhs) + "; uassert code 17046");
+	if (!(rhs instanceof Array))
+		throw new Error("both operands of " + this.getOpName() + ": must be arrays. Second " +
+			"argument is of type " + Value.getType(rhs) + "; code 17042");
 
-// // perfore basic optimizations
-//     intrusive_ptr<Expression> optimized = ExpressionNary::optimize();
+	return setIsSubsetHelper(lhs, new ValueSet(rhs));
+};
 
-//     // if ExpressionNary::optimize() created a new value, return it directly
-//     if (optimized.get() != this)
-//         return optimized;
 
-//     if (ExpressionConstant* ec = dynamic_cast<ExpressionConstant*>(vpOperand[1].get())) {
-//         const Value rhs = ec->getValue();
-//         uassert(17311, str::stream() << "both operands of $setIsSubset must be arrays. Second "
-//                                      << "argument is of type: " << typeName(rhs.getType()),
-//                 rhs.getType() == Array);
+/**
+ * This class handles the case where the RHS set is constant.
+ *
+ * Since it is constant we can construct the hashset once which makes the runtime performance
+ * effectively constant with respect to the size of RHS. Large, constant RHS is expected to be a
+ * major use case for $redact and this has been verified to improve performance significantly.
+ */
+function Optimized(cachedRhsSet, operands) {
+	this._cachedRhsSet = cachedRhsSet;
+	this.operands = operands;
+}
+Optimized.prototype = Object.create(SetIsSubsetExpression.prototype, {constructor:{value:Optimized}});
+Optimized.prototype.evaluateInternal = function evaluateInternal(vars){
+	var lhs = this.operands[0].evaluateInternal(vars);
+
+	if (!(lhs instanceof Array))
+		throw new Error("both operands of " + this.getOpName() + " must be arrays. First " +
+			"argument is of type: " + Value.getType(lhs) + "; uassert code 17310");
+
+	return setIsSubsetHelper(lhs, this._cachedRhsSet);
+};
 
-//         return new Optimized(arrayToSet(rhs), vpOperand);
-//     }
 
-//     return optimized;
+proto.optimize = function optimize(cachedRhsSet, operands) { //jshint ignore:line
+	// perform basic optimizations
+	var optimized = NaryExpression.optimize();
 
-};
+	// if ExpressionNary::optimize() created a new value, return it directly
+	if(optimized !== this)
+		return optimized;
 
-/**
- * Takes 2 arrays. Assigns the second array to the first array.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var array1 = this.operands[0].evaluateInternal(vars),
-		array2 = this.operands[1].evaluateInternal(vars);
-	if (array1 instanceof Array) throw new Error(this.getOpName() + ": object 1 must be an array");
-	if (array2 instanceof Array) throw new Error(this.getOpName() + ": object 2 must be an array");
-
-	var sizeOfArray1 = array1.length;
-	var sizeOfArray2 = array2.length;
-	var outerLoop = 0;
-	var innerLoop = 0;
-	for (outerLoop = 0; outerLoop < sizeOfArray1; outerLoop++) {
-		for (innerLoop = 0; innerLoop < sizeOfArray2; innerLoop++) {
-			if (array2[outerLoop] == array1[innerLoop])
-				break;
-		}
+	var ce;
+	if ((ce = this.operands[1] instanceof ConstantExpression ? this.operands[1] : undefined)){
+		var rhs = ce.getValue();
+		if (!(rhs instanceof Array))
+			throw new Error("both operands of " + this.getOpName() + " must be arrays. Second " +
+				"argument is of type " + Value.getType(rhs) + "; uassert code 17311");
 
-		/* If the above inner loop was not broken at all then
-		 array2[i] is not present in array1[] */
-		if (innerLoop == sizeOfArray2)
-			return false;
+		return new Optimized(new ValueSet(rhs), this.operands);
 	}
 
-	/* If we reach here then all elements of array2[]
-	 are present in array1[] */
-	return true;
+	return optimized;
 };
 
-/** Register Expression */
-Expression.registerExpression("$setissubset", base.parse);
+Expression.registerExpression("$setIsSubset", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$setIsSubset";
+};

+ 25 - 27
lib/pipeline/expressions/SetUnionExpression.js

@@ -2,45 +2,43 @@
 
 /**
  * A $setunion pipeline expression.
- * @see evaluateInternal
  * @class SetUnionExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SetUnionExpression = module.exports = function SetUnionExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SetUnionExpression, base = require("./NaryBaseExpressionT")(SetUnionExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = SetUnionExpression, base = require("./VariadicExpressionT")(SetUnionExpression), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	Expression = require("./Expression");
+	Expression = require("./Expression"),
+	ValueSet = require("../ValueSet");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$setUnion";
-};
-
-/**
- * Takes 2 objects. Unions the objects
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var object1 = this.operands[0].evaluateInternal(vars),
-		object2 = this.operands[1].evaluateInternal(vars);
-	if (object1 instanceof Array) throw new Error(this.getOpName() + ": object 1 must be an object");
-	if (object2 instanceof Array) throw new Error(this.getOpName() + ": object 2 must be an object");
+	var unionedSet = new ValueSet(),
+		n = this.operands.length;
+	for (var i = 0; i < n; i++){
+		var newEntries = this.operands[i].evaluateInternal(vars);
+		if (newEntries === undefined || newEntries === null){
+			return null;
+		}
+		if (!(newEntries instanceof Array))
+			throw new Error("All operands of " + this.getOpName() + "must be arrays. One argument" +
+				" is of type: " + Value.getType(newEntries) + "; uassert code 17043");
 
-	var object3 = {};
-	for (var attrname1 in object1) {
-		object3[attrname1] = object1[attrname1];
-	}
-	for (var attrname2 in object2) {
-		object3[attrname2] = object2[attrname2];
+		unionedSet.insertRange(newEntries);
 	}
-
-	return object3;
+	return unionedSet.values();
 };
 
-/** Register Expression */
 Expression.registerExpression("$setUnion", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$setUnion";
+};
+
+proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() {
+	return true;
+};

+ 9 - 17
lib/pipeline/expressions/ToLowerExpression.js

@@ -2,35 +2,27 @@
 
 /**
  * A $toLower pipeline expression.
- * @see evaluateInternal
  * @class ToLowerExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var ToLowerExpression = module.exports = function ToLowerExpression(){
+	if (arguments.length !== 0) throw new Error(klass.name + ": args expected: value");
 	base.call(this);
 }, klass = ToLowerExpression, base = require("./FixedArityExpressionT")(ToLowerExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-klass.opName = "$toLower";
-
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-	return klass.opName;
-};
-
-/**
-* Takes a single string and converts that string to lowercase, returning the result. All uppercase letters become lowercase.
-**/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var val = this.operands[0].evaluateInternal(vars),
-		str = Value.coerceToString(val);
+	var pString = this.operands[0].evaluateInternal(vars),
+		str = Value.coerceToString(pString);
 	return str.toLowerCase();
 };
 
-/** Register Expression */
-Expression.registerExpression(klass.opName, base.parse);
+Expression.registerExpression("$toLower", base.parse);
+
+proto.getOpName = function getOpName(){
+	return "$toLower";
+};

+ 9 - 17
lib/pipeline/expressions/ToUpperExpression.js

@@ -2,35 +2,27 @@
 
 /**
  * A $toUpper pipeline expression.
- * @see evaluateInternal
  * @class ToUpperExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var ToUpperExpression = module.exports = function ToUpperExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": args expected: value");
 	base.call(this);
 }, klass = ToUpperExpression, base = require("./FixedArityExpressionT")(ToUpperExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass }});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-klass.opName = "$toUpper";
-
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return klass.opName;
-};
-
-/**
- * Takes a single string and converts that string to lowercase, returning the result. All uppercase letters become lowercase.
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var val = this.operands[0].evaluateInternal(vars),
-		str = Value.coerceToString(val);
+	var pString = this.operands[0].evaluateInternal(vars),
+		str = Value.coerceToString(pString);
 	return str.toUpperCase();
 };
 
-/** Register Expression */
-Expression.registerExpression(klass.opName, base.parse);
+Expression.registerExpression("$toUpper", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$toUpper";
+};

+ 94 - 74
lib/pipeline/expressions/Variables.js

@@ -1,57 +1,72 @@
 "use strict";
 
+// TODO: Look into merging with ExpressionContext and possibly ObjectCtx.
 /**
- * Class that stores/tracks variables
+ * The state used as input and working space for Expressions.
  * @class Variables
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var Variables = module.exports = function Variables(numVars, root){
-	if(numVars) {
-		if(typeof numVars !== 'number') {
-			throw new Error('numVars must be a number');
-		}
-	}
+	if (arguments.length === 0) numVars = 0; // This is only for expressions that use no variables (even ROOT).
+	if (typeof numVars !== "number") throw new Error("numVars must be a Number");
+
 	this._root = root || {};
-	this._rest = numVars ? [] : undefined; //An array of `Value`s
+	this._rest = numVars === 0 ? null : new Array(numVars);
 	this._numVars = numVars;
-}, klass = Variables,
-	base = Object,
-	proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = Variables, proto = klass.prototype;
 
+klass.uassertValidNameForUserWrite = function uassertValidNameForUserWrite(varName) {
+	// System variables users allowed to write to (currently just one)
+	if (varName === "CURRENT") {
+		return;
+	}
 
-klass.ROOT_ID = -1;
+	if (!varName)
+		throw new Error("empty variable names are not allowed; uassert code 16866");
 
-// PROTOTYPE MEMBERS
+	var firstCharIsValid = (varName[0] >= "a" && varName[0] <= "z") ||
+		(varName[0] & "\x80"); // non-ascii
 
-/**
- * Sets the root variable
- * @method setRoot
- * @parameter root {Document} The root variable
- **/
-proto.setRoot = function setRoot(root){
-	if(!(root instanceof Object && root.constructor.name === 'Object')) { //NOTE: Type checking cause c++ does this for you
-		throw new Error('root must be an Object');
+	if (!firstCharIsValid)
+		throw new Error("'" + varName + "' starts with an invalid character for a user variable name; uassert code 16867");
+
+	for (var i = 1, l = varName.length; i < l; i++) {
+		var charIsValid = (varName[i] >= 'a' && varName[i] <= 'z') ||
+			(varName[i] >= 'A' && varName[i] <= 'Z') ||
+			(varName[i] >= '0' && varName[i] <= '9') ||
+			(varName[i] == '_') ||
+			(varName[i] & '\x80'); // non-ascii
+
+		if (!charIsValid)
+			throw new Error("'" + varName + "' contains an invalid character " +
+				"for a variable name: '" + varName[i] + "'; uassert code 16868");
 	}
-	this._root = root;
 };
 
-/**
- * Clears the root variable
- * @method clearRoot
- **/
-proto.clearRoot = function clearRoot(){
-	this._root = {};
-};
+klass.uassertValidNameForUserRead = function uassertValidNameForUserRead(varName) {
+	if (!varName)
+		throw new Error("empty variable names are not allowed; uassert code 16869");
 
-/**
- * Gets the root variable
- * @method getRoot
- * @return {Document} the root variable
- **/
-proto.getRoot = function getRoot(){
-	return this._root;
+	var firstCharIsValid = (varName[0] >= "a" && varName[0] <= "z") ||
+		(varName[0] >= "A" && varName[0] <= "Z") ||
+		(varName[0] & "\x80"); // non-ascii
+
+	if (!firstCharIsValid)
+		throw new Error("'" + varName + "' starts with an invalid character for a variable name; uassert code 16870");
+
+	for (var i = 1, l = varName.length; i < l; i++) {
+		var charIsValid = (varName[i] >= "a" && varName[i] <= "z") ||
+			(varName[i] >= "A" && varName[i] <= "Z") ||
+			(varName[i] >= "0" && varName[i] <= "9") ||
+			(varName[i] == "_") ||
+			(varName[i] & "\x80"); // non-ascii
+
+		if (!charIsValid)
+			throw new Error("'" + varName + "' contains an invalid character " +
+				"for a variable name: '" + varName[i] + "'; uassert code 16871");
+	}
 };
 
 /**
@@ -59,20 +74,11 @@ proto.getRoot = function getRoot(){
  * @method setValue
  * @param id {Number} The index where the value is stored in the _rest Array
  * @param value {Value} The value to store
- **/
+ */
 proto.setValue = function setValue(id, value) {
-	//NOTE: Some added type enforcement cause c++ does this for you
-	if(typeof id !== 'number') {
-		throw new Error('id must be a Number');
-	}
-
-	if(id === klass.ROOT_ID) {
-		throw new Error("mError 17199: can't use Variables#setValue to set ROOT");
-	}
-	if(id >= this._numVars) { // a > comparator would be off-by-one; i.e. if we have 5 vars, the max id would be 4
-		throw new Error("You have more variables than _numVars");
-	}
-
+	if (typeof id !== "number") throw new Error("id must be a Number");
+	if (id === klass.ROOT_ID) throw new Error("can't use Variables#setValue to set ROOT; massert code 17199");
+	if (id >= this._numVars) throw new Error("Assertion error");
 	this._rest[id] = value;
 };
 
@@ -81,46 +87,60 @@ proto.setValue = function setValue(id, value) {
  * @method getValue
  * @param id {Number} The index where the value was stored
  * @return {Value} The value
- **/
+ */
 proto.getValue = function getValue(id) {
-	//NOTE: Some added type enforcement cause c++ does this for you
-	if(typeof id !== 'number') {
-		throw new Error('id must be a Number');
-	}
-
-	if(id === klass.ROOT_ID) {
+	if (typeof id !== "number") throw new Error("id must be a Number");
+	if (id === klass.ROOT_ID)
 		return this._root;
-	}
-	if(id >= this._numVars) { // a > comparator would be off-by-one; i.e. if we have 5 vars, the max id would be 4
-		throw new Error("Cannot get value; id was greater than _numVars");
-	}
-
+	if (id >= this._numVars) throw new Error("Assertion error");
 	return this._rest[id];
 };
 
-
 /**
  * Get the value for id if it's a document
  * @method getDocument
  * @param id {Number} The index where the document was stored
  * @return {Object} The document
- **/
+ */
 proto.getDocument = function getDocument(id) {
-	//NOTE: Some added type enforcement cause c++ does this for you
-	if(typeof id !== 'number') {
-		throw new Error('id must be a Number');
-	}
+	if (typeof id !== "number") throw new Error("id must be a Number");
 
-	if(id === klass.ROOT_ID) {
+	if (id === klass.ROOT_ID)
 		return this._root;
-	}
-	if(id >= this._numVars) { // a > comparator would be off-by-one; i.e. if we have 5 vars, the max id would be 4
-		throw new Error("Cannot get value; id was greater than _numVars");
-	}
 
+	if (id >= this._numVars) throw new Error("Assertion error");
 	var value = this._rest[id];
-	if(typeof value === 'object' && value.constructor.name === 'Object') {
+	if (value instanceof Object && value.constructor === Object)
 		return value;
-	}
+
 	return {};
 };
+
+klass.ROOT_ID = -1;
+
+/**
+ * Use this instead of setValue for setting ROOT
+ * @method setRoot
+ * @parameter root {Document} The root variable
+ */
+proto.setRoot = function setRoot(root){
+	if (!(root instanceof Object && root.constructor === Object)) throw new Error("Assertion failure");
+	this._root = root;
+};
+
+/**
+ * Clears the root variable
+ * @method clearRoot
+ */
+proto.clearRoot = function clearRoot(){
+	this._root = {};
+};
+
+/**
+ * Gets the root variable
+ * @method getRoot
+ * @return {Document} the root variable
+ */
+proto.getRoot = function getRoot(){
+	return this._root;
+};

+ 8 - 8
lib/pipeline/expressions/VariablesIdGenerator.js

@@ -1,31 +1,31 @@
 "use strict";
 
-/** 
- * Class generates unused ids
+/**
+ * Generates Variables::Ids and keeps track of the number of Ids handed out.
  * @class VariablesIdGenerator
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var VariablesIdGenerator = module.exports = function VariablesIdGenerator(){
 	this._nextId = 0;
-}, klass = VariablesIdGenerator, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+}, klass = VariablesIdGenerator, proto = klass.prototype;
 
 /**
  * Gets the next unused id
  * @method generateId
  * @return {Number} The unused id
- **/
+ */
 proto.generateId = function generateId() {
 	return this._nextId++;
 };
 
 /**
- * Gets the number of used ids
+ * Returns the number of Ids handed out by this Generator.
+ * Return value is intended to be passed to Variables constructor.
  * @method getIdCount
  * @return {Number} The number of used ids
- **/
+ */
 proto.getIdCount = function getIdCount() {
 	return this._nextId;
 };
-

+ 26 - 23
lib/pipeline/expressions/VariablesParseState.js

@@ -1,22 +1,25 @@
 "use strict";
 
-/** 
- * Class generates unused ids
+/**
+ * This class represents the Variables that are defined in an Expression tree.
+ *
+ * All copies from a given instance share enough information to ensure unique Ids are assigned
+ * and to propagate back to the original instance enough information to correctly construct a
+ * Variables instance.
+ *
  * @class VariablesParseState
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var Variables = require('./Variables'),
-	VariablesIdGenerator = require('./VariablesIdGenerator');
+ */
+var Variables = require("./Variables"),
+	VariablesIdGenerator = require("./VariablesIdGenerator");
 
 var VariablesParseState = module.exports = function VariablesParseState(idGenerator){
-	if(!idGenerator || idGenerator.constructor !== VariablesIdGenerator) {
-		throw new Error("idGenerator is required and must be of type VariablesIdGenerator");
-	}
+	if (!(idGenerator instanceof VariablesIdGenerator)) throw new Error("idGenerator is required and must be of type VariablesIdGenerator");
 	this._idGenerator = idGenerator;
-	this._variables = {}; //Note: The c++ type was StringMap<Variables::Id>
-}, klass = VariablesParseState, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+	this._variables = {};
+}, klass = VariablesParseState, proto = klass.prototype;
 
 /**
  * Assigns a named variable a unique Id. This differs from all other variables, even
@@ -27,12 +30,12 @@ var VariablesParseState = module.exports = function VariablesParseState(idGenera
  * breaks that equivalence.
  *
  * NOTE: Name validation is responsibility of caller.
- **/
-proto.defineVariable = function generateId(name) {
+ */
+proto.defineVariable = function defineVariable(name) {
 	// caller should have validated before hand by using Variables::uassertValidNameForUserWrite
-	if(name === 'ROOT') {
-		throw new Error("mError 17275: Can't redefine ROOT");
-	}
+	if (name === "ROOT")
+		throw new Error("Can't redefine ROOT; massert code 17275");
+
 	var id = this._idGenerator.generateId();
 	this._variables[name] = id;
 	return id;
@@ -42,14 +45,14 @@ proto.defineVariable = function generateId(name) {
  * Returns the current Id for a variable. uasserts if the variable isn't defined.
  * @method getVariable
  * @param name {String} The name of the variable
- **/
-proto.getVariable = function getIdCount(name) {
-	var found = this._variables[name];
-	if(typeof found === 'number') return found;
-	if(name !== "ROOT" && name !== "CURRENT") {
-		throw new Error("uError 17276: Use of undefined variable " + name);
-	}
+ */
+proto.getVariable = function getVariable(name) {
+	var it = this._variables[name];
+	if (typeof it === "number")
+		return it;
+
+	if (name !== "ROOT" && name !== "CURRENT")
+		throw new Error("Use of undefined variable " + name + "; uassert code 17276");
 
 	return Variables.ROOT_ID;
 };
-

+ 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);

+ 142 - 107
test/lib/pipeline/expressions/AllElementsTrueExpression.js

@@ -5,131 +5,166 @@ var assert = require("assert"),
 	AllElementsTrueExpression = require("../../../../lib/pipeline/expressions/AllElementsTrueExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
-var allElementsTrueExpression = new AllElementsTrueExpression();
-
-function errMsg(expr, args, tree, expected, result) {
-	return 	"for expression " + expr +
-			" with argument " + args +
-			" full tree: " + JSON.stringify(tree) +
-			" expected: " + expected +
-			" result: " + result;
-}
-
-module.exports = {
+// 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 ExpectedResultBase = (function() {
+	var klass = function ExpectedResultBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	}, proto = klass.prototype;
+	proto.run = function() {
+		var spec = this.getSpec,
+			args = spec.input;
+		if (spec.expected !== undefined && spec.expected !== null) {
+			var fields = spec.expected;
+			for (var fieldFirst in fields) {
+				var fieldSecond = fields[fieldFirst],
+					expected = fieldSecond;
+					// obj = {<fieldFirst>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					expr = Expression.parseExpression(fieldFirst, args, vps),
+					result = expr.evaluate({}),
+					errMsg = "for expression " + fieldFirst +
+							" with argument " + JSON.stringify(args) +
+							" full tree " + JSON.stringify(expr.serialize(false)) +
+							" expected: " + JSON.stringify(expected) +
+							" but got: " + JSON.stringify(result);
+				assert.deepEqual(result, expected, errMsg);
+				//TODO test optimize here
+			}
+		}
+		if (spec.error !== undefined && spec.error !== null) {
+			var asserters = spec.error,
+				n = asserters.length;
+			for (var i = 0; i < n; ++i) {
+                // var obj2 = {<asserters[i]>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator2 = new VariablesIdGenerator(),
+					vps2 = new VariablesParseState(idGenerator2);
+				assert.throws(function() {
+					// NOTE: parse and evaluatation failures are treated the same
+					expr = Expression.parseExpression(asserters[i], args, vps2);
+					expr.evaluate({});
+				}); // jshint ignore:line
+			}
+		}
+	};
+	return klass;
+})();
 
-	"AllElementsTrueExpression": {
+exports.AllElementsTrueExpression = {
 
-		"constructor()": {
+	"constructor()": {
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new AllElementsTrueExpression();
-				});
-			}
+		"should construct instance": function() {
+			assert(new AllElementsTrueExpression() instanceof AllElementsTrueExpression);
+			assert(new AllElementsTrueExpression() instanceof Expression);
+		},
 
+		"should error if given args": function() {
+			assert.throws(function() {
+				new AllElementsTrueExpression("bad stuff");
+			});
 		},
 
-		"#getOpName()": {
+	},
 
-			"should return the correct op name; $allElements": function testOpName(){
-				assert.equal(new AllElementsTrueExpression().getOpName(), "$allElementsTrue");
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $allElementsTrue": function() {
+			assert.equal(new AllElementsTrueExpression().getOpName(), "$allElementsTrue");
 		},
 
-		"integration": {
-
-			"JustFalse": function JustFalse(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[false]],
-				 	expr = Expression.parseExpression("$allElementsTrue", input),
-					result = expr.evaluate({}),
-					expected = false,
-					msg = errMsg("$allElementsTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+	},
 
-			"JustTrue": function JustTrue(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[true]],
-					expr = Expression.parseExpression("$allElementsTrue", input),
-					result = expr.evaluate({}),
-					expected = true,
-					msg = errMsg("$allElementsTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+	"#evaluate()": {
 
-			"OneTrueOneFalse": function OneTrueOneFalse(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[true, false]],
-					expr = Expression.parseExpression("$allElementsTrue", input),
-					result = expr.evaluate({}),
-					expected = false,
-					msg = errMsg("$allElementsTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+		"should return false if just false": function JustFalse() {
+            new ExpectedResultBase({
+				getSpec: {
+					input: [[false]],
+					expected: {
+						$allElementsTrue: false,
+						// $anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-			"OneFalseOneTrue": function OneTrueOneFalse(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[false, true]],
-					expr = Expression.parseExpression("$allElementsTrue", input),
-					result = expr.evaluate({}),
-					expected = false,
-					msg = errMsg("$allElementsTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+		"should return true if just true": function JustTrue() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[true]],
+					expected: {
+						$allElementsTrue: true,
+						// $anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"Empty": function Empty(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[]],
-					expr = Expression.parseExpression("$allElementsTrue", input),
-					result = expr.evaluate({}),
-					expected = false,
-					msg = errMsg("$allElementsTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+		"should return false if one true and one false": function OneTrueOneFalse() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[true, false]],
+					expected: {
+						$allElementsTrue: false,
+						// $anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"TrueViaInt": function TrueViaInt(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[1]],
-					expr = Expression.parseExpression("$allElementsTrue", input),
-					result = expr.evaluate({}),
-					expected = true,
-					msg = errMsg("$allElementsTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+		"should return true if empty": function Empty() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						$allElementsTrue: true,
+						// $anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-			"FalseViaInt": function FalseViaInt(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[0]],
-					expr = Expression.parseExpression("$allElementsTrue", input),
-					result = expr.evaluate({}),
-					expected = false,
-					msg = errMsg("$allElementsTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+		"should return true if truthy int": function TrueViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1]],
+					expected: {
+						$allElementsTrue: true,
+						// $anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"Null": function FalseViaInt(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [null],
-					expr = Expression.parseExpression("$allElementsTrue", input);
-				assert.throws(function() {
-					var result = expr.evaluate({});
-				});
-			}
+		"should return false if falsy int": function FalseViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[0]],
+					expected: {
+						$allElementsTrue: false,
+						// $anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-		}
+		"should error if given null instead of array": function FalseViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [null],
+					error: [
+						"$allElementsTrue",
+						// "$anyElementTrue",
+					]
+				}
+			}).run();
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 99 - 27
test/lib/pipeline/expressions/AndExpression_test.js

@@ -1,6 +1,11 @@
 "use strict";
 var assert = require("assert"),
 	AndExpression = require("../../../../lib/pipeline/expressions/AndExpression"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	CoerceToBoolExpression = require("../../../../lib/pipeline/expressions/CoerceToBoolExpression"),
+	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
+	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
 
@@ -8,6 +13,10 @@ module.exports = {
 
 	"AndExpression": {
 
+		beforeEach: function() {
+			this.vps = new VariablesParseState(new VariablesIdGenerator());
+		},
+
 		"constructor()": {
 
 			"should not throw Error when constructing without args": function testConstructor(){
@@ -36,51 +45,51 @@ module.exports = {
 		"#evaluate()": {
 
 			"should return true if no operands were given; {$and:[]}": function testEmpty(){
-				assert.equal(Expression.parseOperand({$and:[]}).evaluate(), true);
+				assert.equal(Expression.parseOperand({$and:[]},this.vps).evaluate(), true);
 			},
 
 			"should return true if operands is one true; {$and:[true]}": function testTrue(){
-				assert.equal(Expression.parseOperand({$and:[true]}).evaluate(), true);
+				assert.equal(Expression.parseOperand({$and:[true]},this.vps).evaluate(), true);
 			},
 
 			"should return false if operands is one false; {$and:[false]}": function testFalse(){
-				assert.equal(Expression.parseOperand({$and:[false]}).evaluate(), false);
+				assert.equal(Expression.parseOperand({$and:[false]},this.vps).evaluate(), false);
 			},
 
 			"should return true if operands are true and true; {$and:[true,true]}": function testTrueTrue(){
-				assert.equal(Expression.parseOperand({$and:[true,true]}).evaluate(), true);
+				assert.equal(Expression.parseOperand({$and:[true,true]},this.vps).evaluate(), true);
 			},
 
 			"should return false if operands are true and false; {$and:[true,false]}": function testTrueFalse(){
-				assert.equal(Expression.parseOperand({$and:[true,false]}).evaluate(), false);
+				assert.equal(Expression.parseOperand({$and:[true,false]},this.vps).evaluate(), false);
 			},
 
 			"should return false if operands are false and true; {$and:[false,true]}": function testFalseTrue(){
-				assert.equal(Expression.parseOperand({$and:[false,true]}).evaluate(), false);
+				assert.equal(Expression.parseOperand({$and:[false,true]},this.vps).evaluate(), false);
 			},
 
 			"should return false if operands are false and false; {$and:[false,false]}": function testFalseFalse(){
-				assert.equal(Expression.parseOperand({$and:[false,false]}).evaluate(), false);
+				assert.equal(Expression.parseOperand({$and:[false,false]},this.vps).evaluate(), false);
 			},
 
 			"should return true if operands are true, true, and true; {$and:[true,true,true]}": function testTrueTrueTrue(){
-				assert.equal(Expression.parseOperand({$and:[true,true,true]}).evaluate(), true);
+				assert.equal(Expression.parseOperand({$and:[true,true,true]},this.vps).evaluate(), true);
 			},
 
 			"should return false if operands are true, true, and false; {$and:[true,true,false]}": function testTrueTrueFalse(){
-				assert.equal(Expression.parseOperand({$and:[true,true,false]}).evaluate(), false);
+				assert.equal(Expression.parseOperand({$and:[true,true,false]},this.vps).evaluate(), false);
 			},
 
 			"should return false if operands are 0 and 1; {$and:[0,1]}": function testZeroOne(){
-				assert.equal(Expression.parseOperand({$and:[0,1]}).evaluate(), false);
+				assert.equal(Expression.parseOperand({$and:[0,1]},this.vps).evaluate(), false);
 			},
 
 			"should return false if operands are 1 and 2; {$and:[1,2]}": function testOneTwo(){
-				assert.equal(Expression.parseOperand({$and:[1,2]}).evaluate(), true);
+				assert.equal(Expression.parseOperand({$and:[1,2]},this.vps).evaluate(), true);
 			},
 
 			"should return true if operand is a path String to a truthy value; {$and:['$a']}": function testFieldPath(){
-				assert.equal(Expression.parseOperand({$and:['$a']}).evaluate({a:1}), true);
+				assert.equal(Expression.parseOperand({$and:['$a']},this.vps).evaluate({a:1}), true);
 			}
 
 		},
@@ -88,50 +97,113 @@ module.exports = {
 		"#optimize()": {
 
 			"should optimize a constant expression to a constant; {$and:[1]} == true": function testOptimizeConstantExpression(){
-				assert.deepEqual(Expression.parseOperand({$and:[1]}).optimize().toJSON(true), {$const:true});
+				var a = Expression.parseOperand({$and:[1]}, this.vps).optimize();
+				assert.equal(a.operands.length, 0, "The operands should have been optimized away");
+				assert.equal(a.evaluateInternal(), true);
 			},
 
 			"should not optimize a non-constant expression; {$and:['$a']}": function testNonConstant(){
-				assert.deepEqual(Expression.parseOperand({$and:['$a']}).optimize().toJSON(), {$and:['$a']});
+				var a = Expression.parseOperand({$and:['$a']}, this.vps).optimize();
+				assert.equal(a.operands[0]._fieldPath.fieldNames.length, 2);
+				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[0], "CURRENT");
+				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[1], "a");
 			},
 
-			"optimize an expression beginning with a constant; {$and:[1,'$a']};": function testConstantNonConstant(){
-				assert.deepEqual(Expression.parseOperand({$and:[1,'$a']}).optimize().toJSON(), {$and:[1,'$a']});
-				assert.notEqual(Expression.parseOperand({$and:[1,'$a']}).optimize().toJSON(), {$and:[0,'$a']});
+			"should not optimize an expression ending with a non-constant. {$and:[1,'$a']};": function testConstantNonConstant(){
+				var a = Expression.parseOperand({$and:[1,'$a']}, this.vps).optimize();
+				assert(a instanceof CoerceToBoolExpression);
+				assert(a.expression instanceof FieldPathExpression);
+
+				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
+				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
+				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
 			},
 
-			"should optimize an expression with a path and a '1' (is entirely constant); {$and:['$a',1]}": function testNonConstantOne(){
-				assert.deepEqual(Expression.parseOperand({$and:['$a',1]}).optimize().toJSON(), {$and:['$a']});
+			"should optimize an expression with a path and a '1'; {$and:['$a',1]}": function testNonConstantOne(){
+				var a = Expression.parseOperand({$and:['$a', 1]}, this.vps).optimize();
+				// The 1 should be removed as it is redundant.
+				assert(a instanceof CoerceToBoolExpression, "The result should be forced to a boolean");
+
+				// This is the '$a' which cannot be optimized.
+				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
+				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
+				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
 			},
 
 			"should optimize an expression with a field path and a '0'; {$and:['$a',0]}": function testNonConstantZero(){
-				assert.deepEqual(Expression.parseOperand({$and:['$a',0]}).optimize().toJSON(true), {$const:false});
+				var a = Expression.parseOperand({$and:['$a',0]}, this.vps).optimize();
+				assert.equal(a.operands.length, 0, "The operands should have been optimized away");
+				assert.equal(a.evaluateInternal(), false, "The 0 operand should have been converted to false");
 			},
 
 			"should optimize an expression with two field paths and '1'; {$and:['$a','$b',1]}": function testNonConstantNonConstantOne(){
-				assert.deepEqual(Expression.parseOperand({$and:['$a','$b',1]}).optimize().toJSON(), {$and:['$a','$b']});
+				var a = Expression.parseOperand({$and:['$a', '$b', 1]}, this.vps).optimize();
+				assert.equal(a.operands.length, 2, "Two operands should remain.");
+
+				// This is the '$a' which cannot be optimized.
+				assert.deepEqual(a.operands[0]._fieldPath.fieldNames.length, 2);
+				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[0], "CURRENT");
+				assert.deepEqual(a.operands[0]._fieldPath.fieldNames[1], "a");
+
+				// This is the '$b' which cannot be optimized.
+				assert.deepEqual(a.operands[1]._fieldPath.fieldNames.length, 2);
+				assert.deepEqual(a.operands[1]._fieldPath.fieldNames[0], "CURRENT");
+				assert.deepEqual(a.operands[1]._fieldPath.fieldNames[1], "b");
 			},
 
 			"should optimize an expression with two field paths and '0'; {$and:['$a','$b',0]}": function testNonConstantNonConstantZero(){
-				assert.deepEqual(Expression.parseOperand({$and:['$a','$b',0]}).optimize().toJSON(true), {$const:false});
+				var a = Expression.parseOperand({$and:['$a', '$b', 0]}, this.vps).optimize();
+				assert(a instanceof ConstantExpression, "With that trailing false, we know the result...");
+				assert.equal(a.operands.length, 0, "The operands should have been optimized away");
+				assert.equal(a.evaluateInternal(), false);
 			},
 
 			"should optimize an expression with '0', '1', and a field path; {$and:[0,1,'$a']}": function testZeroOneNonConstant(){
-				assert.deepEqual(Expression.parseOperand({$and:[0,1,'$a']}).optimize().toJSON(true), {$const:false});
+				var a = Expression.parseOperand({$and:[0,1,'$a']}, this.vps).optimize();
+				assert(a instanceof ConstantExpression);
+				assert.equal(a.evaluateInternal(), false);
 			},
 
 			"should optimize an expression with '1', '1', and a field path; {$and:[1,1,'$a']}": function testOneOneNonConstant(){
-				assert.deepEqual(Expression.parseOperand({$and:[1,1,'$a']}).optimize().toJSON(), {$and:['$a']});
+				var a = Expression.parseOperand({$and:[1,1,'$a']}, this.vps).optimize();
+				assert(a instanceof CoerceToBoolExpression);
+				assert(a.expression instanceof FieldPathExpression);
+
+				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
+				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
+				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
 			},
 
 			"should optimize nested $and expressions properly and optimize out values evaluating to true; {$and:[1,{$and:[1]},'$a','$b']}": function testNested(){
-				assert.deepEqual(Expression.parseOperand({$and:[1,{$and:[1]},'$a','$b']}).optimize().toJSON(), {$and:['$a','$b']});
+				var a = Expression.parseOperand({$and:[1,{$and:[1]},'$a','$b']}, this.vps).optimize();
+				assert.equal(a.operands.length, 2)
+				assert(a.operands[0] instanceof FieldPathExpression);
+				assert(a.operands[1] instanceof FieldPathExpression);
 			},
 
 			"should optimize nested $and expressions containing a nested value evaluating to false; {$and:[1,{$and:[1]},'$a','$b']}": function testNested(){
-				assert.deepEqual(Expression.parseOperand({$and:[1,{$and:[{$and:[0]}]},'$a','$b']}).optimize().toJSON(true), {$const:false});
+				//assert.deepEqual(Expression.parseOperand({$and:[1,{$and:[{$and:[0]}]},'$a','$b']}, this.vps).optimize().toJSON(true), {$const:false});
+				var a = Expression.parseOperand({$and:[1,{$and:[{$and:[0]}]},'$a','$b']}, this.vps).optimize();
+				assert(a instanceof ConstantExpression);
+				assert.equal(a.evaluateInternal(), false);
+			},
+
+			"should optimize when the constants are on the right of the operand list. The rightmost is true": function(){
+				// 1, "x", and 1 are all true.  They should be optimized away.
+				var a = Expression.parseOperand({$and:['$a', 1, "x", 1]}, this.vps).optimize();
+				assert(a instanceof CoerceToBoolExpression);
+				assert(a.expression instanceof FieldPathExpression);
+
+				assert.equal(a.expression._fieldPath.fieldNames.length, 2);
+				assert.equal(a.expression._fieldPath.fieldNames[0], "CURRENT");
+				assert.equal(a.expression._fieldPath.fieldNames[1], "a");
+			},
+			"should optimize when the constants are on the right of the operand list. The rightmost is false": function(){
+				// 1, "x", and 1 are all true.  They should be optimized away.
+				var a = Expression.parseOperand({$and:['$a', 1, "x", 0]}, this.vps).optimize();
+				assert(a instanceof ConstantExpression, "The rightmost false kills it all");
+				assert.equal(a.evaluateInternal(), false);
 			}
-
 		}
 
 	}

+ 142 - 96
test/lib/pipeline/expressions/AnyElementTrueExpression.js

@@ -5,120 +5,166 @@ var assert = require("assert"),
 	AnyElementTrueExpression = require("../../../../lib/pipeline/expressions/AnyElementTrueExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
-var anyElementTrueExpression = new AnyElementTrueExpression();
-
-function errMsg(expr, args, tree, expected, result) {
-	return 	"for expression " + expr +
-			" with argument " + args +
-			" full tree: " + JSON.stringify(tree) +
-			" expected: " + expected +
-			" result: " + result;
-}
-
-module.exports = {
+// 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 ExpectedResultBase = (function() {
+	var klass = function ExpectedResultBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	}, proto = klass.prototype;
+	proto.run = function() {
+		var spec = this.getSpec,
+			args = spec.input;
+		if (spec.expected !== undefined && spec.expected !== null) {
+			var fields = spec.expected;
+			for (var fieldFirst in fields) {
+				var fieldSecond = fields[fieldFirst],
+					expected = fieldSecond;
+					// obj = {<fieldFirst>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					expr = Expression.parseExpression(fieldFirst, args, vps),
+					result = expr.evaluate({}),
+					errMsg = "for expression " + fieldFirst +
+							" with argument " + JSON.stringify(args) +
+							" full tree " + JSON.stringify(expr.serialize(false)) +
+							" expected: " + JSON.stringify(expected) +
+							" but got: " + JSON.stringify(result);
+				assert.deepEqual(result, expected, errMsg);
+				//TODO test optimize here
+			}
+		}
+		if (spec.error !== undefined && spec.error !== null) {
+			var asserters = spec.error,
+				n = asserters.length;
+			for (var i = 0; i < n; ++i) {
+				// var obj2 = {<asserters[i]>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator2 = new VariablesIdGenerator(),
+					vps2 = new VariablesParseState(idGenerator2);
+				assert.throws(function() {
+					// NOTE: parse and evaluatation failures are treated the same
+					expr = Expression.parseExpression(asserters[i], args, vps2);
+					expr.evaluate({});
+				}); // jshint ignore:line
+			}
+		}
+	};
+	return klass;
+})();
 
-	"AnyElementTrueExpression": {
+exports.AnyElementTrueExpression = {
 
-		"constructor()": {
+	"constructor()": {
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new AnyElementTrueExpression();
-				});
-			}
+		"should construct instance": function() {
+			assert(new AnyElementTrueExpression() instanceof AnyElementTrueExpression);
+			assert(new AnyElementTrueExpression() instanceof Expression);
+		},
 
+		"should error if given args": function() {
+			assert.throws(function() {
+				new AnyElementTrueExpression("bad stuff");
+			});
 		},
 
-		"#getOpName()": {
+	},
 
-			"should return the correct op name; $anyElement": function testOpName(){
-				assert.equal(new AnyElementTrueExpression().getOpName(), "$anyElementTrue");
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $anyElementTrue": function() {
+			assert.equal(new AnyElementTrueExpression().getOpName(), "$anyElementTrue");
 		},
 
-		"integration": {
+	},
 
-			"JustFalse": function JustFalse(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[false]],
-					expr = Expression.parseExpression("$anyElementTrue", input),
-					result = expr.evaluate({}),
-					expected = false,
-					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+	"#evaluate()": {
 
-			"JustTrue": function JustTrue(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[true]],
-					expr = Expression.parseExpression("$anyElementTrue", input),
-					result = expr.evaluate({}),
-					expected = true,
-					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+		"should return false if just false": function JustFalse() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[false]],
+					expected: {
+						// $allElementsTrue: false,
+						$anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-			"OneTrueOneFalse": function OneTrueOneFalse(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[true, false]],
-					expr = Expression.parseExpression("$anyElementTrue", input),
-					result = expr.evaluate({}),
-					expected = true,
-					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+		"should return true if just true": function JustTrue() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[true]],
+					expected: {
+						// $allElementsTrue: true,
+						$anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"Empty": function Empty(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[]],
-					expr = Expression.parseExpression("$anyElementTrue", input),
-					result = expr.evaluate({}),
-					expected = false,
-					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+		"should return false if one true and one false": function OneTrueOneFalse() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[true, false]],
+					expected: {
+						// $allElementsTrue: false,
+						$anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"TrueViaInt": function TrueViaInt(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[1]],
-					expr = Expression.parseExpression("$anyElementTrue", input),
-					result = expr.evaluate({}),
-					expected = true,
-					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+		"should return true if empty": function Empty() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						// $allElementsTrue: true,
+						$anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-			"FalseViaInt": function FalseViaInt(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [[0]],
-					expr = Expression.parseExpression("$anyElementTrue", input),
-					result = expr.evaluate({}),
-					expected = false,
-					msg = errMsg("$anyElementTrue", input, expr.serialize(false), expected, result);
-				assert.equal(result, expected, msg);
-			},
+		"should return true if truthy int": function TrueViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1]],
+					expected: {
+						// $allElementsTrue: true,
+						$anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"Null": function FalseViaInt(){
-				var idGenerator = new VariablesIdGenerator(),
-					vps = new VariablesParseState(idGenerator),
-					input = [null],
-					expr = Expression.parseExpression("$anyElementTrue", input);
-				assert.throws(function() {
-					var result = expr.evaluate({});
-				});
-			}
+		"should return false if falsy int": function FalseViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[0]],
+					expected: {
+						// $allElementsTrue: false,
+						$anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-		}
+		"should error if given null instead of array": function FalseViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [null],
+					error: [
+						// "$allElementsTrue",
+						"$anyElementTrue",
+					]
+				}
+			}).run();
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 63 - 43
test/lib/pipeline/expressions/ConcatExpression_test.js

@@ -1,71 +1,91 @@
 "use strict";
 var assert = require("assert"),
 	ConcatExpression = require("../../../../lib/pipeline/expressions/ConcatExpression"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// 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.ConcatExpression = {
 
-	"ConcatExpression": {
+	beforeEach: function() {
+		this.vps = new VariablesParseState(new VariablesIdGenerator());
+	},
 
-		"constructor()": {
+	"constructor()": {
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new ConcatExpression();
-				});
-			},
-			"should throw Error when constructing with args": function testConstructor(){
-				assert.throws(function(){
-					new ConcatExpression("should die");
-				});
-			}
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function() {
+				new ConcatExpression();
+			});
 		},
 
-		"#getOpName()": {
+		"should throw Error when constructing with args": function() {
+			assert.throws(function() {
+				new ConcatExpression("should die");
+			});
+		},
+
+	},
 
-			"should return the correct op name; $concat": function testOpName(){
-				assert.equal(new ConcatExpression().getOpName(), "$concat");
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $concat": function() {
+			assert.equal(new ConcatExpression().getOpName(), "$concat");
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.equal(new ConcatExpression().getFactory(), ConcatExpression);
-			}
+	"#evaluate()": {
 
+		"should return empty string if no operands were given; {$concat:[]}": function() {
+			var expr = Expression.parseOperand({$concat:[]}, this.vps);
+			assert.equal(expr.evaluate(), "");
 		},
 
-		"#evaluate()": {
+		"should return mystring if operands are my string; {$concat:[my, string]}": function() {
+			var expr = Expression.parseOperand({$concat:["my", "string"]}, this.vps);
+			assert.equal(expr.evaluate(), "mystring");
+		},
 
-			"should return empty string if no operands were given; {$concat:[]}": function testEmpty(){
-				assert.equal(Expression.parseOperand({$concat:[]}).evaluate(), "");
-			},
+		"should return mystring if operands are my and $a; {$concat:[my,$a]}": function() {
+			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps);
+			assert.equal(expr.evaluate({a:"string"}), "mystring");
+		},
 
-			"should return mystring if operands are my string; {$concat:[my, string]}": function testConcat(){
-				assert.equal(Expression.parseOperand({$concat:["my", "string"]}).evaluate(), "mystring");
-			},
+		"should return null if an operand evaluates to null; {$concat:[my,$a]}": function() {
+			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps);
+			assert.equal(expr.evaluate({a:null}), null);
+		},
 
-			"should return mystring if operands are my and $a; {$concat:[my,$a]}": function testFieldPath(){
-				assert.equal(Expression.parseOperand({$concat:["my","$a"]}).evaluate({a:"string"}), "mystring");
-			},
+		"should return null if an operand evaluates to undefined; {$concat:[my,$a]}": function() {
+			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps);
+			assert.equal(expr.evaluate({a:undefined}), null);
+		},
 
-			"should return null if an operand evaluates to null; {$concat:[my,$a]}": function testNull(){
-				assert.equal(Expression.parseOperand({$concat:["my","$a"]}).evaluate({a:null}), null);
-			},
+		"should throw if an operand is a number": function() {
+			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps);
+			assert.throws(function() {
+				expr.evaluate({a:100});
+			});
+		},
 
-			"should throw if a non-string is passed in: {$concat:[my,$a]}": function testNull(){
-				assert.throws(function(){
-					Expression.parseOperand({$concat:["my","$a"]}).evaluate({a:100});
-				});
+		"should throw if an operand is a date": function() {
+			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps);
+			assert.throws(function() {
+				expr.evaluate({a:new Date()});
+			});
+		},
 
-			}
-		}
+		"should throw if an operand is a boolean": function() {
+			var expr = Expression.parseOperand({$concat:["my","$a"]}, this.vps)
+			assert.throws(function() {
+				expr.evaluate({a:true});
+			});
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 115 - 0
test/lib/pipeline/expressions/MapExpression_test.js

@@ -0,0 +1,115 @@
+"use strict";
+var assert = require("assert"),
+	MapExpression = require("../../../../lib/pipeline/expressions/MapExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
+	AddExpression = require("../../../../lib/pipeline/expressions/AddExpression"), // jshint ignore:line
+	IfNullExpression = require("../../../../lib/pipeline/expressions/IfNullExpression"), // jshint ignore:line
+	Variables = require("../../../../lib/pipeline/expressions/Variables"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	DepsTracker = require("../../../../lib/pipeline/DepsTracker"),
+	utils = require("./utils"),
+	constify = utils.constify,
+	expressionToJson = utils.expressionToJson;
+
+// 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.MapExpression = {
+
+	"constructor()": {
+
+		"should accept 4 arguments": function () {
+			new MapExpression(1, 2, 3, 4);
+		},
+
+		"should accept only 4 arguments": function () {
+			assert.throws(function () { new MapExpression(); });
+			assert.throws(function () { new MapExpression(1); });
+			assert.throws(function () { new MapExpression(1, 2); });
+			assert.throws(function () { new MapExpression(1, 2, 3); });
+			assert.throws(function () { new MapExpression(1, 2, 3, 4, 5); });
+		},
+
+	},
+
+	"#optimize()": {
+
+		"should optimize both $map.input and $map.in": function() {
+			var spec = {$map:{
+					input: {$ifNull:[null, {$const:[1,2,3]}]},
+					as: "i",
+					in: {$add:["$$i","$$i",1,2]},
+				}},
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(spec, vps),
+				optimized = expr.optimize();
+			assert.strictEqual(optimized, expr, "should be same reference");
+			assert.deepEqual(expressionToJson(optimized._input), {$const:[1,2,3]});
+			assert.deepEqual(expressionToJson(optimized._each), constify({$add:["$$i","$$i",1,2]}));
+		},
+
+	},
+
+	"#serialize()": {
+
+		"should serialize to consistent order": function() {
+			var spec = {$map:{
+					as: "i",
+					in: {$add:["$$i","$$i"]},
+					input: {$const:[1,2,3]},
+				}},
+				expected = {$map:{
+					input: {$const:[1,2,3]},
+					as: "i",
+					in: {$add:["$$i","$$i"]},
+				}},
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(spec, vps);
+			assert.deepEqual(expressionToJson(expr), expected);
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should be able to map over a simple array": function() {
+			var spec = {$map:{
+					input: {$const:[1,2,3]},
+					as: "i",
+					in: {$add:["$$i","$$i"]},
+				}},
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(spec, vps),
+				vars = new Variables(1, {}); // must set numVars (usually handled by doc src)
+			assert.deepEqual(expr.evaluate(vars), [2, 4, 6]);
+		},
+
+	},
+
+	"#addDependencies()": {
+
+		"should add dependencies to both $map.input and $map.in": function () {
+			var spec = {$map:{
+					input: "$inputArray",
+					as: "i",
+					in: {$add:["$$i","$someConst"]},
+				}},
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(spec, vps),
+				deps = new DepsTracker();
+			expr.addDependencies(deps);
+			assert.strictEqual(Object.keys(deps.fields).length, 2);
+			assert.strictEqual("inputArray" in deps.fields, true);
+			assert.strictEqual("someConst" in deps.fields, true);
+			assert.strictEqual(deps.needWholeDocument, false);
+			assert.strictEqual(deps.needTextScore, false);
+		},
+
+	},
+
+};

+ 91 - 30
test/lib/pipeline/expressions/MultiplyExpression_test.js

@@ -1,51 +1,112 @@
 "use strict";
 var assert = require("assert"),
 	MultiplyExpression = require("../../../../lib/pipeline/expressions/MultiplyExpression"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	FieldPathExpression = require("../../../../lib/pipeline/expressions/FieldPathExpression"),
+	ConstantExpression = require("../../../../lib/pipeline/expressions/ConstantExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+// 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.MultiplyExpression = {
 
-	"MultiplyExpression": {
+	beforeEach: function(){
+		this.vps = new VariablesParseState(new VariablesIdGenerator());
+	},
 
-		"constructor()": {
+	"constructor()": {
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new MultiplyExpression();
-				});
-			},
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function(){
+				new MultiplyExpression();
+			});
+		},
+
+		"should throw Error when constructing with args": function() {
+			assert.throws(function(){
+				new MultiplyExpression(1);
+			});
+		},
+
+	},
 
-			"should throw Error when constructing with args": function testConstructor(){
-				assert.throws(function(){
-					new MultiplyExpression(1);
-				});
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $multiply": function() {
+			assert.equal(new MultiplyExpression().getOpName(), "$multiply");
 		},
 
-		"#getOpName()": {
+	},
+
+	"#evaluate()": {
+
+		"should multiply constants": function () {
+			assert.strictEqual(Expression.parseOperand({$multiply: [2, 3, 4]}, this.vps).evaluate(), 2 * 3 * 4);
+		},
 
-			"should return the correct op name; $multiply": function testOpName(){
-				assert.equal(new MultiplyExpression().getOpName(), "$multiply");
-			}
+		"should 'splode if an operand is a string": function () {
+			assert.throws(function () {
+				Expression.parseOperand({$multiply: [2, "x", 4]}, this.vps).evaluate();
+			});
+		},
 
+		"should 'splode if an operand is a boolean": function () {
+			assert.throws(function () {
+				Expression.parseOperand({$multiply: [2, "x", 4]}, this.vps).evaluate();
+			});
 		},
 
-		"#evaluate()": {
+		"should 'splode if an operand is a date": function () {
+			assert.throws(function () {
+				Expression.parseOperand({$multiply: [2, "x", 4]}, this.vps).evaluate();
+			});
+		},
 
-			"should return result of multiplying numbers": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$multiply:["$a", "$b"]}).evaluateInternal({a:1, b:2}), 1*2);
-				assert.strictEqual(Expression.parseOperand({$multiply:["$a", "$b", "$c"]}).evaluateInternal({a:1.345, b:2e45, c:0}), 1.345*2e45*0);
-				assert.strictEqual(Expression.parseOperand({$multiply:["$a"]}).evaluateInternal({a:1}), 1);
-			},
-			"should throw an exception if the result is not a number": function testStuff(){
-				assert.throws(Expression.parseOperand({$multiply:["$a", "$b"]}).evaluateInternal({a:1e199, b:1e199}));
-			}
-		}
+		"should handle a null operand": function(){
+			assert.strictEqual(Expression.parseOperand({$multiply: [2, null]}, this.vps).evaluate(), null);
+		},
 
-	}
+		"should handle an undefined operand": function(){
+			assert.strictEqual(Expression.parseOperand({$multiply: [2, undefined]}, this.vps).evaluate(), null);
+		},
 
-};
+		"should multiply mixed numbers": function () {
+			assert.strictEqual(Expression.parseOperand({$multiply: [2.1, 3, 4.4]}, this.vps).evaluate(), 2.1 * 3 * 4.4);
+		},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"should return result of multiplying simple variables": function () {
+			assert.equal(Expression.parseOperand({$multiply: ["$a", "$b"]}, this.vps).evaluate({a: 1, b: 2}), 1 * 2);
+		},
+
+		"should return result of multiplying large variables": function () {
+			assert.strictEqual(Expression.parseOperand({$multiply: ["$a", "$b", "$c"]}, this.vps).evaluate({a: 1.345, b: 2e45, c: 0}), 1.345 * 2e45 * 0);
+		},
+
+		"should return result of multiplying one number": function () {
+			assert.strictEqual(Expression.parseOperand({$multiply: ["$a"]}, this.vps).evaluate({a: 1}), 1);
+		},
+
+		"should throw an exception if the result is not a number": function () {
+			assert.throws(function() {
+				Expression.parseOperand({$multiply: ["$a", "$b"]}, this.vps).evaluate({a: 1e199, b: 1e199});
+			});
+		},
+
+	},
+
+	"optimize": {
+
+		"should optimize out constants separated by a variable": function () {
+			var a = Expression.parseOperand({$multiply: [2, 3, 4, 5, '$a', 6, 7, 8]}, this.vps).optimize();
+			assert(a instanceof MultiplyExpression);
+			assert.equal(a.operands.length, 2);
+			assert(a.operands[0] instanceof FieldPathExpression);
+			assert(a.operands[1] instanceof ConstantExpression);
+			assert.equal(a.operands[1].evaluateInternal(), 2*3*4*5*6*7*8);
+		},
+
+	},
+
+};

+ 321 - 83
test/lib/pipeline/expressions/SetDifferenceExpression.js

@@ -1,87 +1,325 @@
 "use strict";
 var assert = require("assert"),
-		SetDifferenceExpression = require("../../../../lib/pipeline/expressions/SetDifferenceExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-		"SetDifferenceExpression": {
-
-				"constructor()": {
-
-						"should throw Error when constructing without args": function testConstructor() {
-								assert.throws(function() {
-										new SetDifferenceExpression();
-								});
-						}
-
-				},
-
-				"#getOpName()": {
-
-						"should return the correct op name; $setdifference": function testOpName() {
-								assert.equal(new SetDifferenceExpression([1, 2, 3], [4, 5, 6]).getOpName(), "$setdifference");
-						}
-
-				},
-
-				"#evaluateInternal()": {
-
-						"Should fail if array1 is not an array": function testArg1() {
-								var array1 = "not an array",
-										array2 = [6, 7, 8, 9];
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setdifference: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if array2 is not an array": function testArg2() {
-								var array1 = [1, 2, 3, 4],
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setdifference: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if both are not an array": function testArg1andArg2() {
-								var array1 = "not an array",
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setdifference: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should pass and return a difference between the arrays": function testBasicAssignment() {
-								var array1 = [1, 9, 2, 3, 4, 5],
-										array2 = [5, 6, 7, 2, 8, 9];
-								assert.strictEqual(Expression.parseOperand({
-										$setdifference: ["$array1", "$array2"]
-								}).evaluateInternal({
-										array1: array1,
-										array2: array2
-								}), [1, 3, 4, 6, 7, 8]);
-						},
-
-				}
-
-		}
+	SetDifferenceExpression = require("../../../../lib/pipeline/expressions/SetDifferenceExpression"),
+	ExpectedResultBase = require("./SetExpectedResultBase");
 
-};
+// 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.SetDifferenceExpression = {
+
+	"constructor()": {
+
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function() {
+				new SetDifferenceExpression();
+			});
+		},
+
+		"should throw Error when constructing with args": function() {
+			assert.throws(function() {
+				new SetDifferenceExpression("someArg");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $setDifference": function() {
+			assert.equal(new SetDifferenceExpression().getOpName(), "$setDifference");
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should handle when sets are the same": function Same(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						$setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set has redundant items": function Redundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						$setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the both sets have redundant items": function DoubleRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 1, 2], [1, 2, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						$setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a superset": function Super(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						$setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is a superset and has redundant items": function SuperWithRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 2], [1]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						$setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a subset": function Sub(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1], [1, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						$setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets are the same but backwards": function SameBackwards(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [2, 1]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						$setDifference: [],
+					},
+				},
+			}).run();
+		},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"should handle when the sets do not overlap": function NoOverlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 4]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [],
+						// $setUnion: [1, 2, 4, 8],
+						$setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets do overlap": function Overlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 2, 4]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [2],
+						// $setUnion: [1, 2, 4, 8],
+						$setDifference: [1],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is null": function LastNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], null],
+					expected: {
+						// $setIntersection: null,
+						// $setUnion: null,
+						$setDifference: null,
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is null": function FirstNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [null, [1, 2]],
+					expected: {
+						// $setIntersection: null,
+						// $setUnion: null,
+						$setDifference: null,
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has no args": function NoArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						"$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has one arg": function OneArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						"$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty arg": function EmptyArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						"$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty left arg": function LeftArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						"$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty right arg": function RightArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], []],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [1, 2],
+						// $setIsSubset: false,
+						// $setEquals: false,
+						$setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args": function ManyArgs(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[8, 3], ["asdf", "foo"], [80.3, 34], [], [80.3, "foo", 11, "yay"]],
+					expected: {
+						// $setIntersection: [],
+						// $setEquals: false,
+						// $setUnion: [3, 8, 11, 34, 80.3, "asdf", "foo", "yay"],
+					},
+					error: [
+						// "$setIsSubset",
+						"$setDifference",
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args that are equal sets": function ManyArgsEqual(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 4], [1, 2, 2, 4], [4, 1, 2], [2, 1, 1, 4]],
+					expected: {
+						// $setIntersection: [1, 2, 4],
+						// $setEquals: true,
+						// $setUnion: [1, 2, 4],
+					},
+					error: [
+						// "$setIsSubset",
+						"$setDifference",
+					],
+				},
+			}).run();
+		},
+
+	},
+
+};

+ 321 - 83
test/lib/pipeline/expressions/SetEqualsExpression.js

@@ -1,87 +1,325 @@
 "use strict";
 var assert = require("assert"),
-		SetEqualsExpression = require("../../../../lib/pipeline/expressions/SetEqualsExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-		"SetEqualsExpression": {
-
-				"constructor()": {
-
-						"should throw Error when constructing without args": function testConstructor() {
-								assert.throws(function() {
-										new SetEqualsExpression();
-								});
-						}
-
-				},
-
-				"#getOpName()": {
-
-						"should return the correct op name; $setequals": function testOpName() {
-								assert.equal(new SetEqualsExpression([1,2,3],[4,5,6]).getOpName(), "$setequals");
-						}
-
-				},
-
-				"#evaluateInternal()": {
-
-						"Should fail if array1 is not an array": function testArg1() {
-								var array1 = "not an array",
-										array2 = [6, 7, 8, 9];
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setequals: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if array2 is not an array": function testArg2() {
-								var array1 = [1, 2, 3, 4],
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setequals: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if both are not an array": function testArg1andArg2() {
-								var array1 = "not an array",
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setequals: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should pass and array1 should equal array2": function testBasicAssignment() {
-								var array1 = [1, 2, 3, 4],
-										array2 = [6, 7, 8, 9];
-								assert.strictEqual(Expression.parseOperand({
-										$setequals: ["$array1", "$array2"]
-								}).evaluateInternal({
-										array1: array1,
-										array2: array2
-								}), [6, 7, 8, 9]);
-						},
-
-				}
-
-		}
+	SetEqualsExpression = require("../../../../lib/pipeline/expressions/SetEqualsExpression"),
+	ExpectedResultBase = require("./SetExpectedResultBase");
 
-};
+// 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.SetEqualsExpression = {
+
+	"constructor()": {
+
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function() {
+				new SetEqualsExpression();
+			});
+		},
+
+		"should throw Error when constructing with args": function() {
+			assert.throws(function() {
+				new SetEqualsExpression("someArg");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $setEquals": function() {
+			assert.equal(new SetEqualsExpression().getOpName(), "$setEquals");
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should handle when sets are the same": function Same(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2]],
+					expected: {
+						// $setIsSubset: true,
+						$setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set has redundant items": function Redundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2, 2]],
+					expected: {
+						// $setIsSubset: true,
+						$setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the both sets have redundant items": function DoubleRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 1, 2], [1, 2, 2]],
+					expected: {
+						// $setIsSubset: true,
+						$setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a superset": function Super(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1]],
+					expected: {
+						// $setIsSubset: false,
+						$setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is a superset and has redundant items": function SuperWithRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 2], [1]],
+					expected: {
+						// $setIsSubset: false,
+						$setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a subset": function Sub(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1], [1, 2]],
+					expected: {
+						// $setIsSubset: true,
+						$setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets are the same but backwards": function SameBackwards(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [2, 1]],
+					expected: {
+						// $setIsSubset: true,
+						$setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"should handle when the sets do not overlap": function NoOverlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 4]],
+					expected: {
+						// $setIsSubset: false,
+						$setEquals: false,
+						// $setIntersection: [],
+						// $setUnion: [1, 2, 4, 8],
+						// $setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets do overlap": function Overlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 2, 4]],
+					expected: {
+						// $setIsSubset: false,
+						$setEquals: false,
+						// $setIntersection: [2],
+						// $setUnion: [1, 2, 4, 8],
+						// $setDifference: [1],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is null": function LastNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], null],
+					expected: {
+						// $setIntersection: null,
+						// $setUnion: null,
+						// $setDifference: null,
+					},
+					error: [
+						"$setEquals"
+						// "$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is null": function FirstNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [null, [1, 2]],
+					expected: {
+						// $setIntersection: null,
+						// $setUnion: null,
+						// $setDifference: null,
+					},
+					error: [
+						"$setEquals"
+						// "$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has no args": function NoArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						"$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has one arg": function OneArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						"$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty arg": function EmptyArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						"$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty left arg": function LeftArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						"$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty right arg": function RightArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], []],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [1, 2],
+						// $setIsSubset: false,
+						$setEquals: false,
+						// $setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args": function ManyArgs(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[8, 3], ["asdf", "foo"], [80.3, 34], [], [80.3, "foo", 11, "yay"]],
+					expected: {
+						// $setIntersection: [],
+						$setEquals: false,
+						// $setUnion: [3, 8, 11, 34, 80.3, "asdf", "foo", "yay"],
+					},
+					error: [
+						// "$setIsSubset",
+						// "$setDifference",
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args that are equal sets": function ManyArgsEqual(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 4], [1, 2, 2, 4], [4, 1, 2], [2, 1, 1, 4]],
+					expected: {
+						// $setIntersection: [1, 2, 4],
+						$setEquals: true,
+						// $setUnion: [1, 2, 4],
+					},
+					error: [
+						// "$setIsSubset",
+						// "$setDifference",
+					],
+				},
+			}).run();
+		},
+
+	},
+
+};

+ 59 - 0
test/lib/pipeline/expressions/SetExpectedResultBase.js

@@ -0,0 +1,59 @@
+"use strict";
+var assert = require("assert"),
+	SetDifferenceExpression = require("../../../../lib/pipeline/expressions/SetDifferenceExpression"), //jshint ignore:line
+	SetIsSubsetExpression = require("../../../../lib/pipeline/expressions/SetIsSubsetExpression"), //jshint ignore:line
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
+
+// 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 ExpectedResultBase = module.exports = (function() { //jshint ignore:line
+	var klass = function ExpectedResultBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	}, proto = klass.prototype;
+	proto.run = function() {
+		var spec = this.getSpec,
+			args = spec.input;
+		if (spec.expected !== undefined && spec.expected !== null) {
+			var fields = spec.expected;
+			for (var fieldFirst in fields) {
+				var fieldSecond = fields[fieldFirst],
+					expected = fieldSecond;
+					// obj = {<fieldFirst>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator = new VariablesIdGenerator(),
+					vps = new VariablesParseState(idGenerator),
+					expr = Expression.parseExpression(fieldFirst, args, vps),
+					result = expr.evaluate({});
+				if (result instanceof Array){
+					result.sort();
+				}
+				var errMsg = "for expression " + fieldFirst +
+					" with argument " + JSON.stringify(args) +
+					" full tree: " + JSON.stringify(expr.serialize(false)) +
+					" expected: " + JSON.stringify(expected) +
+					" but got: " + JSON.stringify(result);
+				assert.deepEqual(result, expected, errMsg);
+				//TODO test optimize here
+			}
+		}
+		if (spec.error !== undefined && spec.error !== null) {
+			var asserters = spec.error,
+				n = asserters.length;
+			for (var i = 0; i < n; ++i) {
+				// var obj2 = {<asserters[i]>: args}; //NOTE: DEVIATION FROM MONGO: see parseExpression below
+				var idGenerator2 = new VariablesIdGenerator(),
+					vps2 = new VariablesParseState(idGenerator2);
+				assert.throws(function() {
+					// NOTE: parse and evaluatation failures are treated the same
+					expr = Expression.parseExpression(asserters[i], args, vps2);
+					expr.evaluate({});
+				}); // jshint ignore:line
+			}
+		}
+	};
+	return klass;
+})();

+ 319 - 111
test/lib/pipeline/expressions/SetIntersectionExpression.js

@@ -1,117 +1,325 @@
 "use strict";
 var assert = require("assert"),
-		SetIntersectionExpression = require("../../../../lib/pipeline/expressions/SetIntersectionExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-		"SetIntersectionExpression": {
-
-				"constructor()": {
-
-						"should throw Error when constructing without args": function testConstructor() {
-								assert.throws(function() {
-										new SetIntersectionExpression();
-								});
-						}
-
-				},
-
-				"#getOpName()": {
-
-						"should return the correct op name; $setintersection": function testOpName() {
-								assert.equal(new SetIntersectionExpression([1, 2, 3], [4, 5, 6]).getOpName(), "$setintersection");
-						}
-
-				},
-
-				"#evaluateInternal()": {
-
-						"Should fail if array1 is not an array": function testArg1() {
-								var array1 = "not an array",
-										array2 = [6, 7, 8, 9];
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setintersection: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if array2 is not an array": function testArg2() {
-								var array1 = [1, 2, 3, 4],
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setintersection: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if both are not an array": function testArg1andArg2() {
-								var array1 = "not an array",
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setintersection: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should pass and return an interested set1": function testBasicAssignment() {
-								var array1 = {
-										"a": "3",
-										"c": "4"
-								},
-										array2 = {
-												"a": "3",
-												"b": "3"
-										};
-								assert.strictEqual(Expression.parseOperand({
-										$setintersection: ["$array1", "$array2"]
-								}).evaluateInternal({
-										array1: array1,
-										array2: array2
-								}), {
-										"a": "3"
-								});
-						},
-
-						"Should pass and return an intersected set1": function testBasicAssignment() {
-								var array1 = [1, 2, 3, 4, 5],
-										array2 = [2, 3, 6, 7, 8];
-								assert.strictEqual(Expression.parseOperand({
-										$setintersection: ["$array1", "$array2"]
-								}).evaluateInternal({
-										array1: array1,
-										array2: array2
-								}), [2, 3]);
-						},
-
-						"Should pass and return an intersected set2": function testBasicAssignment() {
-								var array1 = [1, 2, 3, 4, 5],
-										array2 = [7, 8, 9];
-								assert.strictEqual(Expression.parseOperand({
-										$setintersection: ["$array1", "$array2"]
-								}).evaluateInternal({
-										array1: array1,
-										array2: array2
-								}), []);
-						},
-
-				}
+	SetIntersectionExpression = require("../../../../lib/pipeline/expressions/SetIntersectionExpression"),
+	ExpectedResultBase = require("./SetExpectedResultBase");
 
+// 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.SetIntersectionExpression = {
+
+	"constructor()": {
+
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function() {
+				new SetIntersectionExpression();
+			});
+		},
+
+		"should throw Error when constructing with args": function() {
+			assert.throws(function() {
+				new SetIntersectionExpression("someArg");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $setIntersection": function() {
+			assert.equal(new SetIntersectionExpression().getOpName(), "$setIntersection");
 		}
 
-};
+	},
+
+	"#evaluate()": {
+
+		"should handle when sets are the same": function Same(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						$setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set has redundant items": function Redundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						$setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the both sets have redundant items": function DoubleRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 1, 2], [1, 2, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						$setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a superset": function Super(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						$setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is a superset and has redundant items": function SuperWithRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 2], [1]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						$setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a subset": function Sub(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1], [1, 2]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: false,
+						$setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"should handle when the sets are the same but backwards": function SameBackwards(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [2, 1]],
+					expected: {
+						// $setIsSubset: true,
+						// $setEquals: true,
+						$setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets do not overlap": function NoOverlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 4]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						$setIntersection: [],
+						// $setUnion: [1, 2, 4, 8],
+						// $setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets do overlap": function Overlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 2, 4]],
+					expected: {
+						// $setIsSubset: false,
+						// $setEquals: false,
+						$setIntersection: [2],
+						// $setUnion: [1, 2, 4, 8],
+						// $setDifference: [1],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is null": function LastNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], null],
+					expected: {
+						$setIntersection: null,
+						// $setUnion: null,
+						// $setDifference: null,
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is null": function FirstNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [null, [1, 2]],
+					expected: {
+						$setIntersection: null,
+						// $setUnion: null,
+						// $setDifference: null,
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has no args": function NoArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [],
+					expected: {
+						$setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has one arg": function OneArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						$setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty arg": function EmptyArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						$setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty left arg": function LeftArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						$setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						// "$setEquals"
+						// "$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty right arg": function RightArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], []],
+					expected: {
+						$setIntersection: [],
+						// $setUnion: [1, 2],
+						// $setIsSubset: false,
+						// $setEquals: false,
+						// $setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args": function ManyArgs(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[8, 3], ["asdf", "foo"], [80.3, 34], [], [80.3, "foo", 11, "yay"]],
+					expected: {
+						$setIntersection: [],
+						// $setEquals: false,
+						// $setUnion: [3, 8, 11, 34, 80.3, "asdf", "foo", "yay"],
+					},
+					error: [
+						// "$setIsSubset",
+						// "$setDifference",
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args that are equal sets": function ManyArgsEqual(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 4], [1, 2, 2, 4], [4, 1, 2], [2, 1, 1, 4]],
+					expected: {
+						$setIntersection: [1, 2, 4],
+						// $setEquals: true,
+						// $setUnion: [1, 2, 4],
+					},
+					error: [
+						// "$setIsSubset",
+						// "$setDifference",
+					],
+				},
+			}).run();
+		},
+
+	},
+
+};

+ 321 - 94
test/lib/pipeline/expressions/SetIsSubsetExpression.js

@@ -1,98 +1,325 @@
 "use strict";
 var assert = require("assert"),
-		SetIsSubsetExpression = require("../../../../lib/pipeline/expressions/SetIsSubsetExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-		"SetIsSubsetExpression": {
-
-				"constructor()": {
-
-						"should throw Error when constructing without args": function testConstructor() {
-								assert.throws(function() {
-										new SetIsSubsetExpression();
-								});
-						}
-
-				},
-
-				"#getOpName()": {
-
-						"should return the correct op name; $setissubset": function testOpName() {
-								assert.equal(new SetIsSubsetExpression([1,2,3],[4,5,6]).getOpName(), "$setissubset");
-						}
-
-				},
-
-				"#evaluateInternal()": {
-
-						"Should fail if array1 is not an array": function testArg1() {
-								var array1 = "not an array",
-										array2 = [6, 7, 8, 9];
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setissubset: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if array2 is not an array": function testArg2() {
-								var array1 = [1, 2, 3, 4],
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setissubset: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if both are not an array": function testArg1andArg2() {
-								var array1 = "not an array",
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setissubset: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should pass and return a true": function testBasicAssignment() {
-								var array1 = [1, 2, 3, 4, 5],
-										array2 = [2,3];
-								assert.strictEqual(Expression.parseOperand({
-										$setissubset: ["$array1", "$array2"]
-								}).evaluateInternal({
-										array1: array1,
-										array2: array2
-								}), true);
-						},
-
-						"Should pass and return false": function testBasicAssignment() {
-								var array1 = [1, 2, 3, 4, 5],
-										array2 = [7, 8, 9];
-								assert.strictEqual(Expression.parseOperand({
-										$setissubset: ["$array1", "$array2"]
-								}).evaluateInternal({
-										array1: array1,
-										array2: array2
-								}), true);
-						},
-
-				}
-
-		}
+	SetIsSubsetExpression = require("../../../../lib/pipeline/expressions/SetIsSubsetExpression"),
+	ExpectedResultBase = require("./SetExpectedResultBase");
 
-};
+// 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.SetIsSubsetExpression = {
+
+	"constructor()": {
+
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function() {
+				new SetIsSubsetExpression();
+			});
+		},
+
+		"should throw Error when constructing with args": function() {
+			assert.throws(function() {
+				new SetIsSubsetExpression("someArg");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $setIsSubset": function() {
+			assert.equal(new SetIsSubsetExpression().getOpName(), "$setIsSubset");
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should handle when sets are the same": function Same(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2]],
+					expected: {
+						$setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set has redundant items": function Redundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2, 2]],
+					expected: {
+						$setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the both sets have redundant items": function DoubleRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 1, 2], [1, 2, 2]],
+					expected: {
+						$setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a superset": function Super(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1]],
+					expected: {
+						$setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is a superset and has redundant items": function SuperWithRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 2], [1]],
+					expected: {
+						$setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a subset": function Sub(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1], [1, 2]],
+					expected: {
+						$setIsSubset: true,
+						// $setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets are the same but backwards": function SameBackwards(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [2, 1]],
+					expected: {
+						$setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"should handle when the sets do not overlap": function NoOverlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 4]],
+					expected: {
+						$setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [],
+						// $setUnion: [1, 2, 4, 8],
+						// $setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets do overlap": function Overlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 2, 4]],
+					expected: {
+						$setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [2],
+						// $setUnion: [1, 2, 4, 8],
+						// $setDifference: [1],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is null": function LastNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], null],
+					expected: {
+						// $setIntersection: null,
+						// $setUnion: null,
+						// $setDifference: null,
+					},
+					error: [
+						// "$setEquals"
+						"$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is null": function FirstNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [null, [1, 2]],
+					expected: {
+						// $setIntersection: null,
+						// $setUnion: null,
+						// $setDifference: null,
+					},
+					error: [
+						// "$setEquals"
+						"$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has no args": function NoArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						// "$setEquals"
+						"$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has one arg": function OneArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						// "$setEquals"
+						"$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty arg": function EmptyArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						// "$setEquals"
+						"$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty left arg": function LeftArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						// "$setEquals"
+						"$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty right arg": function RightArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], []],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [1, 2],
+						$setIsSubset: false,
+						// $setEquals: false,
+						// $setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args": function ManyArgs(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[8, 3], ["asdf", "foo"], [80.3, 34], [], [80.3, "foo", 11, "yay"]],
+					expected: {
+						// $setIntersection: [],
+						// $setEquals: false,
+						// $setUnion: [3, 8, 11, 34, 80.3, "asdf", "foo", "yay"],
+					},
+					error: [
+						"$setIsSubset",
+						// "$setDifference",
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args that are equal sets": function ManyArgsEqual(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 4], [1, 2, 2, 4], [4, 1, 2], [2, 1, 1, 4]],
+					expected: {
+						// $setIntersection: [1, 2, 4],
+						// $setEquals: true,
+						// $setUnion: [1, 2, 4],
+					},
+					error: [
+						"$setIsSubset",
+						// "$setDifference",
+					],
+				},
+			}).run();
+		},
+
+	},
+
+};

+ 319 - 113
test/lib/pipeline/expressions/SetUnionExpression.js

@@ -1,119 +1,325 @@
 "use strict";
 var assert = require("assert"),
-		SetUnionExpression = require("../../../../lib/pipeline/expressions/SetUnionExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-		"SetUnionExpression": {
-
-				"constructor()": {
-
-						"should throw Error when constructing without args": function testConstructor() {
-								assert.throws(function() {
-										new SetUnionExpression();
-								});
-						}
-
-				},
-
-				"#getOpName()": {
-
-						"should return the correct op name; $setunion": function testOpName() {
-								assert.equal(new SetUnionExpression([1, 2, 3], [4, 5, 6]).getOpName(), "$setunion");
-						}
-
-				},
-
-				"#evaluateInternal()": {
-
-						"Should fail if array1 is not an array": function testArg1() {
-								var array1 = "not an array",
-										array2 = [6, 7, 8, 9];
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setunion: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if array2 is not an array": function testArg2() {
-								var array1 = [1, 2, 3, 4],
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setunion: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should fail if both are not an array": function testArg1andArg2() {
-								var array1 = "not an array",
-										array2 = "not an array";
-								assert.throws(function() {
-										Expression.parseOperand({
-												$setunion: ["$array1", "$array2"]
-										}).evaluateInternal({
-												array1: array1,
-												array2: array2
-										});
-								});
-						},
-
-						"Should pass and return a unioned set1": function testBasicAssignment() {
-								var array1 = {
-										"a": "3",
-										"c": "4"
-								},
-										array2 = {
-												"a": "3",
-												"b": "3"
-										};
-								assert.strictEqual(Expression.parseOperand({
-										$setunion: ["$array1", "$array2"]
-								}).evaluateInternal({
-										array1: array1,
-										array2: array2
-								}), {
-										"a": "3",
-										"c": "4",
-										"b": "3"
-								});
-						},
-
-						"Should pass and return a unioned set": function testBasicAssignment() {
-								var array1 = [1, 2, 3, 4, 5],
-										array2 = [2, 3, 6, 7, 8];
-								assert.strictEqual(Expression.parseOperand({
-										$setunion: ["$array1", "$array2"]
-								}).evaluateInternal({
-										array1: array1,
-										array2: array2
-								}), [1, 2, 3, 4, 5, 6, 7, 8]);
-						},
-
-						"Should pass and return unioned set": function testBasicAssignment() {
-								var array1 = [1, 2, 3, 4, 5],
-										array2 = [7, 8, 9];
-								assert.strictEqual(Expression.parseOperand({
-										$setunion: ["$array1", "$array2"]
-								}).evaluateInternal({
-										array1: array1,
-										array2: array2
-								}), [1, 2, 3, 4, 5, 7, 8, 9]);
-						},
-
-				}
+	SetUnionExpression = require("../../../../lib/pipeline/expressions/SetUnionExpression"),
+	ExpectedResultBase = require("./SetExpectedResultBase");
 
+// 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.SetUnionExpression = {
+
+	"constructor()": {
+
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function() {
+				new SetUnionExpression();
+			});
+		},
+
+		"should throw Error when constructing with args": function() {
+			assert.throws(function() {
+				new SetUnionExpression("someArg");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $setUnion": function() {
+			assert.equal(new SetUnionExpression().getOpName(), "$setUnion");
 		}
 
-};
+	},
+
+	"#evaluate()": {
+
+		"should handle when sets are the same": function Same(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2]],
+					expected: {
+						$setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set has redundant items": function Redundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1, 2, 2]],
+					expected: {
+						$setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the both sets have redundant items": function DoubleRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 1, 2], [1, 2, 2]],
+					expected: {
+						$setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a superset": function Super(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [1]],
+					expected: {
+						$setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is a superset and has redundant items": function SuperWithRedundant(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 2], [1]],
+					expected: {
+						$setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is a subset": function Sub(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1], [1, 2]],
+					expected: {
+						$setIsSubset: true,
+						// $setEquals: false,
+						// $setIntersection: [1],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"should handle when the sets are the same but backwards": function SameBackwards(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [2, 1]],
+					expected: {
+						$setIsSubset: true,
+						// $setEquals: true,
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+						// $setDifference: [],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets do not overlap": function NoOverlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 4]],
+					expected: {
+						$setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [],
+						// $setUnion: [1, 2, 4, 8],
+						// $setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the sets do overlap": function Overlap(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], [8, 2, 4]],
+					expected: {
+						$setIsSubset: false,
+						// $setEquals: false,
+						// $setIntersection: [2],
+						// $setUnion: [1, 2, 4, 8],
+						// $setDifference: [1],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the 2nd set is null": function LastNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], null],
+					expected: {
+						// $setIntersection: null,
+						// $setUnion: null,
+						// $setDifference: null,
+					},
+					error: [
+						// "$setEquals"
+						"$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the 1st set is null": function FirstNull(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [null, [1, 2]],
+					expected: {
+						// $setIntersection: null,
+						// $setUnion: null,
+						// $setDifference: null,
+					},
+					error: [
+						// "$setEquals"
+						"$setIsSubset"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has no args": function NoArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						// "$setEquals"
+						"$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has one arg": function OneArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						// "$setEquals"
+						"$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty arg": function EmptyArg(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2]],
+					expected: {
+						// $setIntersection: [1, 2],
+						// $setUnion: [1, 2],
+					},
+					error: [
+						// "$setEquals"
+						"$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty left arg": function LeftArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [],
+					},
+					error: [
+						// "$setEquals"
+						"$setIsSubset"
+						// "$setDifference"
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has empty right arg": function RightArgEmpty(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2], []],
+					expected: {
+						// $setIntersection: [],
+						// $setUnion: [1, 2],
+						$setIsSubset: false,
+						// $setEquals: false,
+						// $setDifference: [1, 2],
+					},
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args": function ManyArgs(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[8, 3], ["asdf", "foo"], [80.3, 34], [], [80.3, "foo", 11, "yay"]],
+					expected: {
+						// $setIntersection: [],
+						// $setEquals: false,
+						// $setUnion: [3, 8, 11, 34, 80.3, "asdf", "foo", "yay"],
+					},
+					error: [
+						"$setIsSubset",
+						// "$setDifference",
+					],
+				},
+			}).run();
+		},
+
+		"should handle when the input has many args that are equal sets": function ManyArgsEqual(){
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1, 2, 4], [1, 2, 2, 4], [4, 1, 2], [2, 1, 1, 4]],
+					expected: {
+						// $setIntersection: [1, 2, 4],
+						// $setEquals: true,
+						// $setUnion: [1, 2, 4],
+					},
+					error: [
+						"$setIsSubset",
+						// "$setDifference",
+					],
+				},
+			}).run();
+		},
+
+	},
+
+};

+ 74 - 46
test/lib/pipeline/expressions/ToLowerExpression_test.js

@@ -1,64 +1,92 @@
 "use strict";
 var assert = require("assert"),
-		ToLowerExpression = require("../../../../lib/pipeline/expressions/ToLowerExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
+	ToLowerExpression = require("../../../../lib/pipeline/expressions/ToLowerExpression"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
+	utils = require("./utils"),
+	constify = utils.constify,
+	expressionToJson = utils.expressionToJson;
 
+// 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 = {
+var TestBase = function TestBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	},
+	ExpectedResultBase = (function() {
+		var klass = function ExpectedResultBase() {
+			base.apply(this, arguments);
+		}, base = TestBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function(){
+			var specElement = this.spec(),
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(constify(specElement), expressionToJson(expr));
+			assert.strictEqual(this.expectedResult, expr.evaluate({}));
+		};
+		proto.spec = function() {
+			return {$toLower:[this.str]};
+		};
+		return klass;
+	})();
 
-		"ToLowerExpression": {
+exports.ToLowerExpression = {
 
-				"constructor()": {
+	"constructor()": {
 
-						"should not throw Error when constructing without args": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new ToLowerExpression();
-								});
-						},
+		"should construct instance": function() {
+			assert(new ToLowerExpression() instanceof ToLowerExpression);
+			assert(new ToLowerExpression() instanceof Expression);
+		},
 
-					"should throw Error when constructing with args": function testConstructor(){
-						assert.throws(function(){
-							new ToLowerExpression(1);
-						});
-					}
+		"should error if given args": function() {
+			assert.throws(function() {
+				new ToLowerExpression("bad stuff");
+			});
+		},
 
-				},
+	},
 
-				"#getOpName()": {
+	"#getOpName()": {
 
-						"should return the correct op name; $toLower": function testOpName() {
-								assert.equal(new ToLowerExpression().getOpName(), "$toLower");
-						}
+		"should return the correct op name; $toLower": function() {
+			assert.equal(new ToLowerExpression().getOpName(), "$toLower");
+		},
 
-				},
+	},
 
-				"#evaluate()": {
+	"#evaluate()": {
 
-						"should return the lowercase version of the string if there is a null character in the middle of the string": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$toLower: "$a"
-								}).evaluate({
-										a: "a\0B"
-								}), "a\0b");
-						},
-						"should return the lowercase version of the string if there is a null character at the beginning of the string": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$toLower: "$a"
-								}).evaluate({
-										a: "\0aB"
-								}), "\0ab");
-						},
-						"should return the lowercase version of the string if there is a null character at the end of the string": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$toLower: "$a"
-								}).evaluate({
-										a: "aB\0"
-								}), "ab\0");
-						}
-				}
+		"should return the lowercase version of the string if there is a null character at the beginning of the string": function NullBegin() {
+			/** String beginning with a null character. */
+			new ExpectedResultBase({
+				str: "\0aB",
+				expectedResult: "\0ab",
+			}).run();
+		},
 
-		}
+		"should return the lowercase version of the string if there is a null character in the middle of the string": function NullMiddle() {
+			/** String containing a null character. */
+			new ExpectedResultBase({
+				str: "a\0B",
+				expectedResult: "a\0b",
+			}).run();
+		},
+
+		"should return the lowercase version of the string if there is a null character at the end of the string": function NullEnd() {
+			/** String ending with a null character. */
+			new ExpectedResultBase({
+				str: "aB\0",
+				expectedResult: "ab\0",
+			}).run();
+		},
+
+	},
 
 };
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+if (!module.parent)(new (require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 85 - 57
test/lib/pipeline/expressions/ToUpperExpression_test.js

@@ -1,64 +1,92 @@
 "use strict";
 var assert = require("assert"),
-		ToUpperExpression = require("../../../../lib/pipeline/expressions/ToUpperExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-		"ToUpperExpression": {
-
-				"constructor()": {
-
-						"should not throw Error when constructing without args": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new ToUpperExpression();
-								});
-						},
-
-					"should throw Error when constructing with args": function testConstructor(){
-						assert.throws(function(){
-							new ToUpperExpression(1);
-						});
-					}
-
-				},
-
-				"#getOpName()": {
-
-						"should return the correct op name; $toUpper": function testOpName() {
-								assert.equal(new ToUpperExpression().getOpName(), "$toUpper");
-						}
-
-				},
-
-				"#evaluateInternal()": {
-
-						"should return the uppercase version of the string if there is a null character in the middle of the string": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$toUpper: "$a"
-								}).evaluateInternal({
-										a: "a\0B"
-								}), "A\0B");
-						},
-						"should return the uppercase version of the string if there is a null character at the beginning of the string": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$toUpper: "$a"
-								}).evaluateInternal({
-										a: "\0aB"
-								}), "\0AB");
-						},
-						"should return the uppercase version of the string if there is a null character at the end of the string": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$toUpper: "$a"
-								}).evaluateInternal({
-										a: "aB\0"
-								}), "AB\0");
-						}
-				}
+	ToUpperExpression = require("../../../../lib/pipeline/expressions/ToUpperExpression"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
+	utils = require("./utils"),
+	constify = utils.constify,
+	expressionToJson = utils.expressionToJson;
 
+// 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 TestBase = function TestBase(overrides) {
+		//NOTE: DEVIATION FROM MONGO: using this base class to make things easier to initialize
+		for (var key in overrides)
+			this[key] = overrides[key];
+	},
+	ExpectedResultBase = (function() {
+		var klass = function ExpectedResultBase() {
+			base.apply(this, arguments);
+		}, base = TestBase, proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function(){
+			var specElement = this.spec(),
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(constify(specElement), expressionToJson(expr));
+			assert.strictEqual(this.expectedResult, expr.evaluate({}));
+		};
+		proto.spec = function() {
+			return {$toUpper:[this.str]};
+		};
+		return klass;
+	})();
+
+exports.ToUpperExpression = {
+
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new ToUpperExpression() instanceof ToUpperExpression);
+			assert(new ToUpperExpression() instanceof Expression);
+		},
+
+		"should error if given args": function() {
+			assert.throws(function() {
+				new ToUpperExpression("bad stuff");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $toUpper": function() {
+			assert.equal(new ToUpperExpression().getOpName(), "$toUpper");
 		}
 
+	},
+
+	"#evaluate()": {
+
+		"should return the uppercase version of the string if there is a null character at the beginning of the string": function NullBegin() {
+			/** String beginning with a null character. */
+			new ExpectedResultBase({
+				str: "\0aB",
+				expectedResult: "\0AB",
+			}).run();
+		},
+
+		"should return the uppercase version of the string if there is a null character in the middle of the string": function NullMiddle() {
+			/** String containing a null character. */
+			new ExpectedResultBase({
+				str: "a\0B",
+				expectedResult: "A\0B",
+			}).run();
+		},
+
+		"should return the uppercase version of the string if there is a null character at the end of the string": function NullEnd() {
+			/** String ending with a null character. */
+			new ExpectedResultBase({
+				str: "aB\0",
+				expectedResult: "AB\0",
+			}).run();
+		},
+
+	},
+
 };
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+if (!module.parent)(new (require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 242 - 235
test/lib/pipeline/expressions/Variables.js

@@ -2,251 +2,258 @@
 var assert = require("assert"),
 	Variables = require("../../../../lib/pipeline/expressions/Variables");
 
+// 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.Variables = {
 
-	"Variables": {
+	"constructor": {
 
-		"constructor": {
+		"should be able to construct empty variables": function() {
+			new Variables();
+		},
+
+		"should be able to give number of variables": function() {
+			new Variables(5);
+		},
 
-			"Should be able to construct empty variables": function canConstructEmpty() {
+		"should throw if not given a number": function() {
+			assert.throws(function() {
+				new Variables('hi');
+			});
+			assert.throws(function() {
+				new Variables({});
+			});
+			assert.throws(function() {
+				new Variables([]);
+			});
+			assert.throws(function() {
+				new Variables(new Date());
+			});
+		},
+
+		"setValue throws if no args given": function() {
+			assert.throws(function() {
 				var variables = new Variables();
-			},
+				variables.setValue(1, 'hi');
+			});
+		},
 
-			"Should be able to give number of variables": function giveNumber() {
-				var variables = new Variables(5);
-			},
-
-			"Should throw if not given a number": function throwsOnInvalid() {
-				assert.throws(function() {
-					var variables = new Variables('hi');
-				});
-				assert.throws(function() {
-					var variables = new Variables({});
-				});
-				assert.throws(function() {
-					var variables = new Variables([]);
-				});
-				assert.throws(function() {
-					var variables = new Variables(new Date());
-				});
-			},
-
-			"setValue throws if no args given": function setValueThrows() {
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.setValue(1, 'hi');
-				});
-				
-			}
-
-		},
-
-		"#setRoot": {
-			"should set the _root variable to the passed value": function setsRoot() {
-				var variables = new Variables(),
-					root = {'hi':'hi'};
-				variables.setRoot(root);
-				assert.equal(root, variables._root);
-			},
-
-			"must be an object": function mustBeObject() {
-				var variables = new Variables(),
-					root = 'hi';
-				assert.throws(function() {
-					variables.setRoot(root);
-				});
-			}
-		},
-
-		"#clearRoot": {
-			"should set the _root variable to empty obj": function setsRootToEmpty() {
-				var variables = new Variables(),
-					root = {'hi':'hi'};
-				variables.setRoot(root);
-				variables.clearRoot();
-				assert.deepEqual({}, variables._root);
-			}
+	},
+
+	"#setRoot": {
+
+		"should set the _root variable to the passed value": function() {
+			var variables = new Variables(),
+				root = {'hi':'hi'};
+			variables.setRoot(root);
+			assert.equal(root, variables._root);
 		},
 
-		"#getRoot": {
-			"should return the _root variable": function returnsRoot() {
-				var variables = new Variables(),
-					root = {'hi':'hi'};
+		"must be an object": function mustBeObject() {
+			var variables = new Variables(),
+				root = 'hi';
+			assert.throws(function() {
 				variables.setRoot(root);
-				assert.equal(root, variables.getRoot());
-			}
-		},
-
-		"#setValue": {
-			"id must be number": function idMustBeNumber() {
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.setValue('hi', 5);
-				});
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.setValue(null, 5);
-				});
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.setValue(new Date(), 5);
-				});
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.setValue([], 5);
-				});
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.setValue({}, 5);
-				});
-				assert.doesNotThrow(function() {
-					var variables = new Variables(5);
-					variables.setValue(1, 5);
-				});
-			},
-
-			"cannot use root id": function cannotUseRootId() {
-				assert.throws(function() {
-					var variables = new Variables(5);
-					variables.setValue(Variables.ROOT_ID, 'hi');
-				});
-			},
-
-			"cannot use id larger than initial size": function idSizeIsCorrect() {
-				assert.throws(function() {
-					var variables = new Variables(5);
-					variables.setValue(5, 'hi'); //off by one check
-				});
-				assert.throws(function() {
-					var variables = new Variables(5);
-					variables.setValue(6, 'hi');
-				});
-			},
-
-			"sets the value": function setsTheValue() {
+			});
+		},
+
+	},
+
+	"#clearRoot": {
+
+		"should set the _root variable to empty obj": function() {
+			var variables = new Variables(),
+				root = {'hi':'hi'};
+			variables.setRoot(root);
+			variables.clearRoot();
+			assert.deepEqual({}, variables._root);
+		},
+
+	},
+
+	"#getRoot": {
+
+		"should return the _root variable": function() {
+			var variables = new Variables(),
+				root = {'hi':'hi'};
+			variables.setRoot(root);
+			assert.equal(root, variables.getRoot());
+		},
+
+	},
+
+	"#setValue": {
+
+		"id must be number": function() {
+			assert.throws(function() {
+				var variables = new Variables();
+				variables.setValue('hi', 5);
+			});
+			assert.throws(function() {
+				var variables = new Variables();
+				variables.setValue(null, 5);
+			});
+			assert.throws(function() {
+				var variables = new Variables();
+				variables.setValue(new Date(), 5);
+			});
+			assert.throws(function() {
+				var variables = new Variables();
+				variables.setValue([], 5);
+			});
+			assert.throws(function() {
+				var variables = new Variables();
+				variables.setValue({}, 5);
+			});
+			assert.doesNotThrow(function() {
 				var variables = new Variables(5);
-				variables.setValue(1, 'hi'); //off by one check
-				assert.equal(variables._rest[1], 'hi');
-			}
-		},
-
-		"#getValue": {
-			"id must be number": function idMustBeNumber() {
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.getValue('hi', 5);
-				});
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.getValue(null, 5);
-				});
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.getValue(new Date(), 5);
-				});
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.getValue([], 5);
-				});
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.getValue({}, 5);
-				});
-				assert.doesNotThrow(function() {
-					var variables = new Variables(5);
-					variables.getValue(1, 5);
-				});
-			},
-
-			"returns root when given root id": function returnsRoot() {
-				var variables = new Variables(5),
-					root = {hi:'hi'};
-				variables.setRoot(root);
-				variables.getValue(Variables.ROOT_ID, root);
-			},
-
-			"cannot use id larger than initial size": function idSizeIsCorrect() {
-				assert.throws(function() {
-					var variables = new Variables(5);
-					variables.getValue(5, 'hi'); //off by one check
-				});
-				assert.throws(function() {
-					var variables = new Variables(5);
-					variables.getValue(6, 'hi');
-				});
-			},
-
-			"gets the value": function getsTheValue() {
+				variables.setValue(1, 5);
+			});
+		},
+
+		"cannot use root id": function() {
+			assert.throws(function() {
 				var variables = new Variables(5);
-				variables.setValue(1, 'hi');
-				assert.equal(variables.getValue(1), 'hi');
-			}
-		},
-
-		"#getDocument": {
-			"id must be number": function idMustBeNumber() {
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.getDocument('hi', 5);
-				});
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.getDocument(null, 5);
-				});
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.getDocument(new Date(), 5);
-				});
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.getDocument([], 5);
-				});
-				assert.throws(function() {
-					var variables = new Variables();
-					variables.getDocument({}, 5);
-				});
-				assert.doesNotThrow(function() {
-					var variables = new Variables(5);
-					variables.getDocument(1, 5);
-				});
-			},
-
-			"returns root when given root id": function returnsRoot() {
-				var variables = new Variables(5),
-					root = {hi:'hi'};
-				variables.setRoot(root);
-				variables.getDocument(Variables.ROOT_ID, root);
-			},
-
-			"cannot use id larger than initial size": function idSizeIsCorrect() {
-				assert.throws(function() {
-					var variables = new Variables(5);
-					variables.getDocument(5, 'hi'); //off by one check
-				});
-				assert.throws(function() {
-					var variables = new Variables(5);
-					variables.getDocument(6, 'hi');
-				});
-			},
-
-			"gets the value": function getsTheDocument() {
-				var variables = new Variables(5),
-					value = {hi:'hi'};
-				variables.setValue(1, value);
-				assert.equal(variables.getDocument(1), value);
-			},
-
-			"only returns documents": function returnsOnlyDocs() {
-				var variables = new Variables(5),
-					value = 'hi';
-				variables.setValue(1, value);
-				assert.deepEqual(variables.getDocument(1), {});
-			}
-		}
-
-	}
+				variables.setValue(Variables.ROOT_ID, 'hi');
+			});
+		},
 
-};
+		"cannot use id larger than initial size": function() {
+			assert.throws(function() {
+				var variables = new Variables(5);
+				variables.setValue(5, 'hi'); //off by one check
+			});
+			assert.throws(function() {
+				var variables = new Variables(5);
+				variables.setValue(6, 'hi');
+			});
+		},
+
+		"sets the value": function() {
+			var variables = new Variables(5);
+			variables.setValue(1, 'hi'); //off by one check
+			assert.equal(variables._rest[1], 'hi');
+		},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+	},
+
+	"#getValue": {
+
+		"id must be number": function() {
+			assert.throws(function() {
+				var variables = new Variables();
+				variables.getValue('hi', 5);
+			});
+			assert.throws(function() {
+				var variables = new Variables();
+				variables.getValue(null, 5);
+			});
+			assert.throws(function() {
+				var variables = new Variables();
+				variables.getValue(new Date(), 5);
+			});
+			assert.throws(function() {
+				var variables = new Variables();
+				variables.getValue([], 5);
+			});
+			assert.throws(function() {
+				var variables = new Variables();
+				variables.getValue({}, 5);
+			});
+			assert.doesNotThrow(function() {
+				var variables = new Variables(5);
+				variables.getValue(1, 5);
+			});
+		},
+
+		"returns root when given root id": function() {
+			var variables = new Variables(5),
+				root = {hi:'hi'};
+			variables.setRoot(root);
+			variables.getValue(Variables.ROOT_ID, root);
+		},
+
+		"cannot use id larger than initial size": function() {
+			assert.throws(function() {
+				var variables = new Variables(5);
+				variables.getValue(5, 'hi'); //off by one check
+			});
+			assert.throws(function() {
+				var variables = new Variables(5);
+				variables.getValue(6, 'hi');
+			});
+		},
+
+		"gets the value": function() {
+			var variables = new Variables(5);
+			variables.setValue(1, 'hi');
+			assert.equal(variables.getValue(1), 'hi');
+		},
+
+	},
+
+	"#getDocument": {
+
+		"id must be number": function() {
+			assert.throws(function() {
+				var variables = new Variables();
+				variables.getDocument('hi', 5);
+			});
+			assert.throws(function() {
+				var variables = new Variables();
+				variables.getDocument(null, 5);
+			});
+			assert.throws(function() {
+				var variables = new Variables();
+				variables.getDocument(new Date(), 5);
+			});
+			assert.throws(function() {
+				var variables = new Variables();
+				variables.getDocument([], 5);
+			});
+			assert.throws(function() {
+				var variables = new Variables();
+				variables.getDocument({}, 5);
+			});
+			assert.doesNotThrow(function() {
+				var variables = new Variables(5);
+				variables.getDocument(1, 5);
+			});
+		},
+
+		"returns root when given root id": function() {
+			var variables = new Variables(5),
+				root = {hi:'hi'};
+			variables.setRoot(root);
+			variables.getDocument(Variables.ROOT_ID, root);
+		},
+
+		"cannot use id larger than initial size": function() {
+			assert.throws(function() {
+				var variables = new Variables(5);
+				variables.getDocument(5, 'hi'); //off by one check
+			});
+			assert.throws(function() {
+				var variables = new Variables(5);
+				variables.getDocument(6, 'hi');
+			});
+		},
+
+		"gets the value": function() {
+			var variables = new Variables(5),
+				value = {hi:'hi'};
+			variables.setValue(1, value);
+			assert.equal(variables.getDocument(1), value);
+		},
+
+		"only returns documents": function() {
+			var variables = new Variables(5),
+				value = 'hi';
+			variables.setValue(1, value);
+			assert.deepEqual(variables.getDocument(1), {});
+		},
+
+	},
+
+};