Browse Source

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

Kyle P Davis 11 năm trước cách đây
mục cha
commit
95131f3105
77 tập tin đã thay đổi với 4798 bổ sung3336 xóa
  1. 2 2
      lib/pipeline/Document.js
  2. 10 10
      lib/pipeline/Value.js
  3. 13 28
      lib/pipeline/expressions/AllElementsTrueExpression.js
  4. 1 1
      lib/pipeline/expressions/AndExpression.js
  5. 12 29
      lib/pipeline/expressions/AnyElementTrueExpression.js
  6. 83 78
      lib/pipeline/expressions/CompareExpression.js
  7. 25 24
      lib/pipeline/expressions/ConcatExpression.js
  8. 32 87
      lib/pipeline/expressions/CondExpression.js
  9. 11 23
      lib/pipeline/expressions/DayOfMonthExpression.js
  10. 13 24
      lib/pipeline/expressions/DayOfWeekExpression.js
  11. 17 32
      lib/pipeline/expressions/DayOfYearExpression.js
  12. 22 23
      lib/pipeline/expressions/DivideExpression.js
  13. 6 12
      lib/pipeline/expressions/Expression.js
  14. 5 4
      lib/pipeline/expressions/FieldPathExpression.js
  15. 13 26
      lib/pipeline/expressions/HourExpression.js
  16. 9 24
      lib/pipeline/expressions/IfNullExpression.js
  17. 62 59
      lib/pipeline/expressions/MapExpression.js
  18. 11 25
      lib/pipeline/expressions/MillisecondExpression.js
  19. 11 23
      lib/pipeline/expressions/MinuteExpression.js
  20. 25 36
      lib/pipeline/expressions/ModExpression.js
  21. 11 23
      lib/pipeline/expressions/MonthExpression.js
  22. 30 23
      lib/pipeline/expressions/MultiplyExpression.js
  23. 11 24
      lib/pipeline/expressions/NotExpression.js
  24. 11 25
      lib/pipeline/expressions/SecondExpression.js
  25. 33 33
      lib/pipeline/expressions/SetDifferenceExpression.js
  26. 33 20
      lib/pipeline/expressions/SetEqualsExpression.js
  27. 43 22
      lib/pipeline/expressions/SetIntersectionExpression.js
  28. 69 61
      lib/pipeline/expressions/SetIsSubsetExpression.js
  29. 25 27
      lib/pipeline/expressions/SetUnionExpression.js
  30. 9 20
      lib/pipeline/expressions/SizeExpression.js
  31. 21 27
      lib/pipeline/expressions/StrcasecmpExpression.js
  32. 23 30
      lib/pipeline/expressions/SubstrExpression.js
  33. 35 24
      lib/pipeline/expressions/SubtractExpression.js
  34. 8 17
      lib/pipeline/expressions/ToLowerExpression.js
  35. 88 0
      lib/pipeline/expressions/ValueSet.js
  36. 94 74
      lib/pipeline/expressions/Variables.js
  37. 8 8
      lib/pipeline/expressions/VariablesIdGenerator.js
  38. 26 23
      lib/pipeline/expressions/VariablesParseState.js
  39. 13 30
      lib/pipeline/expressions/WeekExpression.js
  40. 8 23
      lib/pipeline/expressions/YearExpression.js
  41. 148 49
      test/lib/pipeline/expressions/AllElementsTrueExpression.js
  42. 142 96
      test/lib/pipeline/expressions/AnyElementTrueExpression.js
  43. 409 308
      test/lib/pipeline/expressions/CompareExpression.js
  44. 63 43
      test/lib/pipeline/expressions/ConcatExpression_test.js
  45. 0 72
      test/lib/pipeline/expressions/CondExpression.js
  46. 93 104
      test/lib/pipeline/expressions/CondExpression_test.js
  47. 29 59
      test/lib/pipeline/expressions/DayOfMonthExpression.js
  48. 23 28
      test/lib/pipeline/expressions/DayOfWeekExpression.js
  49. 23 28
      test/lib/pipeline/expressions/DayOfYearExpression.js
  50. 47 0
      test/lib/pipeline/expressions/DivideExpression_test.js
  51. 23 28
      test/lib/pipeline/expressions/HourExpression.js
  52. 0 58
      test/lib/pipeline/expressions/IfNullExpression.js
  53. 40 45
      test/lib/pipeline/expressions/IfNullExpression_test.js
  54. 115 0
      test/lib/pipeline/expressions/MapExpression_test.js
  55. 28 43
      test/lib/pipeline/expressions/MillisecondExpression.js
  56. 23 28
      test/lib/pipeline/expressions/MinuteExpression.js
  57. 62 35
      test/lib/pipeline/expressions/ModExpression.js
  58. 23 28
      test/lib/pipeline/expressions/MonthExpression.js
  59. 91 30
      test/lib/pipeline/expressions/MultiplyExpression_test.js
  60. 0 45
      test/lib/pipeline/expressions/NotExpression.js
  61. 47 0
      test/lib/pipeline/expressions/NotExpression_test.js
  62. 23 34
      test/lib/pipeline/expressions/SecondExpression.js
  63. 321 83
      test/lib/pipeline/expressions/SetDifferenceExpression.js
  64. 321 83
      test/lib/pipeline/expressions/SetEqualsExpression.js
  65. 59 0
      test/lib/pipeline/expressions/SetExpectedResultBase.js
  66. 319 111
      test/lib/pipeline/expressions/SetIntersectionExpression.js
  67. 321 94
      test/lib/pipeline/expressions/SetIsSubsetExpression.js
  68. 319 113
      test/lib/pipeline/expressions/SetUnionExpression.js
  69. 36 30
      test/lib/pipeline/expressions/SizeExpression.js
  70. 89 40
      test/lib/pipeline/expressions/StrcasecmpExpression.js
  71. 0 161
      test/lib/pipeline/expressions/SubstrExpression.js
  72. 109 0
      test/lib/pipeline/expressions/SubstrExpression_test.js
  73. 111 27
      test/lib/pipeline/expressions/SubtractExpression.js
  74. 70 45
      test/lib/pipeline/expressions/ToLowerExpression_test.js
  75. 242 235
      test/lib/pipeline/expressions/Variables.js
  76. 23 28
      test/lib/pipeline/expressions/WeekExpression.js
  77. 24 21
      test/lib/pipeline/expressions/YearExpression.js

+ 2 - 2
lib/pipeline/Document.js

@@ -39,7 +39,7 @@ klass.toJson = function toJson(doc) {
 //SKIPPED: most of MutableDocument except for getNestedField and setNestedField, squashed into Document here (because that's how they use it)
 function getNestedFieldHelper(obj, path) {
 	// NOTE: DEVIATION FROM MONGO: from MutableDocument; similar but necessarily different
-	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fields : path.split(".")),
+	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fieldNames : path.split(".")),
 		lastKey = keys[keys.length - 1];
 	for (var i = 0, l = keys.length - 1, cur = obj; i < l && cur instanceof Object; i++) {
 		var next = cur[keys[i]];
@@ -51,7 +51,7 @@ function getNestedFieldHelper(obj, path) {
 klass.getNestedField = getNestedFieldHelper;  // NOTE: ours is static so these are the same
 klass.setNestedField = function setNestedField(obj, path, val) {
 	// NOTE: DEVIATION FROM MONGO: from MutableDocument; similar but necessarily different
-	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fields : path.split(".")),
+	var keys = Array.isArray(path) ? path : (path instanceof FieldPath ? path.fieldNames : path.split(".")),
 		lastKey = keys[keys.length - 1];
 	for (var i = 0, l = keys.length - 1, cur = obj; i < l && cur instanceof Object; i++) {
 		var next = cur[keys[i]];

+ 10 - 10
lib/pipeline/Value.js

@@ -8,7 +8,7 @@
  * @constructor
  **/
 var Value = module.exports = function Value(){
-	if(this.constructor == Value) throw new Error("Never create instances of this! Use the static helpers only.");
+	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}});
 
 var Document;  // loaded lazily below //TODO: a dirty hack; need to investigate and clean up
@@ -27,7 +27,7 @@ var Document;  // loaded lazily below //TODO: a dirty hack; need to investigate
  * @static
  */
 klass.coerceToBool = function coerceToBool(value) {
-	if (typeof(value) == "string") return true;
+	if (typeof value === "string") return true;
 	return !!value;	// including null or undefined
 };
 
@@ -71,7 +71,7 @@ klass.coerceToDate = function coerceToDate(value) {
 //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;
+	if (type === "object") type = value === null ? "null" : value.constructor.name;
 	switch (type) {
 		//TODO: BSON numbers?
 		case "number":
@@ -131,7 +131,7 @@ klass.compare = function compare(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) {
+		if (l.constructor.name === r.constructor.name) {
 			return 0;
 		} else if (l.constructor.name === "MinKey") {
 			return -1;
@@ -154,7 +154,7 @@ klass.compare = function compare(l, r) {
 	case "string":
 		return klass.cmp(l, r);
 	case "boolean":
-		return l == r ? 0 : l ? 1 : -1;
+		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;
@@ -201,12 +201,12 @@ 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);
+	if (t === "object") t = (v === null ? "null" : v.constructor.name || t);
 	return t;
 };
 // getArrayLength(arr): arr.length
@@ -226,7 +226,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;
+	if (xType === "object") xType = x === null ? "null" : x.constructor.name;
 	switch (xType) {
 		case "MinKey":
 			return -1;

+ 13 - 28
lib/pipeline/expressions/AllElementsTrueExpression.js

@@ -6,43 +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 value = evaluateInternal(vars);
-	if (!vars instanceof Array) throw new Error("$allElementsTrue requires an array");
-
-	for (var i = 0, n = this.operands.length; i < n; ++i) {
-		var checkValue = this.operands[i].evaluateInternal(vars);
-		if (!checkValue.coerceToBool()) return false;
+	var arr = this.operands[0].evaluateInternal(vars);
+	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";
+};

+ 1 - 1
lib/pipeline/expressions/AndExpression.js

@@ -40,7 +40,7 @@ proto.evaluateInternal = function evaluateInternal(vars) {
 	return true;
 };
 
-proto.isAssociativeAndCommutative = function isAssociativeAndCommutative() { 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

+ 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";
+};

+ 83 - 78
lib/pipeline/expressions/CompareExpression.js

@@ -6,108 +6,113 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var CompareExpression = module.exports = function CompareExpression(cmpOp) {
+	if (!(arguments.length === 1 && typeof cmpOp === "string")) throw new Error(klass.name + ": args expected: cmpOp");
     this.cmpOp = cmpOp;
     base.call(this);
-}, klass = CompareExpression,
-    FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-    proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-    });
-
-// DEPENDENCIES
-var Value = require("../Value");
-var Expression = require("./Expression");
-var ConstantExpression = require("./ConstantExpression");
-var FieldPathExpression = require("./FieldPathExpression");
-var FieldRangeExpression = require("./FieldRangeExpression");
-var NaryExpression = require("./NaryExpression");
-
-// NESTED CLASSES
+}, klass = CompareExpression, base = require("./FixedArityExpressionT")(CompareExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
+
+var Value = require("../Value"),
+	Expression = require("./Expression");
+
+
+klass.parse = function parse(jsonExpr, vps, op) {
+	var expr = new CompareExpression(op),
+		args = base.parseArguments(jsonExpr, vps);
+	expr.validateArguments(args);
+	expr.operands = args;
+	return expr;
+};
+
+
 /**
  * Lookup table for truth value returns
- *
  * @param truthValues   truth value for -1, 0, 1
  * @param reverse               reverse comparison operator
  * @param name                  string name
- **/
-var CmpLookup = (function() { // emulating a struct
-	// CONSTRUCTOR
-	var klass = function CmpLookup(truthValues, reverse, name) {
-		if (arguments.length !== 3) throw new Error("args expected: truthValues, reverse, name");
-		this.truthValues = truthValues;
-		this.reverse = reverse;
-		this.name = name;
-	}, base = Object,
-		proto = klass.prototype = Object.create(base.prototype, {
-			constructor: {
-				value: klass
-			}
-		});
-	return klass;
-})();
-
-// verify we need this below
-// PRIVATE STATIC MEMBERS
+ */
+var CmpLookup = function CmpLookup(truthValues, reverse, name) { // emulating a struct
+	if (arguments.length !== 3) throw new Error("args expected: truthValues, reverse, name");
+	this.truthValues = truthValues;
+	this.reverse = reverse;
+	this.name = name;
+};
+
+
+/**
+ * Enumeration of comparison operators. Any changes to these values require adjustment of
+ * the lookup table in the implementation.
+ */
+var CmpOp = klass.CmpOp = {
+	EQ: "$eq",
+	NE: "$ne",
+	GT: "$gt",
+	GTE: "$gte",
+	LT: "$lt",
+	LTE: "$lte",
+	CMP: "$cmp",
+};
+
+
 /**
  * a table of cmp type lookups to truth values
  * @private
- **/
+ */
 var cmpLookupMap = [ //NOTE: converted from this Array to a Dict/Object below using CmpLookup#name as the key
-    //              -1      0      1      reverse             name     (taking advantage of the fact that our 'enums' are strings below)
-    new CmpLookup([false, true, false], CompareExpression.EQ, CompareExpression.EQ),
-    new CmpLookup([true, false, true], CompareExpression.NE, CompareExpression.NE),
-    new CmpLookup([false, false, true], CompareExpression.LT, CompareExpression.GT),
-    new CmpLookup([false, true, true], CompareExpression.LTE, CompareExpression.GTE),
-    new CmpLookup([true, false, false], CompareExpression.GT, CompareExpression.LT),
-    new CmpLookup([true, true, false], CompareExpression.GTE, CompareExpression.LTE),
-    new CmpLookup([false, false, false], CompareExpression.CMP, CompareExpression.CMP)
+	//              -1      0      1      reverse             name     (taking advantage of the fact that our 'enums' are strings below)
+	new CmpLookup([false, true, false], CmpOp.EQ, CmpOp.EQ),
+	new CmpLookup([true, false, true], CmpOp.NE, CmpOp.NE),
+	new CmpLookup([false, false, true], CmpOp.LT, CmpOp.GT),
+	new CmpLookup([false, true, true], CmpOp.LTE, CmpOp.GTE),
+	new CmpLookup([true, false, false], CmpOp.GT, CmpOp.LT),
+	new CmpLookup([true, true, false], CmpOp.GTE, CmpOp.LTE),
+
+	// CMP is special. Only name is used.
+	new CmpLookup([false, false, false], CmpOp.CMP, CmpOp.CMP)
 ].reduce(function(r, o) {
 	r[o.name] = o;
 	return r;
 }, {});
 
 
-klass.parse = function parse(bsonExpr, vps, op) {
-    var expr = new CompareExpression(op);
-    var args = NaryExpression.parseArguments(bsonExpr, vps);
-    expr.validateArguments(args);
-    expr.vpOperand = args;
-    return expr;
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var left = this.operands[0].evaluateInternal(vars),
+		right = this.operands[1].evaluateInternal(vars),
+		cmp = Value.compare(left, right);
 
-};
+    // Make cmp one of 1, 0, or -1.
+	if (cmp === 0) {
+		//leave as 0
+	} else if (cmp < 0) {
+		cmp = -1;
+	} else if (cmp > 0) {
+		cmp = 1;
+	}
 
-// PROTOTYPE MEMBERS
-proto.evaluateInternal = function evaluateInternal(vars) {
-	//debugger;
-    var left = this.operands[0].evaluateInternal(vars),
-        right = this.operands[1].evaluateInternal(vars),
-        cmp = Expression.signum(Value.compare(left, right));
-    if (this.cmpOp == Expression.CmpOp.CMP) return cmp;
-    return cmpLookupMap[this.cmpOp].truthValues[cmp + 1] || false;
+	if (this.cmpOp === CmpOp.CMP)
+		return cmp;
+
+	var returnValue = cmpLookupMap[this.cmpOp].truthValues[cmp + 1];
+	return returnValue;
 };
 
-klass.EQ = "$eq";
-klass.NE = "$ne";
-klass.GT = "$gt";
-klass.GTE = "$gte";
-klass.LT = "$lt";
-klass.LTE = "$lte";
-klass.CMP = "$cmp";
 
 proto.getOpName = function getOpName() {
 	return this.cmpOp;
 };
 
-/** Register Expression */
-Expression.registerExpression("$eq", klass.parse);
-Expression.registerExpression("$ne", klass.parse);
-Expression.registerExpression("$gt", klass.parse);
-Expression.registerExpression("$gte", klass.parse);
-Expression.registerExpression("$lt", klass.parse);
-Expression.registerExpression("$lte", klass.parse);
-Expression.registerExpression("$cmp", klass.parse);
+
+function bindLast(fn, lastArg) { // similar to the boost::bind used in the mongo code
+	return function() {
+		return fn.apply(this, Array.prototype.slice.call(arguments).concat([lastArg]));
+	};
+}
+Expression.registerExpression("$cmp", bindLast(klass.parse, CmpOp.CMP));
+Expression.registerExpression("$eq", bindLast(klass.parse, CmpOp.EQ));
+Expression.registerExpression("$gt", bindLast(klass.parse, CmpOp.GT));
+Expression.registerExpression("$gte", bindLast(klass.parse, CmpOp.GTE));
+Expression.registerExpression("$lt", bindLast(klass.parse, CmpOp.LT));
+Expression.registerExpression("$lte", bindLast(klass.parse, CmpOp.LTE));
+Expression.registerExpression("$ne", bindLast(klass.parse, CmpOp.NE));

+ 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";
+};

+ 32 - 87
lib/pipeline/expressions/CondExpression.js

@@ -6,108 +6,53 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var CondExpression = module.exports = function CondExpression(vars) {
-		if (arguments.length !== 0) throw new Error("zero args expected");
+ */
+var CondExpression = module.exports = function CondExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": expected args: NONE");
     base.call(this);
-}, klass = CondExpression,
-	base = require("./FixedArityExpressionT")(klass, 3),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = CondExpression, base = require("./FixedArityExpressionT")(CondExpression, 3), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-    Expression = require("./Expression"),
-	FixedArityExpressionT = require("./FixedArityExpressionT");
+    Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-klass.opName = "$cond";
-proto.getOpName = function getOpName() {
-    return klass.opName;
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var cond = this.operands[0].evaluateInternal(vars);
+	var idx = Value.coerceToBool(cond) ? 1 : 2;
+	return this.operands[idx].evaluateInternal(vars);
 };
 
-/**
- *
- * @param expr	- I expect this to be the RHS of $cond:{...} or $cond:[,,,]
- * @param vps
- * @returns {*}
- */
 klass.parse = function parse(expr, vps) {
-	// There may only be one argument - an array of 3 items, or a hash containing 3 keys.
-    //this.checkArgLimit(3);
-
-    // if not an object, return;
-	// todo I don't understand why we'd do this.  shouldn't expr be {}, [], or wrong?
-    if (typeof(expr) !== Object || )
-		return FixedArityExpressionT.parse(expr, vps);
-
-	// ...or expr could be the entirety of $cond:{...} or $cond:[,,,].
-	if(!(klass.opName in expr)) {
-		throw new Error("Invalid expression. Expected to see '"+klass.opName+"'");
+    if (Value.getType(expr) !== "Object") {
+		return base.parse(expr, vps);
 	}
+	// verify(str::equals(expr.fieldName(), "$cond")); //NOTE: DEVIATION FROM MONGO: we do not have fieldName any more and not sure this is even possible anyway
 
     var ret = new CondExpression();
-
-	// If this is an Object and not an array, verify all the bits are specified.
-	// If this is an Object that is an array, verify there are three bits.
-	// (My issue here is that we got to this parse function when we parsed the $cond:{...} item, and we're calling
-	// parseOperand (again) without altering the input.)
-//    var args = Expression.parseOperand(expr, vps);
-
-	var args = expr[getOpName()];
-
-	if (typeof args !== 'object') throw new Error("this should not happen");
-	if (args instanceof Array) {
-		// it's the array form. Convert it to the object form.
-		if (args.length !== 3) throw new Error("$cond requires exactly three arguments");
-		args = {if: args[0], then: args[1], else: args[2]};
-	}
-
-	// One way or the other, args is now in object form.
-	Object.keys(args).forEach(function(arg) {
-		if (arg === 'if') {
-			ret.operands[0] = Expression.parseOperand(args['if'], vps);
-		}
-		else if (arg === 'then') {
-			ret.operands[1] = Expression.parseOperand(args['then'], vps);
-		}
-		else if (arg === 'else') {
-			ret.operands[2] = Expression.parseOperand(args['else'], vps);
-		}
-		else {
-			throw new Error("Unrecognized parameter to $cond: '" + arg + "'; code 17083");
+	ret.operands.length = 3;
+
+	var args = expr;
+	for (var argfieldName in args) {
+		if (!args.hasOwnProperty(argfieldName)) continue;
+		if (argfieldName === "if") {
+			ret.operands[0] = Expression.parseOperand(args.if, vps);
+		} else if (argfieldName === "then") {
+			ret.operands[1] = Expression.parseOperand(args.then, vps);
+		} else if (argfieldName === "else") {
+			ret.operands[2] = Expression.parseOperand(args.else, vps);
+		} else {
+			throw new Error("Unrecognized parameter to $cond: '" + argfieldName + "'; uasserted code 17083");
 		}
-	});
+	}
 
-    if (!ret.operands[0]) throw new Error("Missing 'if' parameter to $cond; code 17080");
-    if (!ret.operands[1]) throw new Error("Missing 'then' parameter to $cond; code 17081");
-    if (!ret.operands[2]) throw new Error("Missing 'else' parameter to $cond; code 17082");
+    if (!ret.operands[0]) throw new Error("Missing 'if' parameter to $cond; uassert code 17080");
+    if (!ret.operands[1]) throw new Error("Missing 'then' parameter to $cond; uassert code 17081");
+    if (!ret.operands[2]) throw new Error("Missing 'else' parameter to $cond; uassert code 17082");
 
     return ret;
 };
 
-/**
- * Use the $cond operator with the following syntax:
- * { $cond: { if: <boolean-expression>, then: <true-case>, else: <false-case-> } }
- * -or-
- * { $cond: [ <boolean-expression>, <true-case>, <false-case> ] }
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-		var pCond1 = this.operands[0].evaluateInternal(vars);
-
-		this.idx = 0;
-		if (pCond1.coerceToBool()) {
-			this.idx = 1;
-		} else {
-			this.idx = 2;
-		}
+Expression.registerExpression("$cond", CondExpression.parse);
 
-		return this.operands[this.idx].evaluateInternal(vars);
+proto.getOpName = function getOpName() {
+	return "$cond";
 };
-
-/** Register Expression */
-Expression.registerExpression(klass.opName, klass.parse);

+ 11 - 23
lib/pipeline/expressions/DayOfMonthExpression.js

@@ -6,35 +6,23 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var DayOfMonthExpression = module.exports = function DayOfMonthExpression() {
-	base.call(this);
-}, klass = DayOfMonthExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+    if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
+    base.call(this);
+}, klass = DayOfMonthExpression, base = require("./FixedArityExpressionT")(klass, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCDate();
+};
 
-// PROTOTYPE MEMBERS
 proto.getOpName = function getOpName() {
 	return "$dayOfMonth";
 };
 
-/**
- * Takes a date and returns the day of the month as a number between 1 and 31.
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCDate();
-};
-
-/** Register Expression */
 Expression.registerExpression("$dayOfMonth", base.parse);

+ 13 - 24
lib/pipeline/expressions/DayOfWeekExpression.js

@@ -6,34 +6,23 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var DayOfWeekExpression = module.exports = function DayOfWeekExpression(){
+ */
+var DayOfWeekExpression = module.exports = function DayOfWeekExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = DayOfWeekExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor:{
-			value:klass
-		}
-	});
+}, klass = DayOfWeekExpression, base = require("./FixedArityExpressionT")(DayOfWeekExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-	return "$dayOfWeek";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCDay() + 1;
 };
 
