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 <david.e.tsay@nasa.gov>
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
Shefali Joshi 2020-04-10 15:57:38 -07:00 committed by GitHub
parent 11f2c35bb2
commit 26838635b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 412 additions and 315 deletions

View File

@ -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,28 +56,40 @@ 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;
}
this.criteria.forEach(criterion => {
if (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any')) {
criterion.handleSubscription(datum, this.conditionManager.telemetryObjects);
} else {
criterion.emit(`subscription:${datum.id}`, datum);
if (!this.isTelemetryUsed(datum.id)) {
return;
}
this.criteria.forEach(criterion => {
if (this.isAnyOrAllTelemetry(criterion)) {
criterion.getResult(datum, this.conditionManager.telemetryObjects);
} else {
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;
});
}
@ -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();
}
}

View File

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

View File

@ -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 () {

View File

@ -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 () {

View File

@ -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 : {};
if (validatedData) {
this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
}
findOperation(operation) {
for (let i=0; i < OPERATIONS.length; i++) {
if (operation === OPERATIONS[i].name) {
return OPERATIONS[i].operation;
Object.values(telemetryObjects).forEach(telemetryObject => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (this.telemetryDataCache[id] === undefined) {
this.telemetryDataCache[id] = false;
}
}
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));
}
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;
}
}

View File

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

View File

@ -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();
// });
});

View File

@ -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);
}
}
}
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;
function matchAll(results) {
for (const result of results) {
if (!result) {
return false;
}
}
return true;
}
function matchAny(results) {
for (const result of results) {
if (result) {
return true;
}
}
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;
}
return trueCount === limit;
};

View File

@ -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
// }
];
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();
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])
});
});
});
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])
});
});
});
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])
});
});
});
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();
// });
});

View File

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