You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2018/12/20 21:47:21 UTC
[airavata-django-portal] 02/02: AIRAVATA-2761 Unit tests for
boolean expression evaluation
This is an automated email from the ASF dual-hosted git repository.
machristie pushed a commit to branch airavata-2761
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git
commit a8d663700ee4c6b85cdec6c78b17a237ad4584c0
Author: Marcus Christie <ma...@iu.edu>
AuthorDate: Thu Dec 20 16:47:00 2018 -0500
AIRAVATA-2761 Unit tests for boolean expression evaluation
---
.travis.yml | 1 +
django_airavata/apps/api/package.json | 7 +-
.../js/models/InputDataObjectType.js | 13 +-
.../dependencies/BooleanExpressionEvaluator.js | 101 ++++++++++
.../js/models/dependencies/DependencyEvaluator.js | 94 ---------
.../BooleanExpressionEvaluator.test.js | 213 +++++++++++++++++++++
6 files changed, 326 insertions(+), 103 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index c628f90..2939209 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,3 +10,4 @@ script:
# For now ignore long line endings
- flake8 --ignore=E501 .
- ./build_js.sh
+ - cd django_airavata/apps/api && npm run test
diff --git a/django_airavata/apps/api/package.json b/django_airavata/apps/api/package.json
index a595c69..6009c29 100644
--- a/django_airavata/apps/api/package.json
+++ b/django_airavata/apps/api/package.json
@@ -9,7 +9,9 @@
"repository": "https://github.com/apache/airavata-django-portal",
"scripts": {
"build": "babel static/django_airavata_api/js -d static/django_airavata_api/dist",
- "watch": "babel static/django_airavata_api/js --watch -d static/django_airavata_api/dist"
+ "watch": "babel static/django_airavata_api/js --watch -d static/django_airavata_api/dist",
+ "test": "jest",
+ "test:watch": "jest --watch"
},
"dependencies": {
"babel-runtime": "^6.26.0",
@@ -19,6 +21,7 @@
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
- "babel-preset-env": "^1.7.0"
+ "babel-preset-env": "^1.7.0",
+ "jest": "^23.6.0"
}
}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/InputDataObjectType.js b/django_airavata/apps/api/static/django_airavata_api/js/models/InputDataObjectType.js
index f3b9867..2a15279 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/InputDataObjectType.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/InputDataObjectType.js
@@ -1,6 +1,6 @@
import BaseModel from "./BaseModel";
import DataType from "./DataType";
-import DependencyEvaluator from "./dependencies/DependencyEvaluator";
+import BooleanExpressionEvaluator from "./dependencies/BooleanExpressionEvaluator";
import uuidv4 from "uuid/v4";
import ValidatorFactory from "./validators/ValidatorFactory";
@@ -219,14 +219,13 @@ export default class InputDataObjectType extends BaseModel {
*/
evaluateDependencies(inputValues) {
if (Object.keys(this.editorDependencies).length > 0) {
- const dependencyEvaluator = new DependencyEvaluator();
- const dependencyEvaluations = dependencyEvaluator.evaluateDependencies(
- inputValues,
- this.editorDependencies
- );
+ const booleanExpressionEvaluator = new BooleanExpressionEvaluator();
// TODO: if show changes to false, set value to null? Save off current value to restore when shown again?
if ("show" in dependencyEvaluations) {
- this.show = dependencyEvaluations["show"];
+ this.show = booleanExpressionEvaluator.evaluate(
+ dependencyEvaluations.show,
+ inputValues
+ );
}
}
}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/dependencies/BooleanExpressionEvaluator.js b/django_airavata/apps/api/static/django_airavata_api/js/models/dependencies/BooleanExpressionEvaluator.js
new file mode 100644
index 0000000..1962d80
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/dependencies/BooleanExpressionEvaluator.js
@@ -0,0 +1,101 @@
+export default class BooleanExpressionEvaluator {
+
+ /**
+ * Context to use for looking up values of variables in expressions.
+ * @param {object} context
+ */
+ constructor(context) {
+ this.context = context;
+ }
+ /**
+ * Evaluates boolean expression and returns boolean result.
+ * @param {object} expression
+ */
+ evaluate(expression) {
+ const keys = Object.keys(expression);
+ if (keys.length > 1) {
+ // Implicitly AND together several expressions
+ return this.evaluate(
+ {
+ AND: keys.map(k => {
+ const exp = {};
+ exp[k] = expression[k];
+ return exp;
+ })
+ }
+ );
+ }
+ if (keys.length < 1) {
+ throw new Error(
+ "Expression does not contain a key: " + JSON.stringify(expression)
+ );
+ }
+
+ const key = keys[0];
+ const value = expression[key];
+ if (key === "AND") {
+ if (value instanceof Array) {
+ const evaluations = value.map(exp => this.evaluate(exp));
+ return evaluations.reduce((acc, curr) => acc && curr);
+ } else {
+ throw new Error(
+ "Unrecognized operand value for AND: " + JSON.stringify(value)
+ );
+ }
+ } else if (key === "OR") {
+ if (value instanceof Array) {
+ const evaluations = value.map(exp => this.evaluate(exp));
+ return evaluations.reduce((acc, curr) => acc || curr);
+ } else {
+ throw new Error(
+ "Unrecognized operand value for OR: " + JSON.stringify(value)
+ );
+ }
+ } else if (key === "NOT") {
+ if (typeof value === "object" && !(value instanceof Array)) {
+ return !this.evaluate(value);
+ } else {
+ throw new Error(
+ "Unrecognized operand value for NOT: " + JSON.stringify(value)
+ );
+ }
+ }
+
+ if (typeof value === "object") {
+ if (!(key in this.context)) {
+ throw new Error(
+ "Missing context value for expression " +
+ JSON.stringify(expression) +
+ " in context " +
+ JSON.stringify(this.context)
+ );
+ }
+ const contextValue = this.context[key];
+ return this._evaluateComparison(contextValue, value);
+ }
+ }
+
+ _evaluateComparison(value, comparisonDefinition) {
+ const comparison = comparisonDefinition["comparison"];
+ if (!comparison) {
+ throw new Error(
+ "Expression definition is missing 'comparison' property: " +
+ JSON.stringify(comparisonDefinition)
+ );
+ }
+ if (comparison === "equals") {
+ return value === this._getComparisonValue(comparisonDefinition);
+ }
+ throw new Error("Unrecognized comparison " + JSON.stringify(comparison));
+ }
+
+ _getComparisonValue(comparisonDefinition) {
+ if (!("value" in comparisonDefinition)) {
+ throw new Error(
+ "Missing required 'value' property in comparison definition: " +
+ JSON.stringify(comparisonDefinition)
+ );
+ }
+ return comparisonDefinition["value"];
+ }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/dependencies/DependencyEvaluator.js b/django_airavata/apps/api/static/django_airavata_api/js/models/dependencies/DependencyEvaluator.js
deleted file mode 100644
index 0a4f04c..0000000
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/dependencies/DependencyEvaluator.js
+++ /dev/null
@@ -1,94 +0,0 @@
-
-// TODO: rename: BooleanExpressionEvaluator? There's really nothing related to "app input dependencies" in here.
-export default class DependencyEvaluator {
-
- /**
- * Evaluate all dependency expressions and return an object with the same
- * keys as dependenciesConfig but with the result of each expression.
- *
- * @param inputValues
- * @param dependenciesConfig
- */
- evaluateDependencies(inputValues, dependenciesConfig) {
-
- const result = {};
- for (const k of Object.keys(dependenciesConfig)) {
- result[k] = this._evaluateExpression(dependenciesConfig[k], inputValues);
- }
- return result;
- }
-
- /**
- * Evaluates boolean expression for given context and returns boolean result.
- */
- _evaluateExpression(expression, context) {
- console.log("expression: " + JSON.stringify(expression));
- console.log("context: " + JSON.stringify(context));
- const keys = Object.keys(expression);
- if (keys.length > 1) {
- throw new Error("Expression contains more than one key: " + JSON.stringify(expression));
- }
- if (keys.length < 1) {
- throw new Error("Expression does not contain a key: " + JSON.stringify(expression));
- }
-
- const key = keys[0];
- const value = expression[key];
- if (key === "AND") {
- if (value instanceof Array) {
- const evaluations = value.map(exp => this._evaluateExpression(exp, context));
- return evaluations.reduce((acc, curr) => acc && curr);
- } else if (typeof value === 'object') {
- return this._evaluateExpression(exp, context);
- } else {
- throw new Error("Unrecognized operand value for AND: " + JSON.stringify(value));
- }
- } else if (key === "OR") {
- if (value instanceof Array) {
- const evaluations = value.map(exp => this._evaluateExpression(exp, context));
- return evaluations.reduce((acc, curr) => acc || curr);
- } else if (typeof value === 'object') {
- return this._evaluateExpression(exp, context);
- } else {
- throw new Error("Unrecognized operand value for OR: " + JSON.stringify(value));
- }
- } else if (key === "NOT") {
- if (typeof value === 'object') {
- return !this._evaluateExpression(value, context);
- } else {
- throw new Error("Unrecognized operand value for NOT: " + JSON.stringify(value));
- }
- }
-
- if (typeof value === 'object') {
- if (!(key in context)) {
- throw new Error("Missing context value for expression " + JSON.stringify(expression) + " in context " + JSON.stringify(context));
- }
- const contextValue = context[key];
- return this._evaluateExpressionType(contextValue, value);
- }
- }
-
- // TODO: rename: _evaluateExpressionDefinition
- // TODO: rename: _evaluateComparison
- _evaluateExpressionType(value, expressionType) {
- const type = expressionType["type"];
- if (!type) {
- throw new Error("Expression definition is missing 'type' property: " + JSON.stringify(expressionType));
- }
- if (type === "equals") {
- return value === this._getExpressionTypeValue(expressionType);
- }
- throw new Error("Unrecognized expression type " + JSON.stringify(expressionType));
- }
-
- // TODO: rename: _getExpressionDefinitionValue(expressionDefiniton)
- // TODO: rename: _getComparisonTarget
- _getExpressionTypeValue(expressionType) {
-
- if (!("value" in expressionType)) {
- throw new Error("Missing required 'value' property in expression definition: " + JSON.stringify(expressionType));
- }
- return expressionType["value"];
- }
-}
diff --git a/django_airavata/apps/api/static/django_airavata_api/tests/models/dependencies/BooleanExpressionEvaluator.test.js b/django_airavata/apps/api/static/django_airavata_api/tests/models/dependencies/BooleanExpressionEvaluator.test.js
new file mode 100644
index 0000000..f1d37eb
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/tests/models/dependencies/BooleanExpressionEvaluator.test.js
@@ -0,0 +1,213 @@
+import BooleanExpressionEvaluator from "../../../js/models/dependencies/BooleanExpressionEvaluator";
+
+const context = {
+ INPUT1: "1",
+ INPUT2: "2",
+ INPUT3: "3"
+};
+const booleanExpressionEvaluator = new BooleanExpressionEvaluator(context);
+
+test("throws error when expression is empty", () => {
+ expect(() => booleanExpressionEvaluator.evaluate({})).toThrow();
+});
+
+test("INPUT1 == 1 AND INPUT2 == 2 is TRUE", () => {
+ const result = booleanExpressionEvaluator.evaluate({
+ AND: [
+ {
+ INPUT1: {
+ comparison: "equals",
+ value: "1"
+ }
+ },
+ {
+ INPUT2: {
+ comparison: "equals",
+ value: "2"
+ }
+ }
+ ]
+ });
+ expect(result).toBe(true);
+});
+
+test("INPUT1 == 2 AND INPUT2 == 2 is FALSE", () => {
+ const result = booleanExpressionEvaluator.evaluate({
+ AND: [
+ {
+ INPUT1: {
+ comparison: "equals",
+ value: "2" // "1" !== "2"
+ }
+ },
+ {
+ INPUT2: {
+ comparison: "equals",
+ value: "2"
+ }
+ }
+ ]
+ });
+ expect(result).toBe(false);
+});
+
+test("INPUT1 == 2 OR INPUT2 == 2 is TRUE", () => {
+ const result = booleanExpressionEvaluator.evaluate({
+ OR: [
+ {
+ INPUT1: {
+ comparison: "equals",
+ value: "2"
+ }
+ },
+ {
+ INPUT2: {
+ comparison: "equals",
+ value: "2"
+ }
+ }
+ ]
+ });
+ expect(result).toBe(true);
+});
+
+test("INPUT1 == 2 OR INPUT2 == 1 is FALSE", () => {
+ const result = booleanExpressionEvaluator.evaluate({
+ OR: [
+ {
+ INPUT1: {
+ comparison: "equals",
+ value: "2"
+ }
+ },
+ {
+ INPUT2: {
+ comparison: "equals",
+ value: "1"
+ }
+ }
+ ]
+ });
+ expect(result).toBe(false);
+});
+
+test("(NOT INPUT1 == 2) AND INPUT2 == 2 is TRUE", () => {
+ const result = booleanExpressionEvaluator.evaluate({
+ AND: [
+ {
+ NOT: {
+ INPUT1: {
+ comparison: "equals",
+ value: "2"
+ }
+ }
+ },
+ {
+ INPUT2: {
+ comparison: "equals",
+ value: "2"
+ }
+ }
+ ]
+ });
+ expect(result).toBe(true);
+});
+
+// single comparison
+test("INPUT1 == 1 is TRUE", () => {
+ const result = booleanExpressionEvaluator.evaluate({
+ INPUT1: {
+ comparison: "equals",
+ value: "1"
+ }
+ });
+ expect(result).toBe(true);
+});
+
+// single comparison
+test("INPUT1 == 2 is FALSE", () => {
+ const result = booleanExpressionEvaluator.evaluate({
+ INPUT1: {
+ comparison: "equals",
+ value: "2"
+ }
+ });
+ expect(result).toBe(false);
+});
+
+test("non-array given for AND throws Error", () => {
+ expect(() =>
+ booleanExpressionEvaluator.evaluate({
+ AND: "a"
+ })
+ ).toThrow();
+});
+
+test("non-array given for OR throws Error", () => {
+ expect(() =>
+ booleanExpressionEvaluator.evaluate({
+ OR: "a"
+ })
+ ).toThrow();
+});
+
+test("non-object given for NOT throws Error", () => {
+ expect(() =>
+ booleanExpressionEvaluator.evaluate({
+ NOT: [
+ {
+ INPUT1: {
+ comparison: "equals",
+ value: 1
+ }
+ }
+ ]
+ })
+ ).toThrow();
+});
+
+test("referenced variable not in context throws Error", () => {
+ expect(() =>
+ booleanExpressionEvaluator.evaluate({
+ ZINPUT1: {
+ comparison: "equals",
+ value: 1
+ }
+ })
+ ).toThrow(/missing context value/i);
+});
+
+test("missing 'comparison' property throws Error", () => {
+ expect(() =>
+ booleanExpressionEvaluator.evaluate({
+ INPUT1: {
+ value: 1
+ }
+ })
+ ).toThrow(/missing 'comparison' property/i);
+});
+
+test("unrecognized 'comparison' property throws Error", () => {
+ expect(() =>
+ booleanExpressionEvaluator.evaluate({
+ INPUT1: {
+ comparison: "foo",
+ value: 1
+ }
+ })
+ ).toThrow(/unrecognized comparison/i);
+});
+
+test("Implicitly ANDed INPUT1 == 1 AND INPUT2 == 2 is TRUE", () => {
+ const result = booleanExpressionEvaluator.evaluate({
+ INPUT1: {
+ comparison: "equals",
+ value: "1"
+ },
+ INPUT2: {
+ comparison: "equals",
+ value: "2"
+ }
+ });
+ expect(result).toBe(true);
+});