-/**
- * Takes a date and returns the day of the week as a number between 1 (Sunday) and 7 (Saturday.)
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars){
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCDay()+1;
+proto.getOpName = function getOpName() {
+	return "$dayOfWeek";
 };
 
-/** Register Expression */
-Expression.registerExpression("$dayOfWeek",base.parse);
+Expression.registerExpression("$dayOfWeek", base.parse);

+ 17 - 32
lib/pipeline/expressions/DayOfYearExpression.js

@@ -6,42 +6,27 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var DayOfYearExpression = module.exports = function DayOfYearExpression(){
+ */
+var DayOfYearExpression = module.exports = function DayOfYearExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = DayOfYearExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor:{
-			value:klass
-		}
-	});
+}, klass = DayOfYearExpression, base = require("./FixedArityExpressionT")(DayOfYearExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-    return "$dayOfYear";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate),
+		//NOTE: DEVIATION FROM MONGO: our calculations are a little different but are equivalent
+		y11 = new Date(date.getUTCFullYear(), 0, 1), // same year, first month, first day; time omitted
+		ymd = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + 1), // same y,m,d; time omitted, add 1 because days start at 1
+		yday = Math.ceil((ymd - y11) / 86400000); // count days
+	return yday;
 };
 
-/**
- * Takes a date and returns the day of the year as a number between 1 and 366.
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars){
-	//NOTE: the below silliness is to deal with the leap year scenario when we should be returning 366
-    var date = this.operands[0].evaluateInternal(vars);
-    return klass.getDateDayOfYear(date);
-};
-
-// STATIC METHODS
-klass.getDateDayOfYear = function getDateDayOfYear(d){
-    var y11 = new Date(d.getUTCFullYear(), 0, 1),       // same year, first month, first day; time omitted
-	ymd = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()+1);  // same y,m,d; time omitted, add 1 because days start at 1
-    return Math.ceil((ymd - y11) / 86400000);   //NOTE: 86400000 ms is 1 day
+proto.getOpName = function getOpName() {
+	return "$dayOfYear";
 };
 
-/** Register Expression */
-Expression.registerExpression("$dayOfYear",base.parse);
+Expression.registerExpression("$dayOfYear", base.parse);

+ 22 - 23
lib/pipeline/expressions/DivideExpression.js

@@ -4,43 +4,42 @@
  * A $divide pipeline expression.
  * @see evaluateInternal
  * @class DivideExpression
+ * @extends mungedb-aggregate.pipeline.expressions.FixedArityExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
  **/
 var DivideExpression = module.exports = function DivideExpression(){
+    if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
     base.call(this);
-}, klass = DivideExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor:{
-			value:klass
-		}
-	});
+}, klass = DivideExpression, base = require("./FixedArityExpressionT")(DivideExpression, 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(){ //TODO: try to move this to a static and/or instance field instead of a getter function
-	return "$divide";
-};
-
 /**
  * Takes an array that contains a pair of numbers and returns the value of the first number divided by the second number.
  * @method evaluateInternal
  **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var left = this.operands[0].evaluateInternal(vars),
-		right = this.operands[1].evaluateInternal(vars);
-	if (!(left instanceof Date) && (!right instanceof Date)) throw new Error("$divide does not support dates; code 16373");
-	right = Value.coerceToDouble(right);
-	if (right === 0) return undefined;
-	left = Value.coerceToDouble(left);
-	return left / right;
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
+
+	if (typeof lhs === "number" && typeof rhs === "number") {
+        var numer = lhs,
+            denom = rhs;
+        if (denom === 0) throw new Error("can't $divide by zero; uassert code 16608");
+
+        return numer / denom;
+    } else if (lhs === undefined || lhs === null || rhs === undefined || rhs === null) {
+        return null;
+    } else{
+        throw new Error("User assertion: 16609: $divide only supports numeric types, not " + Value.getType(lhs) + " and " + Value.getType(rhs));
+    }
 };
 
-/** Register Expression */
-Expression.registerExpression("$divide",base.parse);
+Expression.registerExpression("$divide", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$divide";
+};

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

+ 13 - 26
lib/pipeline/expressions/HourExpression.js

@@ -2,40 +2,27 @@
 
 /**
  * An $hour pipeline expression.
- * @see evaluateInternal
  * @class HourExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var HourExpression = module.exports = function HourExpression(){
+ */
+var HourExpression = module.exports = function HourExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = HourExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor:{
-			value:klass
-		}
-	});
+}, klass = HourExpression, base = require("./FixedArityExpressionT")(HourExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName(){
-	return "$hour";
-};
-
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-/**
- * Takes a date and returns the hour between 0 and 23.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars){
-	var date = this.operands[0].evaluateInternal(vars);
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
 	return date.getUTCHours();
 };
 
+proto.getOpName = function getOpName() {
+	return "$hour";
+};
 
-/** Register Expression */
-Expression.registerExpression("$hour",base.parse);
+Expression.registerExpression("$hour", base.parse);

+ 9 - 24
lib/pipeline/expressions/IfNullExpression.js

@@ -7,39 +7,24 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var IfNullExpression = module.exports = function IfNullExpression() {
-	if (arguments.length !== 0) throw new Error("zero args expected");
+	if (arguments.length !== 0) throw new Error(klass.name + ": expected args: NONE");
 	base.call(this);
-}, klass = IfNullExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = IfNullExpression, base = require("./FixedArityExpressionT")(IfNullExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$ifNull";
-};
-
-// virtuals from ExpressionNary
-
-/**
- * Use the $ifNull operator with the following syntax: { $ifNull: [ <expression>, <replacement-if-null> ] }
- * @method evaluate
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var left = this.operands[0].evaluateInternal(vars);
-	if (left !== undefined && left !== null) return left;
+	if (left !== undefined && left !== null)
+		return left;
 	var right = this.operands[1].evaluateInternal(vars);
 	return right;
 };
 
-/** Register Expression */
 Expression.registerExpression("$ifNull", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$ifNull";
+};

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

+ 11 - 25
lib/pipeline/expressions/MillisecondExpression.js

@@ -2,41 +2,27 @@
 
 /**
  * An $millisecond pipeline expression.
- * @see evaluateInternal
  * @class MillisecondExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var MillisecondExpression = module.exports = function MillisecondExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = MillisecondExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = MillisecondExpression, base = require("./FixedArityExpressionT")(MillisecondExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$millisecond";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCMilliseconds(); //NOTE: no leap milliseconds in JS - http://code.google.com/p/v8/issues/detail?id=1944
 };
 
-/**
- * Takes a date and returns the millisecond between 0 and 999, but can be 1000 to account for leap milliseconds.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCMilliseconds(); //TODO: incorrect for last millisecond of leap year, need to fix...
-	// currently leap milliseconds are unsupported in v8
-	// http://code.google.com/p/v8/issues/detail?id=1944
+proto.getOpName = function getOpName() {
+	return "$millisecond";
 };
 
-/** Register Expression */
 Expression.registerExpression("$millisecond", base.parse);

+ 11 - 23
lib/pipeline/expressions/MinuteExpression.js

@@ -2,39 +2,27 @@
 
 /**
  * An $minute pipeline expression.
- * @see evaluateInternal
  * @class MinuteExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var MinuteExpression = module.exports = function MinuteExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = MinuteExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = MinuteExpression, base = require("./FixedArityExpressionT")(MinuteExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$minute";
-};
-
-/**
- * Takes a date and returns the minute between 0 and 59.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
 	return date.getUTCMinutes();
 };
 
-/** Register Expression */
+proto.getOpName = function getOpName() {
+	return "$minute";
+};
+
 Expression.registerExpression("$minute", base.parse);

+ 25 - 36
lib/pipeline/expressions/ModExpression.js

@@ -4,51 +4,40 @@
  * An $mod pipeline expression.
  * @see evaluate
  * @class ModExpression
+ * @extends mungedb-aggregate.pipeline.expressions.FixedArityExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var ModExpression = module.exports = function ModExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = ModExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
-
-// DEPENDENCIES
+}, klass = ModExpression, base = require("./FixedArityExpressionT")(ModExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
+
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$mod";
-};
-
-/**
- * Takes an array that contains a pair of numbers and returns the remainder of the first number divided by the second number.
- * @method evaluate
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	this.checkArgCount(2);
-	var left = this.operands[0].evaluateInternal(vars),
-		right = this.operands[1].evaluateInternal(vars);
-	if (left instanceof Date || right instanceof Date) throw new Error("$mod does not support dates; code 16374");
-
-	// pass along jstNULLs and Undefineds
-	if (left === undefined || left === null) return left;
-	if (right === undefined || right === null) return right;
-
-	// ensure we aren't modding by 0
-	right = Value.coerceToDouble(right);
-	if (right === 0) return undefined;
-
-	left = Value.coerceToDouble(left);
-	return left % right;
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
+
+	var leftType = Value.getType(lhs),
+		rightType = Value.getType(rhs);
+
+	if (typeof lhs === "number" && typeof rhs === "number") {
+		// ensure we aren't modding by 0
+		if(rhs === 0) throw new Error("can't $mod by 0; uassert code 16610");
+
+		return lhs % rhs;
+	} else if (lhs === undefined || lhs === null || rhs === undefined || rhs === null) {
+		return null;
+	} else {
+		throw new Error("$mod only supports numeric types, not " + Value.getType(lhs) + " and " + Value.getType(rhs));
+	}
 };
 
-/** Register Expression */
 Expression.registerExpression("$mod", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$mod";
+};

