From 26838635b6c3ee6fdf67a1db9e9a3b0baf2095d4 Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Fri, 10 Apr 2020 15:57:38 -0700 Subject: [PATCH] Ensures correct results are returned for conditions and criteria for a given telemetry datapoint (#2904) * Ensures that results for a specific datapoint are evaluated atomically. * Remove generating timestamp for telemetry data * Get results directly instead of using events * Refactor all/any telemetry criterion to use new evaluator * Use current timesystem to compare latest * AllTelemetryCriterion extends TelemetryCriterion Co-authored-by: David Tsay Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com> Co-authored-by: Andrew Henry --- src/plugins/condition/Condition.js | 103 +++------ src/plugins/condition/ConditionManager.js | 99 +++++---- src/plugins/condition/ConditionManagerSpec.js | 2 +- src/plugins/condition/ConditionSpec.js | 10 +- .../criterion/AllTelemetryCriterion.js | 83 ++----- .../condition/criterion/TelemetryCriterion.js | 96 ++++---- .../criterion/TelemetryCriterionSpec.js | 26 ++- src/plugins/condition/utils/evaluator.js | 68 +++--- src/plugins/condition/utils/evaluatorSpec.js | 206 +++++++++++++++--- src/plugins/condition/utils/time.js | 34 ++- 10 files changed, 412 insertions(+), 315 deletions(-) diff --git a/src/plugins/condition/Condition.js b/src/plugins/condition/Condition.js index 0f8deed113..838555105f 100644 --- a/src/plugins/condition/Condition.js +++ b/src/plugins/condition/Condition.js @@ -23,8 +23,7 @@ import EventEmitter from 'EventEmitter'; import uuid from 'uuid'; import TelemetryCriterion from "./criterion/TelemetryCriterion"; -import { TRIGGER } from "./utils/constants"; -import {computeCondition, computeConditionByLimit} from "./utils/evaluator"; +import { evaluateResults } from './utils/evaluator'; import { getLatestTimestamp } from './utils/time'; import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion"; @@ -57,29 +56,41 @@ export default class ConditionClass extends EventEmitter { this.conditionManager = conditionManager; this.id = conditionConfiguration.id; this.criteria = []; - this.criteriaResults = {}; this.result = undefined; - this.latestTimestamp = {}; this.timeSystems = this.openmct.time.getAllTimeSystems(); if (conditionConfiguration.configuration.criteria) { this.createCriteria(conditionConfiguration.configuration.criteria); } this.trigger = conditionConfiguration.configuration.trigger; - this.conditionManager.on('broadcastTelemetry', this.handleBroadcastTelemetry, this); } - handleBroadcastTelemetry(datum) { + getResult(datum) { if (!datum || !datum.id) { console.log('no data received'); return; } + if (!this.isTelemetryUsed(datum.id)) { + return; + } this.criteria.forEach(criterion => { - if (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any')) { - criterion.handleSubscription(datum, this.conditionManager.telemetryObjects); + if (this.isAnyOrAllTelemetry(criterion)) { + criterion.getResult(datum, this.conditionManager.telemetryObjects); } else { - criterion.emit(`subscription:${datum.id}`, datum); + criterion.getResult(datum); } }); + + this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger); + } + + isAnyOrAllTelemetry(criterion) { + return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any')); + } + + isTelemetryUsed(id) { + return this.criteria.some(criterion => { + return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id; + }); } update(conditionConfiguration) { @@ -90,7 +101,6 @@ export default class ConditionClass extends EventEmitter { updateTrigger(trigger) { if (this.trigger !== trigger) { this.trigger = trigger; - this.handleConditionUpdated(); } } @@ -134,7 +144,6 @@ export default class ConditionClass extends EventEmitter { criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct); } criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); - criterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj)); if (!this.criteria) { this.criteria = []; } @@ -163,20 +172,11 @@ export default class ConditionClass extends EventEmitter { const newCriterionConfiguration = this.generateCriterion(criterionConfiguration); let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct); newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); - newCriterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj)); let criterion = found.item; criterion.unsubscribe(); criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); - criterion.off('criterionResultUpdated', (obj) => this.handleCriterionResult(obj)); this.criteria.splice(found.index, 1, newCriterion); - delete this.criteriaResults[criterion.id]; - } - } - - removeCriterion(id) { - if (this.destroyCriterion(id)) { - this.handleConditionUpdated(); } } @@ -189,7 +189,6 @@ export default class ConditionClass extends EventEmitter { }); criterion.destroy(); this.criteria.splice(found.index, 1); - delete this.criteriaResults[criterion.id]; return true; } @@ -200,27 +199,9 @@ export default class ConditionClass extends EventEmitter { let found = this.findCriterion(criterion.id); if (found) { this.criteria[found.index] = criterion.data; - // TODO nothing is listening to this - this.emitEvent('conditionUpdated', { - trigger: this.trigger, - criteria: this.criteria - }); } } - updateCriteriaResults(eventData) { - const id = eventData.id; - - if (this.findCriterion(id)) { - this.criteriaResults[id] = !!eventData.data.result; - } - } - - handleCriterionResult(eventData) { - this.updateCriteriaResults(eventData); - this.handleConditionUpdated(eventData.data); - } - requestLADConditionResult() { let latestTimestamp; let criteriaResults = {}; @@ -237,28 +218,21 @@ export default class ConditionClass extends EventEmitter { latestTimestamp = getLatestTimestamp( latestTimestamp, data, - this.timeSystems + this.timeSystems, + this.openmct.time.timeSystem() ); }); return { id: this.id, - data: Object.assign({}, latestTimestamp, { result: this.evaluate(criteriaResults) }) - } + data: Object.assign( + {}, + latestTimestamp, + { result: evaluateResults(Object.values(criteriaResults), this.trigger) } + ) + }; }); } - getTelemetrySubscriptions() { - return this.criteria.map(criterion => criterion.telemetryObjectIdAsString); - } - - handleConditionUpdated(datum) { - // trigger an updated event so that consumers can react accordingly - this.result = this.evaluate(this.criteriaResults); - this.emitEvent('conditionResultUpdated', - Object.assign({}, datum, { result: this.result }) - ); - } - getCriteria() { return this.criteria; } @@ -272,28 +246,7 @@ export default class ConditionClass extends EventEmitter { return success; } - evaluate(results) { - if (this.trigger && this.trigger === TRIGGER.XOR) { - return computeConditionByLimit(results, 1); - } else if (this.trigger && this.trigger === TRIGGER.NOT) { - return computeConditionByLimit(results, 0); - } else { - return computeCondition(results, this.trigger === TRIGGER.ALL); - } - } - - emitEvent(eventName, data) { - this.emit(eventName, { - id: this.id, - data: data - }); - } - destroy() { - this.conditionManager.off('broadcastTelemetry', this.handleBroadcastTelemetry, this); - if (typeof this.stopObservingForChanges === 'function') { - this.stopObservingForChanges(); - } this.destroyCriteria(); } } diff --git a/src/plugins/condition/ConditionManager.js b/src/plugins/condition/ConditionManager.js index eb0d833c8a..65647b1d14 100644 --- a/src/plugins/condition/ConditionManager.js +++ b/src/plugins/condition/ConditionManager.js @@ -30,7 +30,6 @@ export default class ConditionManager extends EventEmitter { super(); this.openmct = openmct; this.conditionSetDomainObject = conditionSetDomainObject; - this.timeAPI = this.openmct.time; this.timeSystems = this.openmct.time.getAllTimeSystems(); this.composition = this.openmct.composition.get(conditionSetDomainObject); this.composition.on('add', this.subscribeToTelemetry, this); @@ -56,8 +55,9 @@ export default class ConditionManager extends EventEmitter { this.telemetryObjects[id] = Object.assign({}, endpoint, {telemetryMetaData: this.openmct.telemetry.getMetadata(endpoint).valueMetadatas}); this.subscriptions[id] = this.openmct.telemetry.subscribe( endpoint, - this.broadcastTelemetry.bind(this, id) + this.telemetryReceived.bind(this, id) ); + // TODO check if this is needed this.updateConditionTelemetry(); } @@ -74,7 +74,6 @@ export default class ConditionManager extends EventEmitter { } initialize() { - this.conditionResults = {}; this.conditionClassCollection = []; if (this.conditionSetDomainObject.configuration.conditionCollection.length) { this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => { @@ -96,7 +95,6 @@ export default class ConditionManager extends EventEmitter { initCondition(conditionConfiguration, index) { let condition = new Condition(conditionConfiguration, this.openmct, this); - condition.on('conditionResultUpdated', this.handleConditionResult.bind(this)); if (index !== undefined) { this.conditionClassCollection.splice(index + 1, 0, condition); } else { @@ -160,13 +158,10 @@ export default class ConditionManager extends EventEmitter { removeCondition(index) { let condition = this.conditionClassCollection[index]; - condition.destroyCriteria(); - condition.off('conditionResultUpdated', this.handleConditionResult.bind(this)); + condition.destroy(); this.conditionClassCollection.splice(index, 1); this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1); - delete this.conditionResults[condition.id]; this.persistConditions(); - this.handleConditionResult(); } findConditionById(id) { @@ -184,7 +179,23 @@ export default class ConditionManager extends EventEmitter { this.persistConditions(); } - getCurrentCondition(conditionResults) { + getCurrentCondition() { + const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection; + let currentCondition = conditionCollection[conditionCollection.length-1]; + + for (let i = 0; i < conditionCollection.length - 1; i++) { + const condition = this.findConditionById(conditionCollection[i].id) + if (condition.result) { + //first condition to be true wins + currentCondition = conditionCollection[i]; + break; + } + } + + return currentCondition; + } + + getCurrentConditionLAD(conditionResults) { const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection; let currentCondition = conditionCollection[conditionCollection.length-1]; @@ -198,36 +209,6 @@ export default class ConditionManager extends EventEmitter { return currentCondition; } - updateConditionResults(resultObj) { - if (!resultObj) { - return; - } - - const id = resultObj.id; - - if (this.findConditionById(id)) { - this.conditionResults[id] = resultObj.data.result; - } - } - - handleConditionResult(resultObj) { - this.updateConditionResults(resultObj); - const currentCondition = this.getCurrentCondition(this.conditionResults); - const timestamp = JSON.parse(JSON.stringify(resultObj.data)) - delete timestamp.result - - this.emit('conditionSetResultUpdated', - Object.assign( - { - output: currentCondition.configuration.output, - id: this.conditionSetDomainObject.identifier, - conditionId: currentCondition.id - }, - timestamp - ) - ) - } - requestLADConditionSetOutput() { if (!this.conditionClassCollection.length) { return Promise.resolve([]); @@ -249,10 +230,11 @@ export default class ConditionManager extends EventEmitter { latestTimestamp = getLatestTimestamp( latestTimestamp, data, - this.timeSystems + this.timeSystems, + this.openmct.time.timeSystem() ); }); - const currentCondition = this.getCurrentCondition(conditionResults); + const currentCondition = this.getCurrentConditionLAD(conditionResults); return Object.assign( { @@ -266,8 +248,28 @@ export default class ConditionManager extends EventEmitter { }); } - broadcastTelemetry(id, datum) { - this.emit(`broadcastTelemetry`, Object.assign({}, this.createNormalizedDatum(datum, id), {id: id})); + telemetryReceived(id, datum) { + const normalizedDatum = this.createNormalizedDatum(datum, id); + const timeSystemKey = this.openmct.time.timeSystem().key; + let timestamp = {}; + timestamp[timeSystemKey] = normalizedDatum[timeSystemKey]; + + this.conditionClassCollection.forEach(condition => { + condition.getResult(normalizedDatum); + }); + + const currentCondition = this.getCurrentCondition(); + + this.emit('conditionSetResultUpdated', + Object.assign( + { + output: currentCondition.configuration.output, + id: this.conditionSetDomainObject.identifier, + conditionId: currentCondition.id + }, + timestamp + ) + ) } getTestData(metadatum) { @@ -282,12 +284,16 @@ export default class ConditionManager extends EventEmitter { } createNormalizedDatum(telemetryDatum, id) { - return Object.values(this.telemetryObjects[id].telemetryMetaData).reduce((normalizedDatum, metadatum) => { + const normalizedDatum = Object.values(this.telemetryObjects[id].telemetryMetaData).reduce((datum, metadatum) => { const testValue = this.getTestData(metadatum); const formatter = this.openmct.telemetry.getValueFormatter(metadatum); - normalizedDatum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]); - return normalizedDatum; + datum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]); + return datum; }, {}); + + normalizedDatum.id = id; + + return normalizedDatum; } updateTestData(testData) { @@ -310,7 +316,6 @@ export default class ConditionManager extends EventEmitter { } this.conditionClassCollection.forEach((condition) => { - condition.off('conditionResultUpdated', this.handleConditionResult); condition.destroy(); }) } diff --git a/src/plugins/condition/ConditionManagerSpec.js b/src/plugins/condition/ConditionManagerSpec.js index 869bdb6cb6..2f9a6c3499 100644 --- a/src/plugins/condition/ConditionManagerSpec.js +++ b/src/plugins/condition/ConditionManagerSpec.js @@ -121,7 +121,7 @@ describe('ConditionManager', () => { conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); conditionMgr.on('conditionSetResultUpdated', mockListener); - conditionMgr.on('broadcastTelemetry', mockListener); + conditionMgr.on('telemetryReceived', mockListener); }); it('creates a conditionCollection with a default condition', function () { diff --git a/src/plugins/condition/ConditionSpec.js b/src/plugins/condition/ConditionSpec.js index 79cbdab220..4d445835be 100644 --- a/src/plugins/condition/ConditionSpec.js +++ b/src/plugins/condition/ConditionSpec.js @@ -25,12 +25,11 @@ import {TRIGGER} from "./utils/constants"; import TelemetryCriterion from "./criterion/TelemetryCriterion"; let openmct = {}, - mockListener, testConditionDefinition, testTelemetryObject, conditionObj, conditionManager, - mockBroadcastTelemetry, + mockTelemetryReceived, mockTimeSystems; describe("The condition", function () { @@ -39,10 +38,9 @@ describe("The condition", function () { conditionManager = jasmine.createSpyObj('conditionManager', ['on'] ); - mockBroadcastTelemetry = jasmine.createSpy('listener'); - conditionManager.on('broadcastTelemetry', mockBroadcastTelemetry); + mockTelemetryReceived = jasmine.createSpy('listener'); + conditionManager.on('telemetryReceived', mockTelemetryReceived); - mockListener = jasmine.createSpy('listener'); testTelemetryObject = { identifier:{ namespace: "", key: "test-object"}, type: "test-object", @@ -104,8 +102,6 @@ describe("The condition", function () { openmct, conditionManager ); - - conditionObj.on('conditionUpdated', mockListener); }); it("generates criteria with the correct properties", function () { diff --git a/src/plugins/condition/criterion/AllTelemetryCriterion.js b/src/plugins/condition/criterion/AllTelemetryCriterion.js index bceb21c0dd..7faa6bbff8 100644 --- a/src/plugins/condition/criterion/AllTelemetryCriterion.js +++ b/src/plugins/condition/criterion/AllTelemetryCriterion.js @@ -20,11 +20,10 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import EventEmitter from 'EventEmitter'; -import {OPERATIONS} from '../utils/operations'; -import {computeCondition} from "@/plugins/condition/utils/evaluator"; +import TelemetryCriterion from './TelemetryCriterion'; +import { evaluateResults } from "../utils/evaluator"; -export default class TelemetryCriterion extends EventEmitter { +export default class AllTelemetryCriterion extends TelemetryCriterion { /** * Subscribes/Unsubscribes to telemetry and emits the result @@ -34,23 +33,20 @@ export default class TelemetryCriterion extends EventEmitter { * @param openmct */ constructor(telemetryDomainObjectDefinition, openmct) { - super(); + super(telemetryDomainObjectDefinition, openmct); + } - this.openmct = openmct; - this.objectAPI = this.openmct.objects; - this.telemetryAPI = this.openmct.telemetry; - this.timeAPI = this.openmct.time; - this.id = telemetryDomainObjectDefinition.id; - this.telemetry = telemetryDomainObjectDefinition.telemetry; - this.operation = telemetryDomainObjectDefinition.operation; - this.telemetryObjects = Object.assign({}, telemetryDomainObjectDefinition.telemetryObjects); - this.input = telemetryDomainObjectDefinition.input; - this.metadata = telemetryDomainObjectDefinition.metadata; + initialize() { + this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects }; this.telemetryDataCache = {}; } + isValid() { + return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation; + } + updateTelemetry(telemetryObjects) { - this.telemetryObjects = Object.assign({}, telemetryObjects); + this.telemetryObjects = { ...telemetryObjects }; } formatData(data, telemetryObjects) { @@ -68,60 +64,32 @@ export default class TelemetryCriterion extends EventEmitter { }); const datum = { - result: computeCondition(this.telemetryDataCache, this.telemetry === 'all') + result: evaluateResults(Object.values(this.telemetryDataCache), this.telemetry) }; if (data) { - // TODO check back to see if we should format times here - this.timeAPI.getAllTimeSystems().forEach(timeSystem => { + this.openmct.time.getAllTimeSystems().forEach(timeSystem => { datum[timeSystem.key] = data[timeSystem.key] }); } return datum; } - handleSubscription(data, telemetryObjects) { - if(this.isValid()) { - this.emitEvent('criterionResultUpdated', this.formatData(data, telemetryObjects)); - } else { - this.emitEvent('criterionResultUpdated', this.formatData({}, telemetryObjects)); - } - } + getResult(data, telemetryObjects) { + const validatedData = this.isValid() ? data : {}; - findOperation(operation) { - for (let i=0; i < OPERATIONS.length; i++) { - if (operation === OPERATIONS[i].name) { - return OPERATIONS[i].operation; - } + if (validatedData) { + this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData); } - return null; - } - computeResult(data) { - let result = false; - if (data) { - let comparator = this.findOperation(this.operation); - let params = []; - params.push(data[this.metadata]); - if (this.input instanceof Array && this.input.length) { - this.input.forEach(input => params.push(input)); + Object.values(telemetryObjects).forEach(telemetryObject => { + const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); + if (this.telemetryDataCache[id] === undefined) { + this.telemetryDataCache[id] = false; } - if (typeof comparator === 'function') { - result = comparator(params); - } - } - return result; - } - - emitEvent(eventName, data) { - this.emit(eventName, { - id: this.id, - data: data }); - } - isValid() { - return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation; + this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry); } requestLAD(options) { @@ -139,7 +107,7 @@ export default class TelemetryCriterion extends EventEmitter { let keys = Object.keys(Object.assign({}, options.telemetryObjects)); const telemetryRequests = keys - .map(key => this.telemetryAPI.request( + .map(key => this.openmct.telemetry.request( options.telemetryObjects[key], options )); @@ -163,10 +131,7 @@ export default class TelemetryCriterion extends EventEmitter { } destroy() { - this.emitEvent('criterionRemoved'); delete this.telemetryObjects; delete this.telemetryDataCache; - delete this.telemetryObjectIdAsString; - delete this.telemetryObject; } } diff --git a/src/plugins/condition/criterion/TelemetryCriterion.js b/src/plugins/condition/criterion/TelemetryCriterion.js index 998e27431c..016373712f 100644 --- a/src/plugins/condition/criterion/TelemetryCriterion.js +++ b/src/plugins/condition/criterion/TelemetryCriterion.js @@ -21,7 +21,7 @@ *****************************************************************************/ import EventEmitter from 'EventEmitter'; -import {OPERATIONS} from '../utils/operations'; +import { OPERATIONS } from '../utils/operations'; export default class TelemetryCriterion extends EventEmitter { @@ -36,20 +36,27 @@ export default class TelemetryCriterion extends EventEmitter { super(); this.openmct = openmct; - this.objectAPI = this.openmct.objects; - this.telemetryAPI = this.openmct.telemetry; - this.timeAPI = this.openmct.time; + this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition; this.id = telemetryDomainObjectDefinition.id; this.telemetry = telemetryDomainObjectDefinition.telemetry; this.operation = telemetryDomainObjectDefinition.operation; this.input = telemetryDomainObjectDefinition.input; this.metadata = telemetryDomainObjectDefinition.metadata; - this.telemetryObject = telemetryDomainObjectDefinition.telemetryObject; - this.telemetryObjectIdAsString = this.objectAPI.makeKeyString(telemetryDomainObjectDefinition.telemetry); - this.on(`subscription:${this.telemetryObjectIdAsString}`, this.handleSubscription); + this.result = undefined; + + this.initialize(); this.emitEvent('criterionUpdated', this); } + initialize() { + this.telemetryObject = this.telemetryDomainObjectDefinition.telemetryObject; + this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry); + } + + isValid() { + return this.telemetryObject && this.metadata && this.operation; + } + updateTelemetry(telemetryObjects) { this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString]; } @@ -60,20 +67,44 @@ export default class TelemetryCriterion extends EventEmitter { }; if (data) { - // TODO check back to see if we should format times here - this.timeAPI.getAllTimeSystems().forEach(timeSystem => { + this.openmct.time.getAllTimeSystems().forEach(timeSystem => { datum[timeSystem.key] = data[timeSystem.key] }); } return datum; } - handleSubscription(data) { - if(this.isValid()) { - this.emitEvent('criterionResultUpdated', this.formatData(data)); - } else { - this.emitEvent('criterionResultUpdated', this.formatData({})); + getResult(data) { + const validatedData = this.isValid() ? data : {}; + this.result = this.computeResult(validatedData); + } + + requestLAD(options) { + options = Object.assign({}, + options, + { + strategy: 'latest', + size: 1 + } + ); + + if (!this.isValid()) { + return { + id: this.id, + data: this.formatData({}) + }; } + + return this.openmct.telemetry.request( + this.telemetryObject, + options + ).then(results => { + const latestDatum = results.length ? results[results.length - 1] : {}; + return { + id: this.id, + data: this.formatData(latestDatum) + }; + }); } findOperation(operation) { @@ -95,7 +126,7 @@ export default class TelemetryCriterion extends EventEmitter { this.input.forEach(input => params.push(input)); } if (typeof comparator === 'function') { - result = comparator(params); + result = !!comparator(params); } } return result; @@ -108,42 +139,9 @@ export default class TelemetryCriterion extends EventEmitter { }); } - isValid() { - return this.telemetryObject && this.metadata && this.operation; - } - - requestLAD(options) { - options = Object.assign({}, - options, - { - strategy: 'latest', - size: 1 - } - ); - - if (!this.isValid()) { - return { - id: this.id, - data: this.formatData({}) - }; - } - - return this.telemetryAPI.request( - this.telemetryObject, - options - ).then(results => { - const latestDatum = results.length ? results[results.length - 1] : {}; - return { - id: this.id, - data: this.formatData(latestDatum) - }; - }); - } destroy() { - this.off(`subscription:${this.telemetryObjectIdAsString}`, this.handleSubscription); - this.emitEvent('criterionRemoved'); - delete this.telemetryObjectIdAsString; delete this.telemetryObject; + delete this.telemetryObjectIdAsString; } } diff --git a/src/plugins/condition/criterion/TelemetryCriterionSpec.js b/src/plugins/condition/criterion/TelemetryCriterionSpec.js index 67c08c1165..a5ff94c7e0 100644 --- a/src/plugins/condition/criterion/TelemetryCriterionSpec.js +++ b/src/plugins/condition/criterion/TelemetryCriterionSpec.js @@ -54,7 +54,7 @@ describe("The telemetry criterion", function () { key: "testSource", source: "value", name: "Test", - format: "enum" + format: "string" }] } }; @@ -80,8 +80,9 @@ describe("The telemetry criterion", function () { testCriterionDefinition = { id: 'test-criterion-id', telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier), - operation: 'lessThan', - metadata: 'sin', + operation: 'textContains', + metadata: 'value', + input: ['Hell'], telemetryObject: testTelemetryObject }; @@ -100,12 +101,21 @@ describe("The telemetry criterion", function () { expect(telemetryCriterion.telemetryObjectIdAsString).toEqual(testTelemetryObject.identifier.key); }); - it("updates and emits event on new data from telemetry providers", function () { - spyOn(telemetryCriterion, 'emitEvent').and.callThrough(); - telemetryCriterion.handleSubscription({ + it("returns a result on new data from relevant telemetry providers", function () { + telemetryCriterion.getResult({ value: 'Hello', - utc: 'Hi' + utc: 'Hi', + id: testTelemetryObject.identifier.key }); - expect(telemetryCriterion.emitEvent).toHaveBeenCalled(); + expect(telemetryCriterion.result).toBeTrue(); }); + + // it("does not return a result on new data from irrelavant telemetry providers", function () { + // telemetryCriterion.getResult({ + // value: 'Hello', + // utc: 'Hi', + // id: '1234' + // }); + // expect(telemetryCriterion.result).toBeFalse(); + // }); }); diff --git a/src/plugins/condition/utils/evaluator.js b/src/plugins/condition/utils/evaluator.js index 7e2b70f7e2..1b1ecb71a5 100644 --- a/src/plugins/condition/utils/evaluator.js +++ b/src/plugins/condition/utils/evaluator.js @@ -19,36 +19,50 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +import { TRIGGER } from "./constants"; -export const computeCondition = (resultMap, allMustBeTrue) => { - let result = false; - for (let key in resultMap) { - if (resultMap.hasOwnProperty(key)) { - result = resultMap[key]; - if (allMustBeTrue && !result) { - //If we want all conditions to be true, then even one negative result should break. - break; - } else if (!allMustBeTrue && result) { - //If we want at least one condition to be true, then even one positive result should break. - break; - } +export const evaluateResults = (results, trigger) => { + if (trigger && trigger === TRIGGER.XOR) { + return matchExact(results, 1); + } else if (trigger && trigger === TRIGGER.NOT) { + return matchExact(results, 0); + } else if (trigger && trigger === TRIGGER.ALL) { + return matchAll(results); + } else { + return matchAny(results); + } +} + +function matchAll(results) { + for (const result of results) { + if (!result) { + return false; } } - return result; -}; -//Returns true only if limit number of results are satisfied -export const computeConditionByLimit = (resultMap, limit) => { - let trueCount = 0; - for (let key in resultMap) { - if (resultMap.hasOwnProperty(key)) { - if (resultMap[key]) { - trueCount++; - } - if (trueCount > limit) { - break; - } + return true; +} + +function matchAny(results) { + for (const result of results) { + if (result) { + return true; } } - return trueCount === limit; -}; + + return false; +} + +function matchExact(results, target) { + let matches = 0; + for (const result of results) { + if (result) { + matches++; + } + if (matches > target) { + return false; + } + } + + return matches === target; +} diff --git a/src/plugins/condition/utils/evaluatorSpec.js b/src/plugins/condition/utils/evaluatorSpec.js index 132f77f181..04152480a6 100644 --- a/src/plugins/condition/utils/evaluatorSpec.js +++ b/src/plugins/condition/utils/evaluatorSpec.js @@ -20,47 +20,185 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { computeConditionByLimit } from "./evaluator"; +import { evaluateResults } from './evaluator'; +import { TRIGGER } from './constants'; -describe('evaluate results based on trigger', function () { +describe('evaluate results', () => { + // const allTrue = [true, true, true, true, true]; + // const oneTrue = [false, false, false, false, true]; + // const multipleTrue = [false, true, false, true, false]; + // const noneTrue = [false, false, false, false, false]; + // const allTrueWithUndefined = [true, true, true, undefined, true]; + // const oneTrueWithUndefined = [undefined, undefined, undefined, undefined, true]; + // const multipleTrueWithUndefined = [true, undefined, true, undefined, true]; + // const allUndefined = [undefined, undefined, undefined, undefined, undefined]; + // const singleTrue = [true]; + // const singleFalse = [false]; + // const singleUndefined = [undefined]; + // const empty = []; - it('should evaluate to true if trigger is NOT', () => { - const results = { - result: false, - result1: false, - result2: false - }; - const result = computeConditionByLimit(results, 0); - expect(result).toBeTrue(); + const tests = [ + { + name: 'allTrue', + values: [true, true, true, true, true], + any: true, + all: true, + not: false, + xor: false + }, { + name: 'oneTrue', + values: [false, false, false, false, true], + any: true, + all: false, + not: false, + xor: true + }, { + name: 'multipleTrue', + values: [false, true, false, true, false], + any: true, + all: false, + not: false, + xor: false + }, { + name: 'noneTrue', + values: [false, false, false, false, false], + any: false, + all: false, + not: true, + xor: false + }, { + name: 'allTrueWithUndefined', + values: [true, true, true, undefined, true], + any: true, + all: false, + not: false, + xor: false + }, { + name: 'oneTrueWithUndefined', + values: [undefined, undefined, undefined, undefined, true], + any: true, + all: false, + not: false, + xor: true + }, { + name: 'multipleTrueWithUndefined', + values: [true, undefined, true, undefined, true], + any: true, + all: false, + not: false, + xor: false + }, { + name: 'allUndefined', + values: [undefined, undefined, undefined, undefined, undefined], + any: false, + all: false, + not: true, + xor: false + }, { + name: 'singleTrue', + values: [true], + any: true, + all: true, + not: false, + xor: true + }, { + name: 'singleFalse', + values: [false], + any: false, + all: false, + not: true, + xor: false + }, { + name: 'singleUndefined', + values: [undefined], + any: false, + all: false, + not: true, + xor: false + } + // , { + // name: 'empty', + // values: [], + // any: false, + // all: false, + // not: true, + // xor: false + // } + ]; + + describe(`based on trigger ${TRIGGER.ANY}`, () => { + it('should evaluate to expected result', () => { + tests.forEach(test => { + const result = evaluateResults(test.values, TRIGGER.ANY); + expect(result).toEqual(test[TRIGGER.ANY]) + }); + }); }); - it('should evaluate to false if trigger is NOT', () => { - const results = { - result: true, - result1: false, - result2: false - }; - const result = computeConditionByLimit(results, 0); - expect(result).toBeFalse(); + describe(`based on trigger ${TRIGGER.ALL}`, () => { + it('should evaluate to expected result', () => { + tests.forEach(test => { + const result = evaluateResults(test.values, TRIGGER.ALL); + expect(result).toEqual(test[TRIGGER.ALL]) + }); + }); }); - it('should evaluate to true if trigger is XOR', () => { - const results = { - result: false, - result1: true, - result2: false - }; - const result = computeConditionByLimit(results, 1); - expect(result).toBeTrue(); + describe(`based on trigger ${TRIGGER.NOT}`, () => { + it('should evaluate to expected result', () => { + tests.forEach(test => { + const result = evaluateResults(test.values, TRIGGER.NOT); + expect(result).toEqual(test[TRIGGER.NOT]) + }); + }); }); - it('should evaluate to false if trigger is XOR', () => { - const results = { - result: false, - result1: true, - result2: true - }; - const result = computeConditionByLimit(results, 1); - expect(result).toBeFalse(); + describe(`based on trigger ${TRIGGER.XOR}`, () => { + it('should evaluate to expected result', () => { + tests.forEach(test => { + const result = evaluateResults(test.values, TRIGGER.XOR); + expect(result).toEqual(test[TRIGGER.XOR]) + }); + }); }); + + // it('should evaluate to true if trigger is NOT', () => { + // const results = { + // result: false, + // result1: false, + // result2: false + // }; + // const result = computeConditionByLimit(results, 0); + // expect(result).toBeTrue(); + // }); + + // it('should evaluate to false if trigger is NOT', () => { + // const results = { + // result: true, + // result1: false, + // result2: false + // }; + // const result = computeConditionByLimit(results, 0); + // expect(result).toBeFalse(); + // }); + + // it('should evaluate to true if trigger is XOR', () => { + // const results = { + // result: false, + // result1: true, + // result2: false + // }; + // const result = computeConditionByLimit(results, 1); + // expect(result).toBeTrue(); + // }); + + // it('should evaluate to false if trigger is XOR', () => { + // const results = { + // result: false, + // result1: true, + // result2: true + // }; + // const result = computeConditionByLimit(results, 1); + // expect(result).toBeFalse(); + // }); }); diff --git a/src/plugins/condition/utils/time.js b/src/plugins/condition/utils/time.js index bc60923487..87d2522d04 100644 --- a/src/plugins/condition/utils/time.js +++ b/src/plugins/condition/utils/time.js @@ -20,15 +20,33 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -export const getLatestTimestamp = (current, compare, timeSystems) => { - const timestamp = Object.assign({}, current); +export const getLatestTimestamp = ( + currentTimestamp, + compareTimestamp, + timeSystems, + currentTimeSystem +) => { + let latest = { ...currentTimestamp }; + const compare = { ...compareTimestamp }; + const key = currentTimeSystem.key; + + if (!latest || !latest[key]) { + latest = updateLatestTimeStamp(compare, timeSystems) + } + + if (compare[key] > latest[key]) { + latest = updateLatestTimeStamp(compare, timeSystems) + } + + return latest; +} + +function updateLatestTimeStamp(timestamp, timeSystems) { + let latest = {}; timeSystems.forEach(timeSystem => { - if (!timestamp[timeSystem.key] - || compare[timeSystem.key] > timestamp[timeSystem.key] - ) { - timestamp[timeSystem.key] = compare[timeSystem.key]; - } + latest[timeSystem.key] = timestamp[timeSystem.key]; }); - return timestamp; + + return latest; }