+ 11 - 23
lib/pipeline/expressions/MonthExpression.js

@@ -2,39 +2,27 @@
 
 /**
  * A $month pipeline expression.
- * @see evaluate
  * @class MonthExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var MonthExpression = module.exports = function MonthExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = MonthExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = MonthExpression, base = require("./FixedArityExpressionT")(MonthExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$month";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCMonth() + 1;
 };
 
-/**
- * Takes a date and returns the month as a number between 1 and 12.
- * @method evaluate
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCMonth();
+proto.getOpName = function getOpName() {
+	return "$month";
 };
 
-/** Register Expression */
 Expression.registerExpression("$month", base.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;
+};

+ 11 - 24
lib/pipeline/expressions/NotExpression.js

@@ -7,36 +7,23 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var NotExpression = module.exports = function NotExpression() {
-		if (arguments.length !== 0) throw new Error("zero args expected");
+	if (arguments.length !== 0) throw new Error(klass.name + ": expected args: NONE");
 	base.call(this);
-}, klass = NotExpression,
-	base = require("./FixedArityExpressionT")(klass, 1),
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = NotExpression, base = require("./FixedArityExpressionT")(NotExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-klass.opName = "$not";
-proto.getOpName = function getOpName() {
-	return klass.opName;
-};
-
-/**
- * Returns the boolean opposite value passed to it. When passed a true value, $not returns false; when passed a false value, $not returns true.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var op = this.operands[0].evaluateInternal(vars);
-	return !Value.coerceToBool(op);
+	var op = this.operands[0].evaluateInternal(vars),
+		b = Value.coerceToBool(op);
+	return !b;
 };
 
-/** Register Expression */
-Expression.registerExpression(klass.opName, base.parse);
+Expression.registerExpression("$not", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$not";
+};

+ 11 - 25
lib/pipeline/expressions/SecondExpression.js

@@ -2,41 +2,27 @@
 
 /**
  * An $second pipeline expression.
- * @see evaluateInternal
  * @class SecondExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SecondExpression = module.exports = function SecondExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SecondExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SecondExpression, base = require("./FixedArityExpressionT")(SecondExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
-var Expression = require("./Expression");
+var Expression = require("./Expression"),
+	Value = require("../Value");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$second";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCSeconds();  //NOTE: no leap seconds in JS - http://code.google.com/p/v8/issues/detail?id=1944
 };
 
-/**
- * Takes a date and returns the second between 0 and 59, but can be 60 to account for leap seconds.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCSeconds(); //TODO: incorrect for last second of leap year, need to fix...
-	// currently leap seconds are unsupported in v8
-	// http://code.google.com/p/v8/issues/detail?id=1944
+proto.getOpName = function getOpName() {
+	return "$second";
 };
 
-/** Register Expression */
 Expression.registerExpression("$second", base.parse);

+ 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 - 20
lib/pipeline/expressions/SizeExpression.js

@@ -6,36 +6,25 @@
  * @class SizeExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
+ * @extends mungedb-aggregate.pipeline.FixedArityExpressionT
  * @constructor
- **/
+ */
 var SizeExpression = module.exports = function SizeExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": args expected: value");
 	base.call(this);
-}, klass = SizeExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SizeExpression, base = require("./FixedArityExpressionT")(SizeExpression, 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 "$size";
-};
-
-/**
- * Takes an array and return the size.
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
 	var array = this.operands[0].evaluateInternal(vars);
-	if (array instanceof Date) throw new Error("$size does not support dates; code 16376");
+	if (!(array instanceof Array)) throw new Error("The argument to $size must be an Array but was of type" + Value.getType(array) + "; uassert code 16376");
 	return array.length;
 };
 
-/** Register Expression */
 Expression.registerExpression("$size", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$size";
+};

+ 21 - 27
lib/pipeline/expressions/StrcasecmpExpression.js

@@ -7,40 +7,34 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var StrcasecmpExpression = module.exports = function StrcasecmpExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = StrcasecmpExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = StrcasecmpExpression, base = require("./FixedArityExpressionT")(StrcasecmpExpression, 2), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	NaryExpression = require("./NaryExpression"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$strcasecmp";
-};
-
-/**
- * Takes in two strings. Returns a number. $strcasecmp is positive if the first string is “greater than” the second and negative if the first string is “less than” the second. $strcasecmp returns 0 if the strings are identical.
- * @method evaluate
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var val1 = this.operands[0].evaluateInternal(vars),
-		val2 = this.operands[1].evaluateInternal(vars),
-		str1 = Value.coerceToString(val1).toUpperCase(),
-		str2 = Value.coerceToString(val2).toUpperCase(),
-		cmp = Value.compare(str1, str2);
-	return cmp;
+	var string1 = this.operands[0].evaluateInternal(vars),
+		string2 = this.operands[1].evaluateInternal(vars);
+
+	var str1 = Value.coerceToString(string1).toUpperCase(),
+		str2 = Value.coerceToString(string2).toUpperCase(),
+		result = Value.compare(str1, str2);
+
+	if (result === 0) {
+		return 0;
+	} else if (result > 0) {
+		return 1;
+	} else {
+		return -1;
+	}
 };
 
-/** Register Expression */
 Expression.registerExpression("$strcasecmp", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$strcasecmp";
+};

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

@@ -7,43 +7,36 @@
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var SubstrExpression = module.exports = function SubstrExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = SubstrExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 3),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SubstrExpression, base = require("./FixedArityExpressionT")(klass, 3), 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 "$substr";
-};
-
-/**
- * Takes a string and two numbers. The first number represents the number of bytes in the string to skip, and the second number specifies the number of bytes to return from the string.
- * @method evaluateInternal
- **/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var val = this.operands[0].evaluateInternal(vars),
-		idx = this.operands[1].evaluateInternal(vars),
-		len = this.operands[2].evaluateInternal(vars),
-		str = Value.coerceToString(val);
-	if (typeof(idx) != "number") throw new Error(this.getOpName() + ": starting index must be a numeric type; code 16034");
-	if (typeof(len) != "number") throw new Error(this.getOpName() + ": length must be a numeric type; code 16035");
-	if (idx >= str.length) return "";
-	//TODO: Need to handle -1
-	len = (len === -1 ? undefined : len);
-	return str.substr(idx, len);
+	var string = this.operands[0].evaluateInternal(vars),
+		pLower = this.operands[1].evaluateInternal(vars),
+		pLength = this.operands[2].evaluateInternal(vars);
+
+	var str = Value.coerceToString(string);
+	if (typeof pLower !== "number") throw new Error(this.getOpName() + ":  starting index must be a numeric type (is type " + Value.getType(pLower) + "); uassert code 16034");
+	if (typeof pLength !== "number") throw new Error(this.getOpName() + ":  length must be a numeric type (is type " + Value.getType(pLength) + "); uassert code 16035");
+
+	var lower = Value.coerceToLong(pLower),
+		length = Value.coerceToLong(pLength);
+	if (lower >= str.length) {
+		// If lower > str.length() then string::substr() will throw out_of_range, so return an
+		// empty string if lower is not a valid string index.
+		return "";
+	}
+	return str.substr(lower, length);
 };
 
-/** Register Expression */
 Expression.registerExpression("$substr", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$substr";
+};

+ 35 - 24
lib/pipeline/expressions/SubtractExpression.js

@@ -4,39 +4,50 @@
  * A $subtract pipeline expression.
  * @see evaluateInternal
  * @class SubtractExpression
+ * @extends mungedb-aggregate.pipeline.expressions.FixedArityExpressionT
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
-var SubtractExpression = module.exports = function SubtractExpression(){
+ */
+var SubtractExpression = module.exports = function SubtractExpression() {
 	base.call(this);
-}, klass = SubtractExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 2),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = SubtractExpression, base = require("./FixedArityExpressionT")(SubtractExpression, 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 "$subtract";
-};
-
-/**
-* Takes an array that contains a pair of numbers and subtracts the second from the first, returning their difference.
-**/
 proto.evaluateInternal = function evaluateInternal(vars) {
-	var left = this.operands[0].evaluateInternal(vars),
-		right = this.operands[1].evaluateInternal(vars);
-	if (left instanceof Date || right instanceof Date) throw new Error("$subtract does not support dates; code 16376");
-	return left - right;
+	var lhs = this.operands[0].evaluateInternal(vars),
+		rhs = this.operands[1].evaluateInternal(vars);
+
+	if (typeof lhs === "number" && typeof rhs === "number") {
+		return lhs - rhs;
+	} else if (lhs === null || lhs === undefined || rhs === null || rhs === undefined) {
+		return null;
+	} else if (lhs instanceof Date) {
+		if (rhs instanceof Date) {
+			var timeDelta = lhs - rhs;
+			return timeDelta;
+		} else if (typeof rhs === "number") {
+			var millisSinceEpoch = lhs - Value.coerceToLong(rhs);
+			return millisSinceEpoch;
+		} else {
+			throw new Error("can't $subtract a " +
+				Value.getType(rhs) +
+				" from a Date" +
+				"; uassert code 16613");
+		}
+	} else {
+		throw new Error("can't $subtract a " +
+			Value.getType(rhs) +
+			" from a " +
+			Value.getType(lhs) +
+			"; uassert code 16556");
+	}
 };
 
-/** Register Expression */
 Expression.registerExpression("$subtract", base.parse);
+
+proto.getOpName = function getOpName() {
+	return "$subtract";
+};

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

@@ -2,36 +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";
+};

+ 88 - 0
lib/pipeline/expressions/ValueSet.js

@@ -0,0 +1,88 @@
+"use strict";
+
+/**
+ * Somewhat mimics the ValueSet in mongo (std::set<Value>)
+ * @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;
+};

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

+ 13 - 30
lib/pipeline/expressions/WeekExpression.js

@@ -2,49 +2,32 @@
 
 /**
  * A $week pipeline expression.
- * @see evaluateInternal
  * @class WeekExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var WeekExpression = module.exports = function WeekExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = WeekExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = WeekExpression, base = require("./FixedArityExpressionT")(WeekExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
 	DayOfYearExpression = require("./DayOfYearExpression"),
 	Expression = require("./Expression");
 
-// PROTOTYPE MEMBERS
-proto.getOpName = function getOpName() {
-	return "$week";
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate),
+		//NOTE: DEVIATION FROM MONGO: our calculations are a little different but are equivalent
+		y11 = new Date(date.getUTCFullYear(), 0, 1), // same year, first month, first day; time omitted
+		ymd = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + 1), // same y,m,d; time omitted, add 1 because days start at 1
+		yday = Math.ceil((ymd - y11) / 86400000); // count days
+	return (yday / 7) | 0;
 };
 
-/**
- * Takes a date and returns the week of the year as a number between 0 and 53.
- * Weeks begin on Sundays, and week 1 begins with the first Sunday of the year.
- * Days preceding the first Sunday of the year are in week 0.
- * This behavior is the same as the “%U” operator to the strftime standard library function.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars),
-		dayOfWeek = date.getUTCDay(),
-		dayOfYear = DayOfYearExpression.getDateDayOfYear(date),
-		prevSundayDayOfYear = dayOfYear - dayOfWeek, // may be negative
-		nextSundayDayOfYear = prevSundayDayOfYear + 7; // must be positive
-	// Return the zero based index of the week of the next sunday, equal to the one based index of the week of the previous sunday, which is to be returned.
-	return (nextSundayDayOfYear / 7) | 0; // also, the `| 0` here truncates this so that we return an integer
+proto.getOpName = function getOpName() {
+	return "$week";
 };
 
-/** Register Expression */
 Expression.registerExpression("$week", base.parse);

+ 8 - 23
lib/pipeline/expressions/YearExpression.js

@@ -2,42 +2,27 @@
 
 /**
  * A $year pipeline expression.
- * @see evaluateInternal
  * @class YearExpression
  * @namespace mungedb-aggregate.pipeline.expressions
  * @module mungedb-aggregate
  * @constructor
- **/
+ */
 var YearExpression = module.exports = function YearExpression() {
+	if (arguments.length !== 0) throw new Error(klass.name + ": no args expected");
 	base.call(this);
-}, klass = YearExpression,
-	FixedArityExpression = require("./FixedArityExpressionT")(klass, 1),
-	base = FixedArityExpression,
-	proto = klass.prototype = Object.create(base.prototype, {
-		constructor: {
-			value: klass
-		}
-	});
+}, klass = YearExpression, base = require("./FixedArityExpressionT")(YearExpression, 1), proto = klass.prototype = Object.create(base.prototype, {constructor: {value: klass}});
 
-// DEPENDENCIES
 var Value = require("../Value"),
-	DayOfYearExpression = require("./DayOfYearExpression"),
 	Expression = require("./Expression");
 
+proto.evaluateInternal = function evaluateInternal(vars) {
+	var pDate = this.operands[0].evaluateInternal(vars),
+		date = Value.coerceToDate(pDate);
+	return date.getUTCFullYear();
+};
 
-// PROTOTYPE MEMBERS
 proto.getOpName = function getOpName() {
 	return "$year";
 };
 
-/**
- * Takes a date and returns the full year.
- * @method evaluateInternal
- **/
-proto.evaluateInternal = function evaluateInternal(vars) {
-	var date = this.operands[0].evaluateInternal(vars);
-	return date.getUTCFullYear();
-};
-
-/** Register Expression */
 Expression.registerExpression("$year", base.parse);

+ 148 - 49
test/lib/pipeline/expressions/AllElementsTrueExpression.js

@@ -1,71 +1,170 @@
 "use strict";
 var assert = require("assert"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
 	AllElementsTrueExpression = require("../../../../lib/pipeline/expressions/AllElementsTrueExpression"),
 	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
-var allElementsTrueExpression = new AllElementsTrueExpression();
+// 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;
+})();
+
+exports.AllElementsTrueExpression = {
 
-module.exports = {
+	"constructor()": {
 
-	"AllElementsTrueExpression": {
+		"should construct instance": function() {
+			assert(new AllElementsTrueExpression() instanceof AllElementsTrueExpression);
+			assert(new AllElementsTrueExpression() instanceof Expression);
+		},
 
-		"constructor()": {
+		"should error if given args": function() {
+			assert.throws(function() {
+				new AllElementsTrueExpression("bad stuff");
+			});
+		},
 
-			"should not throw Error when constructing without args": function testConstructor() {
-				assert.doesNotThrow(function() {
-					new AllElementsTrueExpression();
-				});
-			}
+	},
 
+	"#getOpName()": {
+
+		"should return the correct op name; $allElementsTrue": function() {
+			assert.equal(new AllElementsTrueExpression().getOpName(), "$allElementsTrue");
 		},
 
-		"#getOpName()": {
+	},
 
-			"should return the correct op name; $allElements": function testOpName() {
-				assert.equal(new AllElementsTrueExpression().getOpName(), "$allElementsTrue");
-			}
+	"#evaluate()": {
 
+		"should return false if just false": function JustFalse() {
+            new ExpectedResultBase({
+				getSpec: {
+					input: [[false]],
+					expected: {
+						$allElementsTrue: false,
+						// $anyElementTrue: false,
+					}
+				}
+			}).run();
 		},
 
-		"#evaluateInternal()": {
+		"should return true if just true": function JustTrue() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[true]],
+					expected: {
+						$allElementsTrue: true,
+						// $anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"should return error if parameter is empty:": function testEmpty() {
-				assert.throws(function() {
-					allElementsTrueExpression.evaluateInternal("asdf");
-				});
-			},
+		"should return false if one true and one false": function OneTrueOneFalse() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[true, false]],
+					expected: {
+						$allElementsTrue: false,
+						// $anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-			"should return error if parameter is not an array": function testNonArray() {
-				assert.throws(function() {
-					allElementsTrueExpression.evaluateInternal("This is not an array");
-				});
-			},
-
-			"should return false if first element is false; [false, true, true true]": function testFirstFalse() {
-				assert.equal(allElementsTrueExpression.evaluateInternal(
-					Expression.parseOperand({
-						$allElementsTrue: [false, true, true, true]
-					}).evaluate()), false);
-			},
-
-			"should return false if last element is false; [true, true, true, false]": function testLastFalse() {
-				assert.equal(allElementsTrueExpression.evaluateInternal(
-					Expression.parseOperand({
-						$allElementsTrue: [true, true, true, false]
-					}).evaluate()), false);
-			},
-
-			"should return true if all elements are true; [true,true,true,true]": function testAllTrue() {
-				assert.equal(allElementsTrueExpression.evaluateInternal(
-					Expression.parseOperand({
-						$allElementsTrue: [true, true, true, true]
-					}).evaluate()), true);
-			},
+		"should return true if empty": function Empty() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[]],
+					expected: {
+						$allElementsTrue: true,
+						// $anyElementTrue: false,
+					}
+				}
+			}).run();
+		},
 
-		}
+		"should return true if truthy int": function TrueViaInt() {
+			new ExpectedResultBase({
+				getSpec: {
+					input: [[1]],
+					expected: {
+						$allElementsTrue: true,
+						// $anyElementTrue: true,
+					}
+				}
+			}).run();
+		},
 
-	}
+		"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);
+	},
+
+};

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

+ 409 - 308
test/lib/pipeline/expressions/CompareExpression.js

@@ -1,367 +1,468 @@
 "use strict";
 var assert = require("assert"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression"),
+	pipeline = require("../../../../lib/pipeline"),
+	expressions = pipeline.expressions,
+	Expression = expressions.Expression,
 	CompareExpression = require("../../../../lib/pipeline/expressions/CompareExpression"),
-	FieldRangeExpression = require("../../../../lib/pipeline/expressions/FieldRangeExpression"),
 	VariablesParseState = require("../../../../Lib/pipeline/expressions/VariablesParseState"),
 	VariablesIdGenerator = require("../../../../Lib/pipeline/expressions/VariablesIdGenerator"),
-	ConstantExpression = require("../../../../Lib/pipeline/expressions/ConstantExpression");
-
-module.exports = {
-
-	"CompareExpression": {
-
-		"constructor()": {
-
-			"should throw Error if no args": function testConstructor() {
-				assert.throws(function() {
-					new CompareExpression();
-				});
-			}
-
+	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];
+	},
+	OptimizeBase = (function() {
+		var klass = function OptimizeBase() {
+				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),
+				expression = Expression.parseOperand(specElement, vps),
+				optimized = expression.optimize();
+			assert.deepEqual(constify(this.expectedOptimized()), expressionToJson(optimized));
+		};
+		return klass;
+	})(),
+	FieldRangeOptimize = (function() {
+		var klass = function FieldRangeOptimize() {
+				base.apply(this, arguments);
+			},
+			base = OptimizeBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedOptimized = function(){
+			return this.spec;
+		};
+		return klass;
+	})(),
+	NoOptimize = (function() {
+		var klass = function NoOptimize() {
+				base.apply(this, arguments);
+			},
+			base = OptimizeBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedOptimized = function(){
+			return this.spec;
+		};
+		return klass;
+	})(),
+	ExpectedResultBase = (function() {
+		/** Check expected result for expressions depending on constants. */
+		var klass = function ExpectedResultBase() {
+				base.apply(this, arguments);
+			},
+			base = OptimizeBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.run = function() {
+			base.prototype.run.call(this);
+			var specElement = this.spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expression = Expression.parseOperand(specElement, vps);
+			// Check expression spec round trip.
+			assert.deepEqual(expressionToJson(expression), constify(specElement));
+			// Check evaluation result.
+			assert.strictEqual(expression.evaluate({}), this.expectedResult);
+			// Check that the result is the same after optimizing.
+			var optimized = expression.optimize();
+			assert.strictEqual(optimized.evaluate({}), this.expectedResult);
+		};
+		proto.expectedOptimized = function() {
+			return {$const:this.expectedResult};
+		};
+		return klass;
+	})(),
+	ExpectedTrue = (function(){
+		var klass = function ExpectedTrue() {
+				base.apply(this, arguments);
+			},
+			base = ExpectedResultBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedResult = true;
+		return klass;
+	})(),
+	ExpectedFalse = (function(){
+		var klass = function ExpectedFalse() {
+				base.apply(this, arguments);
+			},
+			base = ExpectedResultBase,
+			proto = klass.prototype = Object.create(base.prototype);
+		proto.expectedResult = false;
+		return klass;
+	})(),
+	ParseError = (function(){
+		var klass = function ParseError() {
+				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);
+			assert.throws(function() {
+				Expression.parseOperand(specElement, vps);
+			});
+		};
+		return klass;
+	})();
+
+exports.CompareExpression = {
+
+	"constructor()": {
+
+		"should throw Error if no args": function() {
+			assert.throws(function() {
+				new CompareExpression();
+			});
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $eq, $ne, $gt, $gte, $lt, $lte, $cmp": function testOpName() {
-				assert.equal((new CompareExpression(CompareExpression.EQ)).getOpName(), "$eq");
-				assert.equal((new CompareExpression(CompareExpression.NE)).getOpName(), "$ne");
-				assert.equal((new CompareExpression(CompareExpression.GT)).getOpName(), "$gt");
-				assert.equal((new CompareExpression(CompareExpression.GTE)).getOpName(), "$gte");
-				assert.equal((new CompareExpression(CompareExpression.LT)).getOpName(), "$lt");
-				assert.equal((new CompareExpression(CompareExpression.LTE)).getOpName(), "$lte");
-				assert.equal((new CompareExpression(CompareExpression.CMP)).getOpName(), "$cmp");
-			}
-
+		"should throw if more than 1 args": function() {
+			assert.throws(function() {
+				new CompareExpression(1,2);
+			});
 		},
 
-		"#evaluateInternal()": {
-
-			"$eq": {
-
-				"should return false if first < second; {$eq:[1,2]}": function testEqLt() {
-					//debugger;
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					var parseOp = Expression.parseOperand({
-						$eq: [{
-							$const: 1
-						}, {
-							$const: 2
-						}]
-					}, vps);
-					var result = parseOp.evaluateInternal({});
-
-					//assert.equal(new CompareExpression( CompareExpression.EQ).evaluateInternal({"$eq":[1,2]}), false);
-					assert.equal(result, false);
-
-				},
-
-				"should return true if first == second; {$eq:[1,1]}": function testEqEq() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-
-					assert.equal(Expression.parseOperand({
-						$eq: [1, 1]
-					}, vps).evaluateInternal({}), true);
-				},
-
-				"should return false if first > second {$eq:[1,0]}": function testEqGt() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					assert.equal(Expression.parseOperand({
-						$eq: [1, 0]
-					}).evaluateInternal({}), false);
-				},
-
-				"should return false if first and second are different types {$eq:[null,0]}": function testEqGt() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					assert.equal(Expression.parseOperand({
-						$eq: [null, 0]
-					}, vps).evaluateInternal({}), false);
-				},
-
-				"should return false if first and second are different types {$eq:[undefined,0]}": function testEqGt() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					assert.equal(Expression.parseOperand({
-						$eq: [undefined, 0]
-					}, vps).evaluateInternal({}), false);
-				},
-
-				"should return false if first and second are different arrays {$eq:[[1],[null]]}": function testEqGt() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					assert.equal(Expression.parseOperand({
-						$eq: [
-							[1],
-							[null]
-						]
-					}, vps).evaluateInternal({}), false);
-				},
-
-				"should return false if first and second are different arrays {$eq:[[1],[]]}": function testEqGt() {
-					assert.equal(Expression.parseOperand({
-						$eq: [
-							[1],
-							[]
-						]
-					}, vps).evaluateInternal({}), false);
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-				},
-
-				"should return true if first and second are the same arrays {$eq:[[1],[1]]}": function testEqGt() {
-					var idGenerator = new VariablesIdGenerator();
-					var vps = new VariablesParseState(idGenerator);
-					assert.equal(Expression.parseOperand({
-						$eq: [
-							[1],
-							[1]
-						]
-					}, vps).evaluateInternal({}), true);
-				}
-			},
-
-			//      "$ne": {
-
-			//              "should return true if first < second; {$ne:[1,2]}": function testNeLt(){
-			//	      assert.equal(Expression.parseOperand({$ne:[1,2]}).evaluateInternal({}), true);
-			//              },
-
-			//              "should return false if first == second; {$ne:[1,1]}": function testNeLt(){
-			//	      assert.equal(Expression.parseOperand({$ne:[1,1]}).evaluateInternal({}), false);
-			//              },
-
-			//              "should return true if first > second; {$ne:[1,0]}": function testNeGt(){
-			//	      assert.equal(Expression.parseOperand({$ne:[1,0]}).evaluateInternal({}), true);
-			//              }
-
-			//      },
-
-			//      "$gt": {
-
-			//              "should return false if first < second; {$gt:[1,2]}": function testGtLt(){
-			//	      assert.equal(Expression.parseOperand({$gt:[1,2]}).evaluateInternal({}), false);
-			//              },
-
-			//              "should return false if first == second; {$gt:[1,1]}": function testGtLt(){
-			//	      assert.equal(Expression.parseOperand({$gt:[1,1]}).evaluateInternal({}), false);
-			//              },
-
-			//              "should return true if first > second; {$gt:[1,0]}": function testGtGt(){
-			//	      assert.equal(Expression.parseOperand({$gt:[1,0]}).evaluateInternal({}), true);
-			//              }
-
-			//      },
-
-			//      "$gte": {
-
-			//              "should return false if first < second; {$gte:[1,2]}": function testGteLt(){
-			//	      assert.equal(Expression.parseOperand({$gte:[1,2]}).evaluateInternal({}), false);
-			//              },
-
-			//              "should return true if first == second; {$gte:[1,1]}": function testGteLt(){
-			//	      assert.equal(Expression.parseOperand({$gte:[1,1]}).evaluateInternal({}), true);
-			//              },
-
-			//              "should return true if first > second; {$gte:[1,0]}": function testGteGt(){
-			//	      assert.equal(Expression.parseOperand({$gte:[1,0]}).evaluateInternal({}), true);
-			//              }
-
-			//      },
-
-			//      "$lt": {
-
-			//              "should return true if first < second; {$lt:[1,2]}": function testLtLt(){
-			//	      assert.equal(Expression.parseOperand({$lt:[1,2]}).evaluateInternal({}), true);
-			//              },
-
-			//              "should return false if first == second; {$lt:[1,1]}": function testLtLt(){
-			//	      assert.equal(Expression.parseOperand({$lt:[1,1]}).evaluateInternal({}), false);
-			//              },
-
-			//              "should return false if first > second; {$lt:[1,0]}": function testLtGt(){
-			//	      assert.equal(Expression.parseOperand({$lt:[1,0]}).evaluateInternal({}), false);
-			//              }
-
-			//      },
+		"should not throw if 1 arg and arg is string": function() {
+			assert.doesNotThrow(function() {
+				new CompareExpression("a");
+			});
+		},
 
-			//      "$lte": {
+	},
 
-			//              "should return true if first < second; {$lte:[1,2]}": function testLteLt(){
-			//	      assert.equal(Expression.parseOperand({$lte:[1,2]}).evaluateInternal({}), true);
-			//              },
+	"#getOpName()": {
 
-			//              "should return true if first == second; {$lte:[1,1]}": function testLteLt(){
-			//	      assert.equal(Expression.parseOperand({$lte:[1,1]}).evaluateInternal({}), true);
-			//              },
+		"should return the correct op name; $eq, $ne, $gt, $gte, $lt, $lte, $cmp": function() {
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.EQ).getOpName(), "$eq");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.NE).getOpName(), "$ne");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.GT).getOpName(), "$gt");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.GTE).getOpName(), "$gte");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.LT).getOpName(), "$lt");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.LTE).getOpName(), "$lte");
+			assert.equal(new CompareExpression(CompareExpression.CmpOp.CMP).getOpName(), "$cmp");
+		},
 
-			//              "should return false if first > second; {$lte:[1,0]}": function testLteGt(){
-			//	      assert.equal(Expression.parseOperand({$lte:[1,0]}).evaluateInternal({}), false);
-			//              }
+	},
 
-			//      },
+	"#evaluate()": {
 
-			//      "$cmp": {
+		/** $eq with first < second. */
+		"EqLt": function EqLt() {
+			new ExpectedFalse({
+				spec: {$eq:[1,2]},
+			}).run();
+		},
 
-			//              "should return -1 if first < second; {$cmp:[1,2]}": function testCmpLt(){
-			//	      assert.equal(Expression.parseOperand({$cmp:[1,2]}).evaluateInternal({}), -1);
-			//              },
+		/** $eq with first == second. */
+		"EqEq": function EqEq() {
+			new ExpectedTrue({
+				spec: {$eq:[1,1]},
+			}).run();
+		},
 
-			//              "should return 0 if first < second; {$cmp:[1,1]}": function testCmpLt(){
-			//	      assert.equal(Expression.parseOperand({$cmp:[1,1]}).evaluateInternal({}), 0);
-			//              },
+		/** $eq with first > second. */
+		"EqGt": function EqEq() {
+			new ExpectedFalse({
+				spec: {$eq:[1,0]},
+			}).run();
+		},
 
-			//              "should return 1 if first < second; {$cmp:[1,0]}": function testCmpLt(){
-			//	      assert.equal(Expression.parseOperand({$cmp:[1,0]}).evaluateInternal({}), 1);
-			//              },
+		/** $ne with first < second. */
+		"NeLt": function NeLt() {
+			new ExpectedTrue({
+				spec: {$ne:[1,2]},
+			}).run();
+		},
 
-			//              "should return 1 even if comparison is larger; {$cmp:['z','a']}": function testCmpBracketed(){
-			//	      assert.equal(Expression.parseOperand({$cmp:['z','a']}).evaluateInternal({}), 1);
-			//              }
+		/** $ne with first == second. */
+		"NeEq": function NeEq() {
+			new ExpectedFalse({
+				spec: {$ne:[1,1]},
+			}).run();
+		},
 
-			//      },
+		/** $ne with first > second. */
+		"NeGt": function NeGt() {
+			new ExpectedTrue({
+				spec: {$ne:[1,0]},
+			}).run();
+		},
 
-			//      "should throw Error": {
+		/** $gt with first < second. */
+		"GtLt": function GtLt() {
+			new ExpectedFalse({
+				spec: {$gt:[1,2]},
+			}).run();
+		},
 
-			//              "if zero operands are provided; {$ne:[]}": function testZeroOperands(){
-			//	      assert.throws(function(){
-			//	              Expression.parseOperand({$ne:[]}).evaluateInternal({});
-			//	      });
-			//              },
+		/** $gt with first == second. */
+		"GtEq": function GtEq() {
+			new ExpectedFalse({
+				spec: {$gt:[1,1]},
+			}).run();
+		},
 
-			//              "if one operand is provided; {$eq:[1]}": function testOneOperand(){
-			//	      assert.throws(function(){
-			//	              Expression.parseOperand({$eq:[1]}).evaluateInternal({});
-			//	      });
-			//              },
+		/** $gt with first > second. */
+		"GtGt": function GtGt() {
+			new ExpectedTrue({
+				spec: {$gt:[1,0]},
+			}).run();
+		},
 
-			//              "if three operands are provided; {$gt:[2,3,4]}": function testThreeOperands(){
-			//	      assert.throws(function(){
-			//	              Expression.parseOperand({$gt:[2,3,4]}).evaluateInternal({});
-			//	      });
-			//              }
-			//      }
+		/** $gte with first < second. */
+		"GteLt": function GteLt() {
+			new ExpectedFalse({
+				spec: {$gte:[1,2]},
+			}).run();
+		},
 
-			// },
+		/** $gte with first == second. */
+		"GteEq": function GteEq() {
+			new ExpectedTrue({
+				spec: {$gte:[1,1]},
+			}).run();
+		},
 
-			// "#optimize()": {
+		/** $gte with first > second. */
+		"GteGt": function GteGt() {
+			new ExpectedTrue({
+				spec: {$gte:[1,0]},
+			}).run();
+		},
 
-			//      "should optimize constants; {$eq:[1,1]}": function testOptimizeConstants(){
-			//              assert.deepEqual(Expression.parseOperand({$eq:[1,1]}).optimize().toJSON(true), {$const:true});
-			//      },
+		/** $gte with first > second. */
+		"LtLt": function LtLt() {
+			new ExpectedTrue({
+				spec: {$lt:[1,2]},
+			}).run();
+		},
 
-			//      "should not optimize if $cmp op; {$cmp:[1,'$a']}": function testNoOptimizeCmp(){
-			//              assert.deepEqual(Expression.parseOperand({$cmp:[1,'$a']}).optimize().toJSON(), {$cmp:[1,'$a']});
-			//      },
+		/** $lt with first == second. */
+		"LtEq": function LtEq() {
+			new ExpectedFalse({
+				spec: {$lt:[1,1]},
+			}).run();
+		},
 
-			//      "should not optimize if $ne op; {$ne:[1,'$a']}": function testNoOptimizeNe(){
-			//              assert.deepEqual(Expression.parseOperand({$ne:[1,'$a']}).optimize().toJSON(), {$ne:[1,'$a']});
-			//      },
+		/** $lt with first > second. */
+		"LtGt": function LtGt() {
+			new ExpectedFalse({
+				spec: {$lt:[1,0]},
+			}).run();
+		},
 
-			//      "should not optimize if no constants; {$ne:['$a','$b']}": function testNoOptimizeNoConstant(){
-			//              assert.deepEqual(Expression.parseOperand({$ne:['$a','$b']}).optimize().toJSON(), {$ne:['$a','$b']});
-			//      },
+		/** $lte with first < second. */
+		"LteLt": function LteLt() {
+			new ExpectedTrue({
+				spec: {$lte:[1,2]},
+			}).run();
+		},
 
-			//      "should not optimize without an immediate field path;": {
+		/** $lte with first == second. */
+		"LteEq": function LteEq() {
+			new ExpectedTrue({
+				spec: {$lte:[1,1]},
+			}).run();
+		},
 
-			//              "{$eq:[{$and:['$a']},1]}": function testNoOptimizeWithoutFieldPath(){
-			//	      assert.deepEqual(Expression.parseOperand({$eq:[{$and:['$a']},1]}).optimize().toJSON(), {$eq:[{$and:['$a']},1]});
-			//              },
+		/** $lte with first > second. */
+		"LteGt": function LteGt() {
+			new ExpectedFalse({
+				spec: {$lte:[1,0]},
+			}).run();
+		},
 
-			//              "(reversed); {$eq:[1,{$and:['$a']}]}": function testNoOptimizeWithoutFieldPathReverse(){
-			//	      assert.deepEqual(Expression.parseOperand({$eq:[1,{$and:['$a']}]}).optimize().toJSON(), {$eq:[1,{$and:['$a']}]});
-			//              }
+		/** $cmp with first < second. */
+		"CmpLt": function CmpLt() {
+			new ExpectedResultBase({
+				spec: {$cmp:[1,2]},
+				expectedResult: -1,
+			}).run();
+		},
 
-			//      },
+		/** $cmp with first == second. */
+		"CmpEq": function CmpEq() {
+			new ExpectedResultBase({
+				spec: {$cmp:[1,1]},
+				expectedResult: 0,
+			}).run();
+		},
 
-			//      "should optimize $eq expressions;": {
+		/** $cmp with first > second. */
+		"CmpGt": function CmpGt() {
+			new ExpectedResultBase({
+				spec: {$cmp:[1,0]},
+				expectedResult: 1,
+			}).run();
+		},
 
-			//              "{$eq:['$a',1]}": function testOptimizeEq(){
-			//	      var expr = Expression.parseOperand({$eq:['$a',1]}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$eq:['$a',1]});
-			//              },
+		/** $cmp results are bracketed to an absolute value of 1. */
+		"CmpBracketed": function CmpBracketed() {
+			var test = new ExpectedResultBase({
+				spec: {$cmp:["z","a"]},
+				expectedResult: 1,
+			}).run();
+		},
 
-			//              "{$eq:[1,'$a']} (reversed)": function testOptimizeEqReverse(){
-			//	      var expr = Expression.parseOperand({$eq:[1,'$a']}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$eq:['$a',1]});
-			//              }
+		/** Zero operands provided. */
+		"ZeroOperands": function ZeroOperands() {
+			new ParseError({
+				spec: {$ne:[]},
+			}).run();
+		},
 
-			//      },
+		/** One operands provided. */
+		"OneOperand": function OneOperand() {
+			new ParseError({
+				spec: {$eq:[1]},
+			}).run();
+		},
 
-			//      "should optimize $lt expressions;": {
+        /** Incompatible types can be compared. */
+		"IncompatibleTypes": function IncompatibleTypes() {
+			var specElement = {$ne:["a",1]},
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(expr.evaluate({}), true);
+		},
 
-			//              "{$lt:['$a',1]}": function testOptimizeLt(){
-			//	      var expr = Expression.parseOperand({$lt:['$a',1]}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$lt:['$a',1]});
-			//              },
+		/** Three operands provided. */
+		"ThreeOperands": function ThreeOperands() {
+			new ParseError({
+				spec: {$gt:[2,3,4]},
+			}).run();
+		},
 
-			//              "{$lt:[1,'$a']} (reversed)": function testOptimizeLtReverse(){
-			//	      var expr = Expression.parseOperand({$lt:[1,'$a']}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$gt:['$a',1]});
-			//              }
+		/**
+		 * An expression depending on constants is optimized to a constant via
+		 * ExpressionNary::optimize().
+		 */
+		"OptimizeConstants": function OptimizeConstants() {
+			new OptimizeBase({
+				spec: {$eq:[1,1]},
+				expectedOptimized: function() {
+					return {$const: true};
+				},
+			}).run();
+		},
 
-			//      },
+		/** $cmp is not optimized. */
+		"NoOptimizeCmp": function NoOptimizeCmp() {
+			new NoOptimize({
+				spec: {$cmp:[1,"$a"]},
+			}).run();
+		},
 
-			//      "should optimize $lte expressions;": {
+		/** $ne is not optimized. */
+		"NoOptimizeNe": function NoOptimizeNe() {
+			new NoOptimize({
+				spec: {$ne:[1,"$a"]},
+			}).run();
+		},
 
-			//              "{$lte:['$b',2]}": function testOptimizeLte(){
-			//	      var expr = Expression.parseOperand({$lte:['$b',2]}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$lte:['$b',2]});
-			//              },
+		/** No optimization is performend without a constant. */
+		"NoOptimizeNoConstant": function NoOptimizeNoConstant() {
+			new NoOptimize({
+				spec: {$ne:["$a", "$b"]},
+			}).run();
+		},
 
-			//              "{$lte:[2,'$b']} (reversed)": function testOptimizeLteReverse(){
-			//	      var expr = Expression.parseOperand({$lte:[2,'$b']}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$gte:['$b',2]});
-			//              }
+		/** No optimization is performend without an immediate field path. */
+		"NoOptimizeWithoutFieldPath": function NoOptimizeWithoutFieldPath() {
+			new NoOptimize({
+				spec: {$eq:[{$and:["$a"]},1]},
+			}).run();
+		},
 
-			//      },
+		/** No optimization is performend without an immediate field path. */
+		"NoOptimizeWithoutFieldPathReverse": function NoOptimizeWithoutFieldPathReverse() {
+			new NoOptimize({
+				spec: {$eq:[1,{$and:["$a"]}]},
+			}).run();
+		},
 
-			//      "should optimize $gt expressions;": {
+		/** An equality expression is optimized. */
+		"OptimizeEq": function OptimizeEq() {
+			new FieldRangeOptimize({
+				spec: {$eq:["$a",1]},
+			}).run();
+		},
 
-			//              "{$gt:['$b',2]}": function testOptimizeGt(){
-			//	      var expr = Expression.parseOperand({$gt:['$b',2]}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$gt:['$b',2]});
-			//              },
+		/** A reverse sense equality expression is optimized. */
+		"OptimizeEqReverse": function OptimizeEqReverse() {
+			new FieldRangeOptimize({
+				spec: {$eq:[1,"$a"]},
+			}).run();
+		},
 
-			//              "{$gt:[2,'$b']} (reversed)": function testOptimizeGtReverse(){
-			//	      var expr = Expression.parseOperand({$gt:[2,'$b']}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$lt:['$b',2]});
-			//              }
+		/** A $lt expression is optimized. */
+		"OptimizeLt": function OptimizeLt() {
+			new FieldRangeOptimize({
+				spec: {$lt:["$a",1]},
+			}).run();
+		},
 
-			//      },
+		/** A reverse sense $lt expression is optimized. */
+		"OptimizeLtReverse": function OptimizeLtReverse() {
+			new FieldRangeOptimize({
+				spec: {$lt:[1,"$a"]},
+			}).run();
+		},
 
-			//      "should optimize $gte expressions;": {
+		/** A $lte expression is optimized. */
+		"OptimizeLte": function OptimizeLte() {
+			new FieldRangeOptimize({
+				spec: {$lte:["$b",2]},
+			}).run();
+		},
 
-			//              "{$gte:['$b',2]}": function testOptimizeGte(){
-			//	      var expr = Expression.parseOperand({$gte:['$b',2]}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$gte:['$b',2]});
-			//              },
+		/** A reverse sense $lte expression is optimized. */
+		"OptimizeLteReverse": function OptimizeLteReverse() {
+			new FieldRangeOptimize({
+				spec: {$lte:[2,"$b"]},
+			}).run();
+		},
 
-			//              "{$gte:[2,'$b']} (reversed)": function testOptimizeGteReverse(){
-			//	      var expr = Expression.parseOperand({$gte:[2,'$b']}).optimize();
-			//	      assert(expr instanceof FieldRangeExpression, "not optimized");
-			//	      assert.deepEqual(expr.toJSON(), {$lte:['$b',2]});
-			//              }
+		/** A $gt expression is optimized. */
+		"OptimizeGt": function OptimizeGt() {
+			new FieldRangeOptimize({
+				spec: {$gt:["$b",2]},
+			}).run();
+		},
 
-			//      },
+		/** A reverse sense $gt expression is optimized. */
+		"OptimizeGtReverse": function OptimizeGtReverse() {
+			new FieldRangeOptimize({
+				spec: {$gt:["$b",2]},
+			}).run();
+		},
 
+		/** A $gte expression is optimized. */
+		"OptimizeGte": function OptimizeGte() {
+			new FieldRangeOptimize({
+				spec: {$gte:["$b",2]},
+			}).run();
+		},
 
-		}
+		/** A reverse sense $gte expression is optimized. */
+		"OptimizeGteReverse": function OptimizeGteReverse() {
+			new FieldRangeOptimize({
+				spec: {$gte:[2,"$b"]},
+			}).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);

+ 0 - 72
test/lib/pipeline/expressions/CondExpression.js

@@ -1,72 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	CondExpression = require("../../../../lib/pipeline/expressions/CondExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-	"CondExpression": {
-
-		"constructor()": {
-
-			"should throw Error when constructing without args": function testConstructor(){
-				assert.throws(function(){
-					new CondExpression();
-				});
-			},
-
-			"should throw Error when constructing with 1 arg": function testConstructor1(){
-				assert.throws(function(){
-					new CondExpression({if:true === true});
-				});
-			},
-			"should throw Error when constructing with 2 args": function testConstructor2(){
-				assert.throws(function(){
-					new CondExpression(true === true,1);
-				});
-			},
-			"should now throw Error when constructing with 3 args": function testConstructor3(){
-				assert.doesNotThrow(function(){
-					//new CondExpression({$cond:[{"if":"true === true"},{"then":"1"},{"else":"0"}]});
-					new CondExpression({$cond:[ true === true, 1, 0 ]});
-				});
-			},
-		},
-
-		"#getOpName()": {
-
-			"should return the correct op name; $cond": function testOpName(){
-				assert.equal(new CondExpression().getOpName(), "$cond");
-			}
-
-		},
-
-		"#evaluateInternal()": {
-
-			"should evaluate boolean expression as true, then return 1; [ true === true, 1, 0 ]": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$cond:[ true === true, 1, 0 ]}).evaluateInternal({}), 1);
-			},
-
-			"should evaluate boolean expression as false, then return 0; [ false === true, 1, 0 ]": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$cond:[ false === true, 1, 0 ]}).evaluateInternal({}), 0);
-			}, 
-
-			"should evaluate boolean expression as true, then return 1; [ (true === true) && true, 1, 0 ]": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$cond:[ (true === true) && true , 1, 0 ]}).evaluateInternal({}), 1);
-			},
-
-			"should evaluate boolean expression as false, then return 0; [ (false === true) && true, 1, 0 ]": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$cond:[ (false === true) && true, 1, 0 ]}).evaluateInternal({}), 0);
-			},
-
-			"should evaluate complex boolean expression as true, then return 1; [ ( 1 > 0 ) && (( 'a' == 'b' ) || ( 3 <= 5 )), 1, 0 ]": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$cond:[ ( 1 > 0 ) && (( 'a' == 'b' ) || ( 3 <= 5 )), 1, 0 ]}).evaluate({}), 1);
-			},
-		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 93 - 104
test/lib/pipeline/expressions/CondExpression_test.js

@@ -1,128 +1,117 @@
 "use strict";
 var assert = require("assert"),
 	CondExpression = require("../../../../lib/pipeline/expressions/CondExpression"),
+	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.CondExpression = {
 
-	"CondExpression": {
+	"constructor()": {
 
-		"constructor()": {
+		"should not throw an Error when constructing without args": function testConstructor(){
+			assert.doesNotThrow(function(){
+				new CondExpression();
+			});
+		},
+
+		"should throw Error when constructing with 1 arg": function testConstructor1(){
+			assert.throws(function(){
+				new CondExpression(1);
+			});
+		},
+
+	},
+
+	"#getOpName()": {
 
-			"should not throw an Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new CondExpression();
+		"should return the correct op name; $cond": function testOpName(){
+			assert.equal(new CondExpression().getOpName(), "$cond");
+		},
+
+	},
+
+	"#evaluate()": {
+		"array style": {
+
+			"should fail if there aren't enough arguments": function() {
+				assert.throws(function(){
+					Expression.parseOperand({$cond:[1,2]}, {});
 				});
 			},
 
-			"should throw Error when constructing with 1 arg": function testConstructor1(){
+			"should fail if there are too many arguments": function() {
 				assert.throws(function(){
-					new CondExpression(1);
+					Expression.parseOperand({$cond:[1, 2, 3, 4]}, {});
 				});
-			}
-		},
+			},
 
-		"#getOpName()": {
+			"should evaluate boolean expression as true, then return 1; [ true === true, 1, 0 ]": function () {
+				assert.strictEqual(Expression.parseOperand({$cond: [ true, 1, 0 ]}, {}).evaluate({}), 1);
+			},
 
-			"should return the correct op name; $cond": function testOpName(){
-				assert.equal(new CondExpression().getOpName(), "$cond");
-			}
+			"should evaluate boolean expression as false, then return 0; [ false === true, 1, 0 ]": function () {
+				assert.strictEqual(Expression.parseOperand({$cond: [ false, 1, 0 ]}, {}).evaluate({}), 0);
+			},
 
 		},
 
-		"#evaluateInternal()": {
-			"array style": {
+		"object style": {
 
-				"should fail if there aren't enough arguments": function() {
-					assert.throws(function(){
-						Expression.parseOperand({$cond:[1,2]}, {});
-					})
-				},
-				"should fail if there are too many arguments": function() {
+			beforeEach: function(){
+				this.shouldFail = function(expr) {
 					assert.throws(function(){
-						Expression.parseOperand({$cond:[1, 2, 3, 4]}, {});
-					})
-				},
-				"should evaluate boolean expression as true, then return 1; [ true === true, 1, 0 ]": function () {
-					assert.strictEqual(Expression.parseOperand({$cond: [ true, 1, 0 ]}, {}).evaluateInternal({}), 1);
-				},
-
-				"should evaluate boolean expression as false, then return 0; [ false === true, 1, 0 ]": function () {
-					assert.strictEqual(Expression.parseOperand({$cond: [ false, 1, 0 ]}, {}).evaluateInternal({}), 0);
-				},
-				"should fail when the 'if' position is empty": function(){
-					assert.throws(function(){
-						Expression.parseOperand({$cond:[undefined, 2, 3]}, {});
-					})
-				},
-				"should fail when the 'then' position is empty": function(){
-					assert.throws(function(){
-						Expression.parseOperand({$cond:[1, undefined, 3]}, {});
-					})
-				},
-				"should fail when the 'else' position is empty": function(){
-					assert.throws(function(){
-						Expression.parseOperand({$cond:[1, 2, undefined]}, {});
-					})
-				}
+						Expression.parseOperand(expr, {});
+					});
+				};
+				this.vps = new VariablesParseState(new VariablesIdGenerator());
 			},
 
-			"object style": {
-				beforeEach: function(){
-					this.shouldFail = function(expr) {
-						assert.throws(function(){
-							Expression.parseOperand(expr, {});
-						});
-					}
-				},
-				"should fail because the $cond is missing": function(){
-					this.shouldFail({$zoot:[true, 1, 0 ]}, {});
-				},
-				"should fail because of missing if": function(){
-					this.shouldFail({$cond:{xif:1, then:2, else:3}});
-				},
-				"should fail because of missing then": function(){
-					this.shouldFail({$cond:{if:1, xthen:2, else:3}});
-				},
-				"should fail because of missing else": function(){
-					this.shouldFail({$cond:{if:1, then:2, xelse:3}});
-				},
-				"should fail because of empty if": function(){
-					this.shouldFail({$cond:{if:undefined, then:2, else:3}});
-				},
-				"should fail because of empty then": function(){
-					this.shouldFail({$cond:{if:1, then:undefined, else:3}});
-				},
-				"should fail because of empty else": function(){
-					this.shouldFail({$cond:{if:1, then:2, else:undefined}});
-				},
-				"should fail because of mystery args": function(){
-					this.shouldFail({$cond:{if:1, then:2, else:3, zoot:4}});
-				},
-				"should evaluate true": function(){
-					assert.strictEqual(
-						Expression.parseOperand({$cond:{ if: true, then: 1, else: 0}}, {}).evaluate({}),
-						1);
-				},
-				"should evaluate true even with mixed up args": function(){
-					assert.strictEqual(
-						Expression.parseOperand({$cond:{ else: 0, then: 1, if: "$a" }}, {}).evaluate({$a: 1}),
-						1);
-				},
-				"should evaluate false": function(){
-					assert.strictEqual(
-						Expression.parseOperand({$cond:{ if: "$a", then: 0, else: 1}}, {}).evaluate({$a: 0}),
-						1);
-				},
-				"should evaluate false even with mixed up args": function() {
-					assert.strictEqual(
-						Expression.parseOperand({$cond: { else: 1, then: 0, if: "$a"}}, {}).evaluate({$a: 0}),
-						1);
-				}
-			}
-		}
-	}
-};
+			"should fail because of missing if": function(){
+				this.shouldFail({$cond:{ then:2, else:3}});
+			},
+
+			"should fail because of missing then": function(){
+				this.shouldFail({$cond:{if:1,  else:3}});
+			},
+
+			"should fail because of missing else": function(){
+				this.shouldFail({$cond:{if:1, then:2 }});
+			},
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+			"should fail because of mystery args": function(){
+				this.shouldFail({$cond:{if:1, then:2, else:3, zoot:4}});
+			},
+
+			"should evaluate true": function(){
+				assert.strictEqual(
+					Expression.parseOperand({$cond:{ if: true, then: 1, else: 0}}, {}).evaluate({}),
+					1);
+			},
+
+			"should evaluate true even with mixed up args": function(){
+				assert.strictEqual(
+					Expression.parseOperand({$cond:{ else: 0, then: 1, if: "$a" }}, this.vps).evaluate({a: 1}),
+					1);
+			},
+
+			"should evaluate false": function(){
+				assert.strictEqual(
+					Expression.parseOperand({$cond:{ if: "$a", then: 0, else: 1}}, this.vps).evaluate({a: 0}),
+					1);
+			},
+
+			"should evaluate false even with mixed up args": function() {
+				assert.strictEqual(
+					Expression.parseOperand({$cond: { else: 1, then: 0, if: "$a"}}, this.vps).evaluate({a: 0}),
+					1);
+			},
+
+		},
+
+	},
+
+};

+ 29 - 59
test/lib/pipeline/expressions/DayOfMonthExpression.js

@@ -1,74 +1,44 @@
 "use strict";
 var assert = require("assert"),
-		DayOfMonthExpression = require("../../../../lib/pipeline/expressions/DayOfMonthExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression"),
-		VariablesParseState = require("../../../../Lib/pipeline/expressions/VariablesParseState"),
-		VariablesIdGenerator = require("../../../../Lib/pipeline/expressions/VariablesIdGenerator");
+	DayOfMonthExpression = require("../../../../lib/pipeline/expressions/DayOfMonthExpression"),
+	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.DayOfMonthExpression = {
 
-		"DayOfMonthExpression": {
+	"constructor()": {
 
-				"constructor()": {
+		"should create instance": function() {
+			assert(new DayOfMonthExpression() instanceof DayOfMonthExpression);
+			assert(new DayOfMonthExpression() instanceof Expression);
+		},
 
-						"should throw Error when constructing without args": function testConstructor() {
-								assert.throws(function() {
-										new DayOfMonthExpression();
-								});
-						},
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new DayOfMonthExpression("bad stuff");
+			});
+		},
 
-						"should not throw Error when constructing with an arg": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new DayOfMonthExpression("1/1/2014");
-								});
-						}
+	},
 
-				},
+	"#getOpName()": {
 
-				"#getOpName()": {
+		"should return the correct op name; $dayOfMonth": function() {
+			assert.equal(new DayOfMonthExpression().getOpName(), "$dayOfMonth");
+		},
 
-						"should return the correct op name; $dayOfMonth": function testOpName() {
-								assert.equal(new DayOfMonthExpression("1/1/2014").getOpName(), "$dayOfMonth");
-						}
+	},
 
-				},
+	"#evaluate()": {
 
-				"#evaulateInternal1()": {
+		"should return day of month; 10 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$dayOfMonth", operands);
+			assert.strictEqual(expr.evaluate({}), 1);
+		},
 
-						"should return day of month; 10 for 2013-03-10": function testOpName() {
-								assert.equal(new DayOfMonthExpression("2013-03-10T00:00:00.000Z").evaluateInternal(), "10");
-						}
+	},
 
-				},
-
-				"#evaluateInternal2()": {
-
-						"should return day of month; 18 for 2013-02-18": function testStuff() {
-
-								var idGenerator = new VariablesIdGenerator();
-								var vps = new VariablesParseState(idGenerator);
-								var parseOp = Expression.parseOperand({
-										$dayOfMonth: "$someDate"
-								}, vps);
-
-								var result = parseOp.evaluateInternal({
-										$someDate: new Date("2013-02-18T00:00:00.000Z")
-								});
-
-								assert.strictEqual(result, "2");
-
-										// assert.strictEqual(Expression.parseOperand({
-										// $dayOfMonth: "$someDate"
-										// }, vps).evaluate({
-										// someDate: new Date("2013-02-18T00:00:00.000Z")
-										// }), 18);
-								}
-
-						}
-
-				}
-
-		};
-
-		if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+};

+ 23 - 28
test/lib/pipeline/expressions/DayOfWeekExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	DayOfWeekExpression = require("../../../../lib/pipeline/expressions/DayOfWeekExpression"),
 	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.DayOfWeekExpression = {
 
-	"DayOfWeekExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new DayOfWeekExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new DayOfWeekExpression() instanceof DayOfWeekExpression);
+			assert(new DayOfWeekExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $dayOfWeek": function testOpName(){
-				assert.equal(new DayOfWeekExpression().getOpName(), "$dayOfWeek");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new DayOfWeekExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new DayOfWeekExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $dayOfWeek": function() {
+			assert.equal(new DayOfWeekExpression().getOpName(), "$dayOfWeek");
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return day of week; 2 for 2013-02-18": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$dayOfWeek:"$someDate"}).evaluateInternal({someDate:new Date("2013-02-18T00:00:00.000Z")}), 2);
-			}
+	"#evaluate()": {
 
-		}
+		"should return day of week; 7 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$dayOfWeek", operands);
+			assert.strictEqual(expr.evaluate({}), 7);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 23 - 28
test/lib/pipeline/expressions/DayOfYearExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	DayOfYearExpression = require("../../../../lib/pipeline/expressions/DayOfYearExpression"),
 	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.DayOfYearExpression = {
 
-	"DayOfYearExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new DayOfYearExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new DayOfYearExpression() instanceof DayOfYearExpression);
+			assert(new DayOfYearExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $dayOfYear": function testOpName(){
-				assert.equal(new DayOfYearExpression().getOpName(), "$dayOfYear");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new DayOfYearExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new DayOfYearExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $dayOfYear": function() {
+			assert.equal(new DayOfYearExpression().getOpName(), "$dayOfYear");
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return day of year; 49 for 2013-02-18": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$dayOfYear:"$someDate"}).evaluate({someDate:new Date("2013-02-18T00:00:00.000Z")}), 49);
-			}
+	"#evaluate()": {
 
-		}
+		"should return day of year; 305 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$dayOfYear", operands);
+			assert.strictEqual(expr.evaluate({}), 305);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 47 - 0
test/lib/pipeline/expressions/DivideExpression_test.js

@@ -0,0 +1,47 @@
+"use strict";
+var assert = require("assert"),
+	DivideExpression = require("../../../../lib/pipeline/expressions/DivideExpression"),
+	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));
+
+exports.DivideExpression = {
+
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new DivideExpression() instanceof DivideExpression);
+			assert(new DivideExpression() instanceof Expression);
+		},
+
+		"should error if given args": function() {
+			assert.throws(function() {
+				new DivideExpression("bad stuff");
+			});
+		}
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $size": function() {
+			assert.equal(new DivideExpression().getOpName(), "$divide");
+		}
+
+	},
+
+	"#evaluate()": {
+
+		"should divide two numbers": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$divide: ["$a", "$b"]}, vps),
+				input = {a: 6, b: 2};
+			assert.strictEqual(expr.evaluate(input), 3);
+		}
+
+	}
+
+};

+ 23 - 28
test/lib/pipeline/expressions/HourExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	HourExpression = require("../../../../lib/pipeline/expressions/HourExpression"),
 	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.HourExpression = {
 
-	"HourExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new HourExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new HourExpression() instanceof HourExpression);
+			assert(new HourExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $hour": function testOpName(){
-				assert.equal(new HourExpression().getOpName(), "$hour");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new HourExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new HourExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $hour": function() {
+			assert.equal(new HourExpression().getOpName(), "$hour");
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return hour; 15 for 2013-02-18 3:00pm": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$hour:"$someDate"}).evaluate({someDate:new Date("2013-02-18T15:00:00.000Z")}), 15);
-			}
+	"#evaluate()": {
 
-		}
+		"should return hour; 19 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$hour", operands);
+			assert.strictEqual(expr.evaluate({}), 19);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 0 - 58
test/lib/pipeline/expressions/IfNullExpression.js

@@ -1,58 +0,0 @@
-"use strict";
-var assert = require("assert"),
-		IfNullExpression = require("../../../../lib/pipeline/expressions/IfNullExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-		"IfNullExpression": {
-
-				"constructor()": {
-
-						"should not throw Error when constructing without args": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new IfNullExpression();
-								});
-						}
-
-				},
-
-				"#getOpName()": {
-
-						"should return the correct op name; $ifNull": function testOpName() {
-								assert.equal(new IfNullExpression().getOpName(), "$ifNull");
-						}
-
-				},
-
-				"#evaluateInternal()": {
-
-						"should return the left hand side if the left hand side is not null or undefined": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$ifNull: ["$a", "$b"]
-								}).evaluateInternal({
-										a: 1,
-										b: 2
-								}), 1);
-						},
-						"should return the right hand side if the left hand side is null or undefined": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$ifNull: ["$a", "$b"]
-								}).evaluateInternal({
-										a: null,
-										b: 2
-								}), 2);
-								assert.strictEqual(Expression.parseOperand({
-										$ifNull: ["$a", "$b"]
-								}).evaluateInternal({
-										b: 2
-								}), 2);
-						}
-				}
-
-		}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 40 - 45
test/lib/pipeline/expressions/IfNullExpression_test.js

@@ -6,59 +6,54 @@ var assert = require("assert"),
 	Variables = require("../../../../lib/pipeline/expressions/Variables"),
 	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.IfNullExpression = {
 
-	"IfNullExpression": {
+	"constructor()": {
 
-		"constructor()": {
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function () {
+				new IfNullExpression();
+			});
+		},
 
-			"should not throw Error when constructing without args": function() {
-				assert.doesNotThrow(function () {
-					new IfNullExpression();
-				});
-			},
-			"should throw Error when constructing with args": function () {
-				assert.throws(function () {
-					new IfNullExpression(1);
-				});
-			}
+		"should throw Error when constructing with args": function () {
+			assert.throws(function () {
+				new IfNullExpression(1);
+			});
 		},
 
-		"#getOpName()": {
+	},
 
-			"should return the correct op name; $ifNull": function() {
-				assert.equal(new IfNullExpression().getOpName(), "$ifNull");
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $ifNull": function() {
+			assert.equal(new IfNullExpression().getOpName(), "$ifNull");
 		},
 
-		"#evaluateInternal()": {
-			beforeEach: function () {
-				this.vps = new VariablesParseState(new VariablesIdGenerator());
-				this.parsed = Expression.parseExpression("$ifNull", ["$a", "$b"], this.vps);
-				this.vars = new Variables(2);
-				this.vars.setValue(0, "a");
-				this.vars.setValue(1, "b");
-				this.makeParsed = function(a, b) {
-					return Expression.parseExpression("$ifNull", [a, b], this.vps);
-				}
-			},
-
-			"should return the left hand side if the left hand side is not null or undefined": function() {
-				//assert.strictEqual(this.parsed.evaluate(this.vars), 1);
-				assert.strictEqual(this.makeParsed(1, 2).evaluate(this.vars), 1);
-			},
-			"should return the right hand side if the left hand side is null": function() {
-				//assert.strictEqual(this.parsed.evaluate({a: null, b: 2}), 2);
-				assert.strictEqual(this.makeParsed(null, 2).evaluate(this.vars), 2);
-			},
-			"should return the right hand side if the left hand side is undefined": function() {
-				//assert.strictEqual(this.parsed.evaluate({b: 2}), 2);
-				assert.strictEqual(this.makeParsed(undefined, 2).evaluate(this.vars), 2);
-			}
-		}
-	}
-};
+	},
+
+	"#evaluate()": {
+
+		beforeEach: function () {
+			this.vps = new VariablesParseState(new VariablesIdGenerator());
+			this.parsed = Expression.parseExpression("$ifNull", ["$a", "$b"], this.vps);
+		},
+
+		"should return the left hand side if the left hand side is not null or undefined": function() {
+			assert.strictEqual(this.parsed.evaluate({a: 1, b: 2}), 1);
+		},
 
-if (!module.parent)(new (require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+		"should return the right hand side if the left hand side is null": function() {
+			assert.strictEqual(this.parsed.evaluate({a: null, b: 2}), 2);
+		},
+
+		"should return the right hand side if the left hand side is undefined": function() {
+			assert.strictEqual(this.parsed.evaluate({b: 2}), 2);
+		},
+
+	},
+
+};

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

+ 28 - 43
test/lib/pipeline/expressions/MillisecondExpression.js

@@ -1,59 +1,44 @@
 "use strict";
 var assert = require("assert"),
-		MillisecondExpression = require("../../../../lib/pipeline/expressions/MillisecondExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
+	MillisecondExpression = require("../../../../lib/pipeline/expressions/MillisecondExpression"),
+	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.MillisecondExpression = {
 
-		"MillisecondExpression": {
+	"constructor()": {
 
-				"constructor()": {
+		"should create instance": function() {
+			assert(new MillisecondExpression() instanceof MillisecondExpression);
+			assert(new MillisecondExpression() instanceof Expression);
+		},
 
-						"should not throw Error when constructing without args": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new MillisecondExpression();
-								});
-						}
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new MillisecondExpression("bad stuff");
+			});
+		},
 
-				},
+	},
 
-				"#getOpName()": {
+	"#getOpName()": {
 
-						"should return the correct op name; $millisecond": function testOpName() {
-								assert.equal(new MillisecondExpression().getOpName(), "$millisecond");
-						}
+		"should return the correct op name; $millisecond": function() {
+			assert.equal(new MillisecondExpression().getOpName(), "$millisecond");
+		},
 
-				},
+	},
 
-				"#getFactory()": {
+	"#evaluate()": {
 
-						"should return the constructor for this class": function factoryIsConstructor() {
-								assert.strictEqual(new MillisecondExpression().getFactory(), undefined);
-						}
+		"should return millisecond; 819 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$millisecond", operands);
+			assert.strictEqual(expr.evaluate({}), 819);
+		},
 
-				},
-
-				"#evaluate()": {
-
-						"should return the current millisecond in the date; 19 for 2013-02-18 11:24:19 EST": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$millisecond: "$someDate"
-								}).evaluate({
-										someDate: new Date("2013-02-18T11:24:19.456Z")
-								}), 456);
-						}
-
-						/*
-			"should return the leap millisecond in the date; 60 for June 30, 2012 at 23:59:60 UTC": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$millisecond:"$someDate"}).evaluate({someDate:new Date("June 30, 2012 at 23:59:60 UTC")}), 60);
-			}
-
-				*/
-				}
-
-		}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 23 - 28
test/lib/pipeline/expressions/MinuteExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	MinuteExpression = require("../../../../lib/pipeline/expressions/MinuteExpression"),
 	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.MinuteExpression = {
 
-	"MinuteExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new MinuteExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new MinuteExpression() instanceof MinuteExpression);
+			assert(new MinuteExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $minute": function testOpName(){
-				assert.equal(new MinuteExpression().getOpName(), "$minute");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new MinuteExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new MinuteExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $minute": function() {
+			assert.equal(new MinuteExpression().getOpName(), "$minute");
 		},
 
-		"#evaluateInternal()": {
+	},
 
-			"should return minute; 47 for 2013-02-18 3:47 pm": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$minute:"$someDate"}).evaluateInternal({someDate:new Date("2013-02-18T15:47:00.000Z")}), 47);
-			}
+	"#evaluate()": {
 
-		}
+		"should return minute; 31 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$minute", operands);
+			assert.strictEqual(expr.evaluate({}), 31);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 62 - 35
test/lib/pipeline/expressions/ModExpression.js

@@ -1,53 +1,80 @@
 "use strict";
 var assert = require("assert"),
 	ModExpression = require("../../../../lib/pipeline/expressions/ModExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression"),
-	VariablesParseState = require("../../../../lib/pipeline/expressions/Expression");
+	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));
 
-module.exports = {
+exports.ModExpression = {
 
-	"ModExpression": {
+	"constructor()": {
 
-		"constructor()": {
+		"should construct instance": function() {
+			assert(new ModExpression() instanceof ModExpression);
+			assert(new ModExpression() instanceof Expression);
+		},
+
+		"should error if given args": function() {
+			assert.throws(function() {
+				new ModExpression("bad stuff");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $mod": function() {
+			assert.equal(new ModExpression().getOpName(), "$mod");
+		},
+
+	},
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new ModExpression();
-				});
-			}
+	"#evaluate()": {
 
+		"should return modulus of two numbers": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: 6, b: 2};
+			assert.strictEqual(expr.evaluate(input), 0);
 		},
 
-		"#getOpName()": {
-			"should return the correct op name; $mod": function testOpName(){
-				assert.equal(new ModExpression().getOpName(), "$mod");
-			}
+		"should return null if first is null": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: null, b: 2};
+			assert.strictEqual(expr.evaluate(input), null);
+		},
 
+		"should return null if first is undefined": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: undefined, b: 2};
+			assert.strictEqual(expr.evaluate(input), null);
 		},
 
-		"#evaluateInternal()": {
-			"should return rhs if rhs is undefined or null": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}, new VariablesParseState()).evaluate({lhs:20.453, rhs:null}), null);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:20.453}), undefined);
-			},
-			"should return lhs if lhs is undefined or null": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:null, rhs:20.453}), null);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({rhs:20.453}), undefined);
-			},
-			"should return undefined if rhs is 0": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:20.453, rhs:0}), undefined);
-			},
-			"should return proper mod of rhs and lhs if both are numbers": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:234.4234, rhs:45}), 234.4234 % 45);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:0, rhs:45}), 0 % 45);
-				assert.strictEqual(Expression.parseOperand({$mod:["$lhs", "$rhs"]}).evaluate({lhs:-6, rhs:-0.5}), -6 % -0.5);
-			}
+		"should return null if second is null": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: 11, b: null};
+			assert.strictEqual(expr.evaluate(input), null);
+		},
 
-		}
+		"should return null if second is undefined": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$mod: ["$a", "$b"]}, vps),
+				input = {a: 42, b: undefined};
+			assert.strictEqual(expr.evaluate(input), null);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 23 - 28
test/lib/pipeline/expressions/MonthExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	MonthExpression = require("../../../../lib/pipeline/expressions/MonthExpression"),
 	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.MonthExpression = {
 
-	"MonthExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new MonthExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new MonthExpression() instanceof MonthExpression);
+			assert(new MonthExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $month": function testOpName(){
-				assert.equal(new MonthExpression().getOpName(), "$month");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new MonthExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new MonthExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $month": function() {
+			assert.equal(new MonthExpression().getOpName(), "$month");
 		},
 
-		"#evaluateInternal()": {
+	},
 
-			"should return month; 2 for 2013-02-18": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$month:"$someDate"}).evaluate({someDate:new Date("2013-02-18T00:00:00.000Z")}), 2);
-			}
+	"#evaluate()": {
 
-		}
+		"should return month; 11 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$month", operands);
+			assert.strictEqual(expr.evaluate({}), 11);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

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

+ 0 - 45
test/lib/pipeline/expressions/NotExpression.js

@@ -1,45 +0,0 @@
-"use strict";
-var assert = require("assert"),
-	NotExpression = require("../../../../lib/pipeline/expressions/NotExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-	"NotExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new NotExpression();
-				});
-			}
-
-		},
-
-		"#getOpName()": {
-
-			"should return the correct op name; $not": function testOpName(){
-				assert.equal(new NotExpression().getOpName(), "$not");
-			}
-
-		},
-
-		"#evaluateInternal()": {
-
-			"should return false for a true input; false for true": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$not:true}).evaluateInternal({}), false);
-			},
-
-			"should return true for a false input; true for false": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$not:false}).evaluateInternal({}), true);
-			}
-
-		}
-
-	}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 47 - 0
test/lib/pipeline/expressions/NotExpression_test.js

@@ -0,0 +1,47 @@
+"use strict";
+var assert = require("assert"),
+	NotExpression = require("../../../../lib/pipeline/expressions/NotExpression"),
+	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));
+
+exports.NotExpression = {
+
+	"constructor()": {
+
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function(){
+				new NotExpression();
+			});
+		},
+
+		"should throw when constructing with args": function() {
+			assert.throws(function(){
+				new NotExpression(1);
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $not": function() {
+			assert.equal(new NotExpression().getOpName(), "$not");
+		},
+
+	},
+
+	"#evaluate()": {
+
+		"should return false for a true input; false for true": function() {
+			assert.strictEqual(Expression.parseOperand({$not:true}, {}).evaluateInternal({}), false);
+		},
+
+		"should return true for a false input; true for false": function() {
+			assert.strictEqual(Expression.parseOperand({$not:false}, {}).evaluateInternal({}), true);
+		},
+
+	},
+
+};

+ 23 - 34
test/lib/pipeline/expressions/SecondExpression.js

@@ -3,53 +3,42 @@ var assert = require("assert"),
 	SecondExpression = require("../../../../lib/pipeline/expressions/SecondExpression"),
 	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.SecondExpression = {
 
-	"SecondExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new SecondExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new SecondExpression() instanceof SecondExpression);
+			assert(new SecondExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $second": function testOpName(){
-				assert.equal(new SecondExpression().getOpName(), "$second");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new SecondExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new SecondExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $second": function() {
+			assert.equal(new SecondExpression().getOpName(), "$second");
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return the current second in the date; 19 for 2013-02-18 11:24:19 EST": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$second:"$someDate"}).evaluate({someDate:new Date("2013-02-18T11:24:19.000Z")}), 19);
-			}
+	"#evaluate()": {
 
-				/*
-			"should return the leap second in the date; 60 for June 30, 2012 at 23:59:60 UTC": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$second:"$someDate"}).evaluate({someDate:new Date("June 30, 2012 at 23:59:60 UTC")}), 60);
-			}
-
-				*/
-		}
+		"should return second; 53 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$second", operands);
+			assert.strictEqual(expr.evaluate({}), 53);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 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();
+		},
+
+	},
+
+};

+ 36 - 30
test/lib/pipeline/expressions/SizeExpression.js

@@ -1,46 +1,52 @@
 "use strict";
-var assert = require("assert"),
-		SizeExpression = require("../../../../lib/pipeline/expressions/SizeExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
 
+var assert = require("assert"),
+	SizeExpression = require("../../../../lib/pipeline/expressions/SizeExpression"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression");
 
-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));
 
-		"SizeExpression": {
+exports.SizeExpression = {
 
-				"constructor()": {
+	"constructor()": {
 
-						"should throw Error when constructing without args": function testConstructor() {
-								assert.throws(function() {
-										new SizeExpression();
-								});
-						}
+		"should construct instance": function testConstructor() {
+			assert(new SizeExpression() instanceof SizeExpression);
+			assert(new SizeExpression() instanceof Expression);
+		},
 
-				},
+		"should error if given args": function testConstructor() {
+			assert.throws(function() {
+				new SizeExpression("bad stuff");
+			});
+		}
 
-				"#getOpName()": {
+	},
 
-						"should return the correct op name; $size": function testOpName() {
-								assert.equal(new SizeExpression("test").getOpName(), "$size");
-						}
+	"#evaluate()": {
 
-				},
+		"should return the size": function testSize() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$size: ["$a"]}, vps),
+				input = {
+					a: [{a:1},{b:2}],
+					b: [{c:3}]
+				};
+			assert.strictEqual(expr.evaluate(input), 2);
+		}
 
-				"#evaluateInternal()": {
+	},
 
-						// New test not working
-						"should return the size": function testSize() {
-								assert.strictEqual(Expression.parseOperand({
-										$size: ["$a"]
-								}).evaluateInternal({
-										a: [{a:1},{b:2}],
-										b: [{c:3}]
-								}), 4);
-						}
-				}
+	"#getOpName()": {
 
+		"should return the correct op name; $size": function testOpName() {
+			assert.equal(new SizeExpression().getOpName(), "$size");
 		}
 
-};
+	}
 
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);
+};

+ 89 - 40
test/lib/pipeline/expressions/StrcasecmpExpression.js

@@ -1,60 +1,109 @@
 "use strict";
 var assert = require("assert"),
 	StrcasecmpExpression = require("../../../../lib/pipeline/expressions/StrcasecmpExpression"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	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(){
+			this.assertResult(this.expectedResult, this.spec());
+			this.assertResult(-this.expectedResult, this.reverseSpec());
+		};
+		proto.spec = function() { return {$strcasecmp:[this.a, this.b]}; };
+		proto.reverseSpec = function() { return {$strcasecmp:[this.b, this.a]}; };
+		proto.assertResult = function(expectedResult, spec) {
+			var specElement = spec,
+				idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expression = Expression.parseOperand(specElement, vps);
+			assert.deepEqual(constify(spec), expressionToJson(expression));
+			assert.equal(expectedResult, expression.evaluate({}));
+		};
+		return klass;
+	})();
+
+exports.StrcasecmpExpression = {
+
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new StrcasecmpExpression() instanceof StrcasecmpExpression);
+			assert(new StrcasecmpExpression() instanceof Expression);
+		},
 
-	"StrcasecmpExpression": {
+		"should error if given args": function() {
+			assert.throws(function() {
+				new StrcasecmpExpression("bad stuff");
+			});
+		},
 
-		"constructor()": {
+	},
 
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new StrcasecmpExpression();
-				});
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $strcasecmp": function(){
+			assert.equal(new StrcasecmpExpression().getOpName(), "$strcasecmp");
 		},
 
-		"#getOpName()": {
+	},
 
-			"should return the correct op name; $strcasecmp": function testOpName(){
-				assert.equal(new StrcasecmpExpression().getOpName(), "$strcasecmp");
-			}
+	"#evaluate()": {
 
+		"should return '_ab' == '_AB' (w/ null begin)": function NullBegin() {
+			new ExpectedResultBase({
+				a: "\0ab",
+				b: "\0AB",
+				expectedResult: 0,
+			}).run();
 		},
 
-		"#getFactory()": {
-
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new StrcasecmpExpression().getFactory(), undefined);
-			}
+		"should return 'ab_' == 'aB_' (w/ null end)": function NullEnd() {
+			new ExpectedResultBase({
+				a: "ab\0",
+				b: "aB\0",
+				expectedResult: 0,
+			}).run();
+		},
 
+		"should return 'a_a' < 'a_B' (w/ null middle)": function NullMiddleLt() {
+			new ExpectedResultBase({
+				a: "a\0a",
+				b: "a\0B",
+				expectedResult: -1,
+			}).run();
 		},
 
-		"#evaluateInternal()": {
+		"should return 'a_b' == 'a_B' (w/ null middle)": function NullMiddleEq() {
+			new ExpectedResultBase({
+				a: "a\0b",
+				b: "a\0B",
+				expectedResult: 0,
+			}).run();
+		},
 
-			"should return 0 if the strings are equivalent and begin with a null character": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$strcasecmp:["$a", "$b"]}).evaluateInternal({a:"\0ab", b:"\0AB"}), 0);
-			},
-			"should return 0 if the strings are equivalent and end with a null character": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$strcasecmp:["$a", "$b"]}).evaluateInternal({a:"ab\0", b:"AB\0"}), 0);
-			},
-			"should return -1 if the left hand side is less than the right hand side and both contain a null character": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$strcasecmp:["$a", "$b"]}).evaluateInternal({a:"a\0a", b:"A\0B"}), -1);
-			},
-			"should return 0 if the strings are equivalent and both contain a null character": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$strcasecmp:["$a", "$b"]}).evaluateInternal({a:"a\0b", b:"A\0B"}), 0);
-			},
-			"should return 1 if the left hand side is greater than the right hand side and both contain a null character": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$strcasecmp:["$a", "$b"]}).evaluateInternal({a:"a\0c", b:"A\0B"}), 1);
-			}
-		}
+		"should return 'a_c' > 'a_B' (w/ null middle)": function NullMiddleGt() {
+			new ExpectedResultBase({
+				a: "a\0c",
+				b: "a\0B",
+				expectedResult: 1,
+			}).run();
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 0 - 161
test/lib/pipeline/expressions/SubstrExpression.js

@@ -1,161 +0,0 @@
-"use strict";
-var assert = require("assert"),
-		SubstrExpression = require("../../../../lib/pipeline/expressions/SubstrExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
-
-
-module.exports = {
-
-		"SubstrExpression": {
-
-				"constructor()": {
-
-						"should not throw Error when constructing without args": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new SubstrExpression();
-								});
-						}
-
-				},
-
-				"#getOpName()": {
-
-						"should return the correct op name; $substr": function testOpName() {
-								assert.equal(new SubstrExpression().getOpName(), "$substr");
-						}
-
-				},
-
-				"#evaluateInternal()": {
-
-						"Should fail if no end argument is given": function testMissing3rdArg() {
-								var s = "mystring",
-										start = 0,
-										end = s.length;
-								assert.throws(function() {
-										Expression.parseOperand({
-												$substr: ["$s", "$start"]
-										}).evaluateInternal({
-												s: s,
-												start: start
-										});
-								});
-						},
-
-						"Should return entire string when called with 0 and length": function testWholeString() {
-								var s = "mystring",
-										start = 0,
-										end = s.length;
-								assert.strictEqual(Expression.parseOperand({
-										$substr: ["$s", "$start", "$end"]
-								}).evaluateInternal({
-										s: s,
-										start: start,
-										end: end
-								}), "mystring");
-						},
-
-						"Should return entire string less the last character when called with 0 and length-1": function testLastCharacter() {
-								var s = "mystring",
-										start = 0,
-										end = s.length;
-								assert.strictEqual(Expression.parseOperand({
-										$substr: ["$s", "$start", "$end"]
-								}).evaluateInternal({
-										s: s,
-										start: start,
-										end: end - 1
-								}), "mystrin");
-						},
-
-						"Should return empty string when 0 and 0 are given as indexes": function test00Indexes() {
-								var s = "mystring",
-										start = 0,
-										end = 0;
-								assert.strictEqual(Expression.parseOperand({
-										$substr: ["$s", "$start", "$end"]
-								}).evaluateInternal({
-										s: s,
-										start: start,
-										end: end
-								}), "");
-						},
-
-						"Should first character when 0 and 1 are given as indexes": function testFirstCharacter() {
-								var s = "mystring",
-										start = 0,
-										end = 1;
-								assert.strictEqual(Expression.parseOperand({
-										$substr: ["$s", "$start", "$end"]
-								}).evaluateInternal({
-										s: s,
-										start: start,
-										end: end
-								}), "m");
-						},
-
-						"Should return empty string when empty string is given": function testEmptyString() {
-								var s = "",
-										start = 0,
-										end = 0;
-								assert.strictEqual(Expression.parseOperand({
-										$substr: ["$s", "$start", "$end"]
-								}).evaluateInternal({
-										s: s,
-										start: start,
-										end: end
-								}), "");
-						},
-
-						"Should return the entire string if end is -1": function testIndexTooLarge() {
-								var s = "mystring",
-										start = 0,
-										end = -1;
-								assert.strictEqual(Expression.parseOperand({
-										$substr: ["$s", "$start", "$end"]
-								}).evaluateInternal({
-										s: s,
-										start: start,
-										end: end
-								}), "mystring");
-						},
-
-
-						"Should fail if end is before begin": function testUnorderedIndexes() {
-								var s = "mystring",
-										start = s.length,
-										end = 0;
-								assert.throws(function() {
-										Expression.parseOperand({
-												$substr: ["$s", "$start"]
-										}).evaluateInternal({
-												s: s,
-												start: start,
-												end: end
-										});
-								});
-						},
-
-						"Should fail if end is greater than length": function testIndexTooLarge() {
-								var s = "mystring",
-										start = 0,
-										end = s.length + 1;
-								assert.throws(function() {
-										Expression.parseOperand({
-												$substr: ["$s", "$start"]
-										}).evaluateInternal({
-												s: s,
-												start: start,
-												end: end
-										});
-								});
-						},
-
-
-				}
-
-		}
-
-};
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 109 - 0
test/lib/pipeline/expressions/SubstrExpression_test.js

@@ -0,0 +1,109 @@
+"use strict";
+var assert = require("assert"),
+	SubstrExpression = require("../../../../lib/pipeline/expressions/SubstrExpression"),
+	Expression = require("../../../../lib/pipeline/expressions/Expression"),
+	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
+	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.deepEqual(this.expectedResult, expr.evaluate({}));
+		};
+		proto.spec = function() { return {$substr:[this.str, this.offset, this.length]}; };
+		return klass;
+	})();
+
+exports.SubstrExpression = {
+
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new SubstrExpression() instanceof SubstrExpression);
+			assert(new SubstrExpression() instanceof Expression);
+		},
+
+		"should error if given args": function() {
+			assert.throws(function() {
+				new SubstrExpression("bad stuff");
+			});
+		},
+
+	},
+
+	"#getOpName()": {
+
+		"should return the correct op name; $substr": function() {
+			assert.equal(new SubstrExpression().getOpName(), "$substr");
+		},
+
+	},
+
+	"evaluate": {
+
+		"should return full string (if contains null)": function FullNull() {
+			new ExpectedResultBase({
+				str: "a\0b",
+				offset: 0,
+				length: 3,
+				get expectedResult(){ return this.str; },
+			}).run();
+		},
+
+		"should return tail of string (if begin at null)": function BeginAtNull() {
+			new ExpectedResultBase({
+				str: "a\0b",
+				offset: 1,
+				length: 2,
+				expectedResult: "\0b",
+			}).run();
+		},
+
+		"should return head of string (if end at null)": function EndAtNull() {
+			new ExpectedResultBase({
+				str: "a\0b",
+				offset: 0,
+				length: 2,
+				expectedResult: "a\0",
+			}).run();
+		},
+
+		"should return tail of string (if head has null) ": function DropBeginningNull() {
+			new ExpectedResultBase({
+				str: "\0b",
+				offset: 1,
+				length: 1,
+				expectedResult: "b",
+			}).run();
+		},
+
+		"should return head of string (if tail has null)": function DropEndingNull() {
+			new ExpectedResultBase({
+				str: "a\0",
+				offset: 0,
+				length: 1,
+				expectedResult: "a",
+			}).run();
+		},
+
+	},
+
+};

+ 111 - 27
test/lib/pipeline/expressions/SubtractExpression.js

@@ -1,45 +1,129 @@
 "use strict";
 var assert = require("assert"),
 		SubtractExpression = require("../../../../lib/pipeline/expressions/SubtractExpression"),
-		Expression = require("../../../../lib/pipeline/expressions/Expression");
+		Expression = require("../../../../lib/pipeline/expressions/Expression"),
+		VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
+		VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState");
 
+// 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.SubtractExpression = {
 
-		"SubtractExpression": {
+	"constructor()": {
 
-				"constructor()": {
+		"should not throw Error when constructing without args": function() {
+			assert.doesNotThrow(function() {
+				new SubtractExpression();
+			});
+		},
 
-						"should not throw Error when constructing without args": function testConstructor() {
-								assert.doesNotThrow(function() {
-										new SubtractExpression();
-								});
-						}
+	},
 
-				},
+	"#getOpName()": {
 
-				"#getOpName()": {
+		"should return the correct op name; $subtract": function() {
+			assert.equal(new SubtractExpression().getOpName(), "$subtract");
+		},
 
-						"should return the correct op name; $subtract": function testOpName() {
-								assert.equal(new SubtractExpression().getOpName(), "$subtract");
-						}
+	},
 
-				},
+	"#evaluateInternal()": {
 
-				"#evaluateInternal()": {
+		"should return the result of subtraction between two numbers": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				result = expr.evaluate({a:2, b:1}),
+				expected = 1;
+			assert.strictEqual(result, expected);
+		},
 
-						"should return the result of subtraction between two numbers": function testStuff() {
-								assert.strictEqual(Expression.parseOperand({
-										$subtract: ["$a", "$b"]
-								}).evaluateInternal({
-										a: 35636364,
-										b: -0.5656
-								}), 35636364 - (-0.5656));
-						}
-				}
+		"should return null if left is null": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				result = expr.evaluate({a:null, b:1}),
+				expected = null;
+			assert.strictEqual(result, expected);
+		},
 
-		}
+		"should return null if left is undefined": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				result = expr.evaluate({a:undefined, b:1}),
+				expected = null;
+			assert.strictEqual(result, expected);
+		},
+
+		"should return null if right is null": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				result = expr.evaluate({a:2, b:null}),
+				expected = null;
+			assert.strictEqual(result, expected);
+		},
+
+		"should return null if right is undefined": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				result = expr.evaluate({a:2, b:undefined}),
+				expected = null;
+			assert.strictEqual(result, expected);
+		},
+
+		"should subtract 2 dates": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				date2 = new Date("Jan 3 1990"),
+				date1 = new Date("Jan 1 1990"),
+				result = expr.evaluate({a:date2, b:date1}),
+				expected = date2 - date1;
+			assert.strictEqual(result, expected);
+		},
+
+		"should subtract a number of millis from a date": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				date2 = new Date("Jan 3 1990"),
+				millis = 24 * 60 * 60 * 1000,
+				result = expr.evaluate({a:date2, b:millis}),
+				expected = date2 - millis;
+			assert.strictEqual(
+				JSON.stringify(result),
+				JSON.stringify(expected)
+			);
+		},
+
+		"should throw if left is not a date or number": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				date2 = {},
+				date1 = new Date();
+			assert.throws(function() {
+				expr.evaluate({a:date2, b:date1});
+			});
+		},
+
+		"should throw if right is not a date or number": function() {
+			var idGenerator = new VariablesIdGenerator(),
+				vps = new VariablesParseState(idGenerator),
+				expr = Expression.parseOperand({$subtract:["$a", "$b"]}, vps),
+				date2 = new Date(),
+				date1 = {};
+			assert.throws(function() {
+				expr.evaluate({a:date2, b:date1});
+			});
+		},
+
+	},
 
 };
 
-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);

+ 70 - 45
test/lib/pipeline/expressions/ToLowerExpression_test.js

@@ -3,65 +3,90 @@ var assert = require("assert"),
 	ToLowerExpression = require("../../../../lib/pipeline/expressions/ToLowerExpression"),
 	VariablesParseState = require("../../../../lib/pipeline/expressions/VariablesParseState"),
 	VariablesIdGenerator = require("../../../../lib/pipeline/expressions/VariablesIdGenerator"),
-	Expression = require("../../../../lib/pipeline/expressions/Expression");
+	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 = {
 
-		beforeEach: function () {
-			this.vps = new VariablesParseState(new VariablesIdGenerator());
+	"constructor()": {
+
+		"should construct instance": function() {
+			assert(new ToLowerExpression() instanceof ToLowerExpression);
+			assert(new ToLowerExpression() instanceof Expression);
 		},
 
-		"constructor()": {
+		"should error if given args": function() {
+			assert.throws(function() {
+				new ToLowerExpression("bad stuff");
+			});
+		},
 
-			"should not throw Error when constructing without args": function testConstructor() {
-				assert.doesNotThrow(function () {
-					new ToLowerExpression();
-				});
-			},
+	},
 
-			"should throw Error when constructing with args": function testConstructor() {
-				assert.throws(function () {
-					new ToLowerExpression(1);
-				});
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $toLower": function() {
+			assert.equal(new ToLowerExpression().getOpName(), "$toLower");
 		},
 
-		"#getOpName()": {
+	},
 
-			"should return the correct op name; $toLower": function testOpName() {
-				assert.equal(new ToLowerExpression().getOpName(), "$toLower");
-			}
+	"#evaluate()": {
+
+		"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();
 		},
 
-		"#evaluate()": {
-
-			"should lowercase a string": function(){
-				assert.strictEqual(Expression.parseOperand({$toLower: "$a"}, this.vps).evaluate({a: "NOW IS THE TIME"}), "now is the time");
-			},
-			"should not change symbols": function(){
-				var symbs = "!@#$%^&*()_+{}[]:\";'<>?/.,;";
-				assert.strictEqual(Expression.parseOperand({$toLower: "$a"}, this.vps).evaluate({a: symbs}), symbs);
-			},
-			"should not change lowercase": function(){
-				var symbs = "now is the time for all good men to come from the aid of their computers";
-				assert.strictEqual(Expression.parseOperand({$toLower: "$a"}, this.vps).evaluate({a: symbs}), symbs);
-			},
-			"should return the lowercase version of the string if there is a null character in the middle of the string": function() {
-				assert.strictEqual(Expression.parseOperand({$toLower: "$a"}, this.vps).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() {
-				assert.strictEqual(Expression.parseOperand({$toLower: "$a"}, this.vps).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() {
-				assert.strictEqual(Expression.parseOperand({$toLower: "$a" }, this.vps).evaluate({a: "aB\0"}), "ab\0");
-			}
-		}
-	}
+		"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);

+ 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), {});
+		},
+
+	},
+
+};

+ 23 - 28
test/lib/pipeline/expressions/WeekExpression.js

@@ -3,47 +3,42 @@ var assert = require("assert"),
 	WeekExpression = require("../../../../lib/pipeline/expressions/WeekExpression"),
 	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.WeekExpression = {
 
-	"WeekExpression": {
-
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new WeekExpression();
-				});
-			}
+	"constructor()": {
 
+		"should create instance": function() {
+			assert(new WeekExpression() instanceof WeekExpression);
+			assert(new WeekExpression() instanceof Expression);
 		},
 
-		"#getOpName()": {
-
-			"should return the correct op name; $week": function testOpName(){
-				assert.equal(new WeekExpression().getOpName(), "$week");
-			}
-
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new WeekExpression("bad stuff");
+			});
 		},
 
-		"#getFactory()": {
+	},
 
-			"should return the constructor for this class": function factoryIsConstructor(){
-				assert.strictEqual(new WeekExpression().getFactory(), undefined);
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $week": function() {
+			assert.equal(new WeekExpression().getOpName(), "$week");
 		},
 
-		"#evaluate()": {
+	},
 
-			"should return week; 8 for 2013-02-18": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$week:"$someDate"}).evaluate({someDate:new Date("2013-02-18T00:00:00.000Z")}), 7);
-			}
+	"#evaluate()": {
 
-		}
+		"should return week; 7 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$week", operands);
+			assert.strictEqual(expr.evaluate({}), 43);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);

+ 24 - 21
test/lib/pipeline/expressions/YearExpression.js

@@ -3,39 +3,42 @@ var assert = require("assert"),
 	YearExpression = require("../../../../lib/pipeline/expressions/YearExpression"),
 	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.YearExpression = {
 
-	"YearExpression": {
+	"constructor()": {
 
-		"constructor()": {
-
-			"should not throw Error when constructing without args": function testConstructor(){
-				assert.doesNotThrow(function(){
-					new YearExpression();
-				});
-			}
+		"should create instance": function() {
+			assert(new YearExpression() instanceof YearExpression);
+			assert(new YearExpression() instanceof Expression);
+		},
 
+		"should error if given invalid args": function() {
+			assert.throws(function() {
+				new YearExpression("bad stuff");
+			});
 		},
 
-		"#getOpName()": {
+	},
 
-			"should return the correct op name; $year": function testOpName(){
-				assert.equal(new YearExpression().getOpName(), "$year");
-			}
+	"#getOpName()": {
 
+		"should return the correct op name; $year": function() {
+			assert.equal(new YearExpression().getOpName(), "$year");
 		},
 
-		"#evaluateInternal()": {
+	},
 
-			"should return year; 2013 for 2013-02-18": function testStuff(){
-				assert.strictEqual(Expression.parseOperand({$year:"$someDate"}).evaluate({someDate:new Date("Mon Feb 18 2013 00:00:00 GMT-0500 (EST)")}), 2013);
-			}
+	"#evaluate()": {
 
-		}
+		"should return year; 2014 for 2014-11-01T19:31:53.819Z": function() {
+			var operands = [new Date("2014-11-01T19:31:53.819Z")],
+				expr = Expression.parseExpression("$year", operands);
+			assert.strictEqual(expr.evaluate({}), 2014);
+		},
 
-	}
+	},
 
 };
-
-if (!module.parent)(new(require("mocha"))()).ui("exports").reporter("spec").addFile(__filename).run(process.exit);