@machinemetrics/io-adapter-lib 2.35.1 → 2.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.36.0]
4
+ - Added if / else-if / else branch transform
5
+
3
6
  ## [2.35.1]
4
7
  - Fixed value-increase and value-decrease to not emit true when coming back from UNAVAILABLE
5
8
  - Behavior change: value-decrease does not emit true on start
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+ const ConfigError = require('./configError');
5
+
6
+ const BRANCH_KEYS = ['if', 'else-if', 'else'];
7
+
8
+ function isBranchKey(key) {
9
+ return BRANCH_KEYS.includes(key);
10
+ }
11
+
12
+ function getBranchKey(item) {
13
+ if (!_.isObject(item) || _.isArray(item)) {
14
+ return null;
15
+ }
16
+ const keys = _.keys(item);
17
+ if (keys.length !== 1) {
18
+ return null;
19
+ }
20
+ const key = keys[0];
21
+ return isBranchKey(key) ? key : null;
22
+ }
23
+
24
+ /**
25
+ * Preprocesses a variable's transform list to collapse consecutive if/else-if/else
26
+ * into a single branch transform. The user-facing YAML syntax allows:
27
+ *
28
+ * - if:
29
+ * - condition: this == 1
30
+ * - op1
31
+ * - else-if:
32
+ * - condition: this == 2
33
+ * - op1
34
+ * - else:
35
+ * - op1
36
+ *
37
+ * This gets rewritten to:
38
+ *
39
+ * - branch:
40
+ * - if: [...]
41
+ * - else-if: [...]
42
+ * - else: [...]
43
+ */
44
+ function preprocessBranchTransforms(transformList, sectionPath) {
45
+ if (!_.isArray(transformList)) {
46
+ return transformList;
47
+ }
48
+
49
+ const result = [];
50
+ let i = 0;
51
+
52
+ while (i < transformList.length) {
53
+ const key = getBranchKey(transformList[i]);
54
+
55
+ if (key !== 'if') {
56
+ result.push(transformList[i]);
57
+ i += 1;
58
+ } else {
59
+ const branchComponents = [];
60
+ let seenElse = false;
61
+
62
+ while (i < transformList.length) {
63
+ const currentKey = getBranchKey(transformList[i]);
64
+
65
+ if (currentKey === null) {
66
+ break;
67
+ }
68
+
69
+ if (currentKey === 'if') {
70
+ if (branchComponents.length > 0) {
71
+ break;
72
+ }
73
+ const item = transformList[i];
74
+ const body = item[currentKey];
75
+ const condition = body[0];
76
+ const chainTransforms = preprocessBranchTransforms(body.slice(1), `${sectionPath}.${i}.if`);
77
+ branchComponents.push({ [currentKey]: [condition, ...chainTransforms] });
78
+ i += 1;
79
+ } else if (currentKey === 'else-if') {
80
+ if (branchComponents.length === 0) {
81
+ throw new ConfigError('else-if must follow if or else-if')
82
+ .atPath(sectionPath);
83
+ }
84
+ if (seenElse) {
85
+ throw new ConfigError('else-if cannot follow else')
86
+ .atPath(sectionPath);
87
+ }
88
+ const item = transformList[i];
89
+ const body = item[currentKey];
90
+ const condition = body[0];
91
+ const chainTransforms = preprocessBranchTransforms(body.slice(1), `${sectionPath}.${i}.else-if`);
92
+ branchComponents.push({ [currentKey]: [condition, ...chainTransforms] });
93
+ i += 1;
94
+ } else if (currentKey === 'else') {
95
+ if (branchComponents.length === 0) {
96
+ throw new ConfigError('else must follow if or else-if')
97
+ .atPath(sectionPath);
98
+ }
99
+ if (seenElse) {
100
+ throw new ConfigError('only one else allowed per branch')
101
+ .atPath(sectionPath);
102
+ }
103
+ seenElse = true;
104
+ const item = transformList[i];
105
+ const body = item[currentKey];
106
+ const chainTransforms = preprocessBranchTransforms(body, `${sectionPath}.${i}.else`);
107
+ branchComponents.push({ [currentKey]: chainTransforms });
108
+ i += 1;
109
+ } else {
110
+ break;
111
+ }
112
+ }
113
+
114
+ result.push({ branch: branchComponents });
115
+ }
116
+ }
117
+
118
+ return result;
119
+ }
120
+
121
+ module.exports = {
122
+ preprocessBranchTransforms,
123
+ };
@@ -3,6 +3,7 @@
3
3
  const _ = require('lodash');
4
4
  const ConfigError = require('./configError');
5
5
  const TransformConfigUtil = require('./transformConfigUtil');
6
+ const { preprocessBranchTransforms } = require('./branchPreprocessor');
6
7
 
7
8
  class EngineConfig {
8
9
  constructor(expressionService, config = {}, device = {}) {
@@ -48,6 +49,10 @@ class EngineConfig {
48
49
  return variable;
49
50
  });
50
51
 
52
+ this.config.variables = _.mapValues(this.config.variables, (variable, name) => {
53
+ return preprocessBranchTransforms(variable, `variables.${name}`);
54
+ });
55
+
51
56
  _.each(this.getExpressions(this.config), name => expressionService.addExpression(name));
52
57
  _.each(this.getStringExpressions(this.config), name => expressionService.addStringExpression(name));
53
58
  }
@@ -259,7 +259,7 @@ class ExpressionService {
259
259
  if (tokens) {
260
260
  const first = tokens[0];
261
261
  if (first === 'this' || this.channelsLookup[first]) {
262
- accum[first] = _.reduceRight(_.tail(tokens), (sub, item) => {
262
+ const nested = _.reduceRight(_.tail(tokens), (sub, item) => {
263
263
  if (_.isNumber(item)) {
264
264
  return [
265
265
  ..._.times(Math.max(0, item - 1), () => 0),
@@ -268,6 +268,11 @@ class ExpressionService {
268
268
  }
269
269
  return { [item]: sub };
270
270
  }, 0);
271
+ if (_.isArray(nested) || _.isArray(accum[first])) {
272
+ accum[first] = nested;
273
+ } else {
274
+ accum[first] = _.merge(accum[first] || {}, nested);
275
+ }
271
276
  } else {
272
277
  accum[first] = 0;
273
278
  }
@@ -0,0 +1,274 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+ const TransformState = require('./transformState');
5
+
6
+ class BranchChain {
7
+ constructor(chain) {
8
+ this.chain = chain;
9
+ this.opResult = null;
10
+
11
+ const end = this.chain.chainEnd();
12
+ end.on('update', (value) => {
13
+ this.opResult = value;
14
+ });
15
+ end.on('unavailable', () => {
16
+ this.opResult = null;
17
+ });
18
+ }
19
+
20
+ update(context, value, time) {
21
+ this.chain.update(context, value, time);
22
+ return this.opResult;
23
+ }
24
+
25
+ probe(context, time) {
26
+ this.chain.probe(context, time);
27
+ }
28
+
29
+ probeOrForward(context, time) {
30
+ this.chain.probeOrForward(context, time);
31
+ }
32
+
33
+ setUnavailable(context, time) {
34
+ this.chain.setUnavailable(context, time);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Conditional branching transform. Evaluates conditions in sequence and runs the first
40
+ * matching branch's transform chain. Analogous to if/else-if/else.
41
+ *
42
+ * Config format (after preprocessing):
43
+ * - branch:
44
+ * - if:
45
+ * - condition: this == 1 or x > y
46
+ * - op1
47
+ * - op2
48
+ * - else-if:
49
+ * - condition: this == 2
50
+ * - op1
51
+ * - else:
52
+ * - op1
53
+ */
54
+ class BranchFilter extends TransformState {
55
+ constructor({ engine, args: { conditions, branchChains, elseChain } }, builder) {
56
+ super();
57
+
58
+ this.engine = engine;
59
+ this.conditions = conditions;
60
+ this.branchChains = _.map(branchChains, (transforms) => {
61
+ const chain = builder.createTransformChain(engine, transforms);
62
+ return new BranchChain(chain);
63
+ });
64
+ this.elseChain = elseChain
65
+ ? new BranchChain(builder.createTransformChain(engine, elseChain))
66
+ : null;
67
+ }
68
+
69
+ static op = 'branch';
70
+
71
+ static create(args) {
72
+ return new BranchFilter(args, args.builder);
73
+ }
74
+
75
+ static parseConfig(configUtil, defn) {
76
+ const components = defn.args;
77
+ if (!_.isArray(components)) {
78
+ configUtil.throwConfigError('branch must contain a list of if/else-if/else components', this.op);
79
+ }
80
+
81
+ const conditions = [];
82
+ const branchChains = [];
83
+ let elseChain = null;
84
+ let lastWasConditional = false;
85
+
86
+ components.forEach((comp, i) => {
87
+ if (!_.isObject(comp) || _.isArray(comp)) {
88
+ configUtil.throwConfigError('Invalid branch component', this.op);
89
+ }
90
+
91
+ const key = _.first(_.keys(comp));
92
+ const body = comp[key];
93
+
94
+ if (!_.isArray(body)) {
95
+ configUtil.throwConfigError(`${key} must contain a list of transforms`, this.op);
96
+ }
97
+
98
+ if (key === 'if' || key === 'else-if') {
99
+ if (body.length === 0) {
100
+ configUtil.throwConfigError(`${key} must have at least a condition`, this.op);
101
+ }
102
+
103
+ const first = body[0];
104
+ let conditionExpr = null;
105
+ if (_.isObject(first) && !_.isArray(first) && _.has(first, 'condition')) {
106
+ conditionExpr = first.condition;
107
+ if (_.isObject(conditionExpr) && _.has(conditionExpr, 'expression')) {
108
+ conditionExpr = conditionExpr.expression;
109
+ }
110
+ conditionExpr = conditionExpr.toString();
111
+ }
112
+
113
+ if (!conditionExpr) {
114
+ configUtil.throwConfigError(`${key} first element must be condition: <expression>`, this.op);
115
+ }
116
+
117
+ const compiledExpression = configUtil.compileExpression(conditionExpr, this.op, { this: 0 });
118
+ conditions.push({
119
+ expression: conditionExpr,
120
+ compiledExpression,
121
+ expressionSources: configUtil.expressionService.expressionTriggers(`${defn.path}.${i}.condition`),
122
+ hasThis: configUtil.expressionService.findName(conditionExpr, 'this'),
123
+ sourceCache: {},
124
+ });
125
+
126
+ const chainTransforms = body.slice(1);
127
+ const { transform } = configUtil.loadTransformList(chainTransforms, `${defn.path}.${i}.${key}`, false);
128
+ branchChains.push(transform);
129
+ lastWasConditional = true;
130
+ } else if (key === 'else') {
131
+ if (!lastWasConditional) {
132
+ configUtil.throwConfigError('else must follow if or else-if', this.op);
133
+ }
134
+ if (elseChain !== null) {
135
+ configUtil.throwConfigError('only one else allowed per branch', this.op);
136
+ }
137
+
138
+ const { transform } = configUtil.loadTransformList(body, `${defn.path}.${i}.else`, false);
139
+ elseChain = transform;
140
+ lastWasConditional = false;
141
+ } else {
142
+ configUtil.throwConfigError(`Unknown branch component: ${key}`, this.op);
143
+ }
144
+ });
145
+
146
+ defn.args = {
147
+ conditions,
148
+ branchChains,
149
+ elseChain,
150
+ };
151
+ }
152
+
153
+ static getExpressions(configUtil, body, info) {
154
+ const { varName, index, attribute } = info;
155
+ const parts = [];
156
+
157
+ if (!_.isArray(body)) {
158
+ return [];
159
+ }
160
+
161
+ _.each(body, (comp, compIndex) => {
162
+ if (!_.isObject(comp) || _.isArray(comp)) {
163
+ return;
164
+ }
165
+
166
+ const key = _.first(_.keys(comp));
167
+ const compBody = comp[key];
168
+
169
+ if (key === 'if' || key === 'else-if') {
170
+ if (_.isArray(compBody) && compBody.length > 0) {
171
+ const first = compBody[0];
172
+ if (_.isObject(first) && _.has(first, 'condition')) {
173
+ const expr = first.condition?.toString?.() ?? first.condition;
174
+ if (expr) {
175
+ parts.push({
176
+ path: `variables.${varName}.${index}.${attribute}.${compIndex}.condition`,
177
+ expression: expr,
178
+ }, {
179
+ path: `variables.${varName}`,
180
+ expression: expr,
181
+ });
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ if (key === 'if' || key === 'else-if' || key === 'else') {
188
+ const chainTransforms = key === 'else'
189
+ ? compBody
190
+ : compBody.slice(1);
191
+ const normalized = _.map(chainTransforms, (t) => (_.isString(t) ? { [t]: [] } : t));
192
+ // Include key (if/else-if/else) so expression paths match loadTransformList (used by parseConfig)
193
+ const chainVarName = `${varName}.${index}.${attribute}.${compIndex}.${key}`;
194
+ const chainExprs = configUtil.getExpressionsFromChain(
195
+ normalized,
196
+ chainVarName,
197
+ [...(info.ancestorVars || []), varName]
198
+ );
199
+ parts.push(..._.flatten(chainExprs));
200
+ }
201
+ });
202
+
203
+ return parts;
204
+ }
205
+
206
+ supportsPendingChanges() {
207
+ const chains = [...this.branchChains, this.elseChain].filter(Boolean);
208
+ return _.some(chains, (c) => c.chain.chainSupportsPendingChange?.());
209
+ }
210
+
211
+ selectBranch(context, value, time) {
212
+ for (let i = 0; i < this.conditions.length; i += 1) {
213
+ const rule = this.conditions[i];
214
+
215
+ if (rule.hasThis) {
216
+ rule.sourceCache.this = value;
217
+ }
218
+
219
+ const unavailTerm = _.reduce(rule.expressionSources, (res, name) => {
220
+ const state = this.engine.getState(name);
221
+ if (state) {
222
+ rule.sourceCache[name] = state.value;
223
+ }
224
+ return res || !state || !state.available;
225
+ }, false);
226
+
227
+ if (unavailTerm) {
228
+ return -1;
229
+ }
230
+
231
+ try {
232
+ if (rule.compiledExpression.evaluate(rule.sourceCache)) {
233
+ return i;
234
+ }
235
+ } catch (err) {
236
+ this.recordError(context, err, time);
237
+ }
238
+ }
239
+
240
+ return this.elseChain !== null ? this.conditions.length : -1;
241
+ }
242
+
243
+ update(context, value, time) {
244
+ if (value === 'UNAVAILABLE') {
245
+ this.setUnavailable(context, time);
246
+ } else {
247
+ super.update(context, value, time);
248
+ }
249
+ }
250
+
251
+ filter(context, value, time) {
252
+ const branchIndex = this.selectBranch(context, value, time);
253
+
254
+ if (branchIndex < 0) {
255
+ this.commitValue(context, value, time);
256
+ return;
257
+ }
258
+
259
+ const chain = branchIndex < this.branchChains.length ? this.branchChains[branchIndex] : this.elseChain;
260
+
261
+ const result = chain.update(context, value, time);
262
+ if (result !== null && result !== undefined) {
263
+ this.commitValue(context, result, time);
264
+ } else {
265
+ this.setUnavailable(context, time);
266
+ }
267
+ }
268
+
269
+ setUnavailable(context, time) {
270
+ super.setUnavailable(context, time);
271
+ }
272
+ }
273
+
274
+ module.exports = BranchFilter;
@@ -2,6 +2,7 @@
2
2
 
3
3
  const AccumulateFilter = require('./accumulate');
4
4
  const AverageFilter = require('./average');
5
+ const BranchFilter = require('./branch');
5
6
  const DebounceFilter = require('./debounce');
6
7
  const DownsampleFilter = require('./downsample');
7
8
  const EdgeFilter = require('./edge');
@@ -48,6 +49,7 @@ const WindowCountFilter = require('./windowCount');
48
49
  module.exports = {
49
50
  accumulate: AccumulateFilter,
50
51
  average: AverageFilter,
52
+ branch: BranchFilter,
51
53
  debounce: DebounceFilter,
52
54
  downsample: DownsampleFilter,
53
55
  edge: EdgeFilter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@machinemetrics/io-adapter-lib",
3
- "version": "2.35.1",
3
+ "version": "2.36.0",
4
4
  "description": "Configuration and engine implementation for MachineMetrics AdapterScripts and adapters",
5
5
  "main": "index.js",
6
6
  "license": "UNLICENSED",
@@ -0,0 +1,117 @@
1
+ version: 2
2
+ device: mtconnect-adapter
3
+ endpoint: localhost:8001
4
+ mtconnect-port: 8002
5
+ declare-keys:
6
+ - counter
7
+ - x
8
+ - y
9
+ - z
10
+ - obj
11
+ variables:
12
+ branch1:
13
+ - source: counter
14
+ - if:
15
+ - condition: this == 1
16
+ - expression: 100
17
+ - else-if:
18
+ - condition: this == 2
19
+ - expression: 200
20
+ - else:
21
+ - expression: 0
22
+ branch2simple:
23
+ - source: counter
24
+ - if:
25
+ - condition: this == 1
26
+ - expression: 10
27
+ - else:
28
+ - expression: 0
29
+ branch3:
30
+ - source: counter
31
+ - if:
32
+ - condition: x > y
33
+ - expression: 1
34
+ - else-if:
35
+ - condition: x < y
36
+ - expression: -1
37
+ - else:
38
+ - expression: 0
39
+ ifOnly:
40
+ - source: counter
41
+ - if:
42
+ - condition: this == 1
43
+ - expression: 100
44
+ ifElseIf:
45
+ - source: counter
46
+ - if:
47
+ - condition: this == 1
48
+ - expression: 100
49
+ - else-if:
50
+ - condition: this == 2
51
+ - expression: 200
52
+ twoElseIfs:
53
+ - source: counter
54
+ - if:
55
+ - condition: this == 1
56
+ - expression: 10
57
+ - else-if:
58
+ - condition: this == 2
59
+ - expression: 20
60
+ - else-if:
61
+ - condition: this == 3
62
+ - expression: 30
63
+ - else:
64
+ - expression: 0
65
+ ifElseIfConsecutive:
66
+ - source: counter
67
+ - if:
68
+ - condition: this == 1
69
+ - expression: 100
70
+ - else:
71
+ - expression: 2
72
+ - if:
73
+ - condition: counter == 2
74
+ - expression: 200
75
+ branchWithThis:
76
+ - source: counter
77
+ - if:
78
+ - condition: this == 1
79
+ - expression: 100
80
+ - else:
81
+ - expression: this
82
+ branchUnavailable:
83
+ - source: counter
84
+ - if:
85
+ - condition: this == 1
86
+ - expression: x + 1
87
+ - else:
88
+ - expression: this
89
+ branchWithInnerVar:
90
+ - source: counter
91
+ - if:
92
+ - condition: obj.a > 5
93
+ - expression: obj.a
94
+ - else:
95
+ - expression: obj.b
96
+ nested:
97
+ - source: counter
98
+ - if:
99
+ - condition: this > 0
100
+ - if:
101
+ - condition: this > 2
102
+ - expression: z
103
+ - else:
104
+ - expression: y
105
+ - else:
106
+ - expression: x
107
+ nestedInnerVar:
108
+ - source: counter
109
+ - if:
110
+ - condition: this > 0
111
+ - if:
112
+ - condition: obj.a > obj.b
113
+ - expression: obj.a + obj.b
114
+ - else:
115
+ - expression: y
116
+ - else:
117
+ - expression: x
@@ -0,0 +1,11 @@
1
+ version: 2
2
+ device: mtconnect-adapter
3
+ endpoint: localhost:8001
4
+ mtconnect-port: 8002
5
+ declare-keys:
6
+ - counter
7
+ variables:
8
+ badVar:
9
+ - source: counter
10
+ - else:
11
+ - expression: 0
@@ -0,0 +1,14 @@
1
+ version: 2
2
+ device: mtconnect-adapter
3
+ endpoint: localhost:8001
4
+ mtconnect-port: 8002
5
+ declare-keys:
6
+ - counter
7
+ variables:
8
+ badVar:
9
+ - source: counter
10
+ - if:
11
+ - condition: this == 1
12
+ - expression: 100
13
+ - else-if:
14
+ - expression: 200
@@ -0,0 +1,12 @@
1
+ version: 2
2
+ device: mtconnect-adapter
3
+ endpoint: localhost:8001
4
+ mtconnect-port: 8002
5
+ declare-keys:
6
+ - counter
7
+ variables:
8
+ badVar:
9
+ - source: counter
10
+ - else-if:
11
+ - condition: this == 1
12
+ - expression: 100
@@ -0,0 +1,11 @@
1
+ version: 2
2
+ device: mtconnect-adapter
3
+ endpoint: localhost:8001
4
+ mtconnect-port: 8002
5
+ declare-keys:
6
+ - counter
7
+ variables:
8
+ badVar:
9
+ - source: counter
10
+ - if:
11
+ - expression: 100
@@ -0,0 +1,18 @@
1
+ version: 2
2
+ device: mtconnect-adapter
3
+ endpoint: localhost:8001
4
+ mtconnect-port: 8002
5
+ declare-keys:
6
+ - counter
7
+ variables:
8
+ badVar:
9
+ - source: counter
10
+ - if:
11
+ - condition: this == 1
12
+ - expression: 100
13
+ - invert
14
+ - else-if:
15
+ - condition: this == 2
16
+ - expression: 200
17
+ - else:
18
+ - expression: 0
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const expect = require('chai').expect;
4
+ const ConfigError = require('../../lib/config/configError');
5
+ const testUtils = require('../util/testUtils');
6
+
7
+ describe('branch transform invalid config tests', function () {
8
+ it('rejects else by itself', async function () {
9
+ try {
10
+ await testUtils.loadConfig('transform/invalid-branch-else-only.yml');
11
+ expect.fail('Config with else by itself should have thrown');
12
+ } catch (err) {
13
+ expect(err).to.be.instanceof(ConfigError);
14
+ expect(err.message).to.include('else');
15
+ }
16
+ });
17
+
18
+ it('rejects else-if by itself', async function () {
19
+ try {
20
+ await testUtils.loadConfig('transform/invalid-branch-elseif-only.yml');
21
+ expect.fail('Config with else-if by itself should have thrown');
22
+ } catch (err) {
23
+ expect(err).to.be.instanceof(ConfigError);
24
+ expect(err.message).to.include('else-if');
25
+ }
26
+ });
27
+
28
+ it('rejects if without condition as first transform', async function () {
29
+ try {
30
+ await testUtils.loadConfig('transform/invalid-branch-if-no-condition.yml');
31
+ expect.fail('Config with if without condition should have thrown');
32
+ } catch (err) {
33
+ expect(err).to.be.instanceof(ConfigError);
34
+ expect(err.message).to.include('condition');
35
+ }
36
+ });
37
+
38
+ it('rejects else-if without condition as first transform', async function () {
39
+ try {
40
+ await testUtils.loadConfig('transform/invalid-branch-elseif-no-condition.yml');
41
+ expect.fail('Config with else-if without condition should have thrown');
42
+ } catch (err) {
43
+ expect(err).to.be.instanceof(ConfigError);
44
+ expect(err.message).to.include('condition');
45
+ }
46
+ });
47
+
48
+ it('rejects unrelated op between if/else-if/else', async function () {
49
+ try {
50
+ await testUtils.loadConfig('transform/invalid-branch-unrelated-op.yml');
51
+ expect.fail('Config with unrelated op between if/else-if/else should have thrown');
52
+ } catch (err) {
53
+ expect(err).to.be.instanceof(ConfigError);
54
+ expect(err.message).to.match(/else-if|else|Unsupported/);
55
+ }
56
+ });
57
+ });
@@ -0,0 +1,357 @@
1
+ 'use strict';
2
+
3
+ const expect = require('chai').expect;
4
+ const EngineV2 = require('../../lib/engine/engineV2');
5
+ const Builder = require('../../lib/engine/transformBuilderV2');
6
+ const testUtils = require('../util/testUtils');
7
+
8
+ describe('branch transform tests', function () {
9
+ let config;
10
+ before(async () => {
11
+ config = await testUtils.loadConfig('transform/branch.yml');
12
+ });
13
+
14
+ it('selects if branch when condition matches', function () {
15
+ const engine = new EngineV2(config);
16
+ const builder = new Builder(config);
17
+ builder.build(engine);
18
+
19
+ const source = testUtils.valueSource();
20
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branch1, source);
21
+
22
+ source.sendValue('counter', 1, 0);
23
+
24
+ engine.validateFilter([
25
+ [100, 0],
26
+ ]);
27
+ });
28
+
29
+ it('selects else-if branch when condition matches', function () {
30
+ const engine = new EngineV2(config);
31
+ const builder = new Builder(config);
32
+ builder.build(engine);
33
+
34
+ const source = testUtils.valueSource();
35
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branch1, source);
36
+
37
+ source.sendValue('counter', 2, 0);
38
+
39
+ engine.validateFilter([
40
+ [200, 0],
41
+ ]);
42
+ });
43
+
44
+ it('selects else branch when no condition matches', function () {
45
+ const engine = new EngineV2(config);
46
+ const builder = new Builder(config);
47
+ builder.build(engine);
48
+
49
+ const source = testUtils.valueSource();
50
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branch1, source);
51
+
52
+ source.sendValue('counter', 5, 0);
53
+
54
+ engine.validateFilter([
55
+ [0, 0],
56
+ ]);
57
+ });
58
+
59
+ it('evaluates constant expression in branch', function () {
60
+ const engine = new EngineV2(config);
61
+ const builder = new Builder(config);
62
+ builder.build(engine);
63
+
64
+ const source = testUtils.valueSource();
65
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branch2simple, source);
66
+
67
+ source.sendValue('counter', 1, 0);
68
+
69
+ engine.validateFilter([
70
+ [10, 0],
71
+ ]);
72
+ });
73
+
74
+ it('uses external variables in conditions', function () {
75
+ const engine = new EngineV2(config);
76
+ const builder = new Builder(config);
77
+ builder.build(engine);
78
+
79
+ const source = testUtils.valueSource();
80
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branch3, source);
81
+
82
+ source.sendValue('x', 10, 0);
83
+ source.sendValue('y', 5, 0);
84
+ source.sendValue('counter', 0, 0);
85
+
86
+ engine.validateFilter([
87
+ [1, 0],
88
+ ]);
89
+ });
90
+
91
+ it('selects else-if when x < y', function () {
92
+ const engine = new EngineV2(config);
93
+ const builder = new Builder(config);
94
+ builder.build(engine);
95
+
96
+ const source = testUtils.valueSource();
97
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branch3, source);
98
+
99
+ source.sendValue('x', 5, 0);
100
+ source.sendValue('y', 10, 0);
101
+ source.sendValue('counter', 0, 0);
102
+
103
+ engine.validateFilter([
104
+ [-1, 0],
105
+ ]);
106
+ });
107
+
108
+ it('selects else when x == y', function () {
109
+ const engine = new EngineV2(config);
110
+ const builder = new Builder(config);
111
+ builder.build(engine);
112
+
113
+ const source = testUtils.valueSource();
114
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branch3, source);
115
+
116
+ source.sendValue('x', 5, 0);
117
+ source.sendValue('y', 5, 0);
118
+ source.sendValue('counter', 0, 0);
119
+
120
+ engine.validateFilter([
121
+ [0, 0],
122
+ ]);
123
+ });
124
+
125
+ it('if only: selects branch when condition matches', function () {
126
+ const engine = new EngineV2(config);
127
+ const builder = new Builder(config);
128
+ builder.build(engine);
129
+
130
+ const source = testUtils.valueSource();
131
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.ifOnly, source);
132
+
133
+ source.sendValue('counter', 1, 0);
134
+
135
+ engine.validateFilter([
136
+ [100, 0],
137
+ ]);
138
+ });
139
+
140
+ it('if only: passes through when condition does not match', function () {
141
+ const engine = new EngineV2(config);
142
+ const builder = new Builder(config);
143
+ builder.build(engine);
144
+
145
+ const source = testUtils.valueSource();
146
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.ifOnly, source);
147
+
148
+ source.sendValue('counter', 5, 0);
149
+
150
+ engine.validateFilter([
151
+ [5, 0],
152
+ ]);
153
+ });
154
+
155
+ it('if/else-if without else: selects first branch', function () {
156
+ const engine = new EngineV2(config);
157
+ const builder = new Builder(config);
158
+ builder.build(engine);
159
+
160
+ const source = testUtils.valueSource();
161
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.ifElseIf, source);
162
+
163
+ source.sendValue('counter', 1, 0);
164
+
165
+ engine.validateFilter([
166
+ [100, 0],
167
+ ]);
168
+ });
169
+
170
+ it('if/else-if without else: selects second branch', function () {
171
+ const engine = new EngineV2(config);
172
+ const builder = new Builder(config);
173
+ builder.build(engine);
174
+
175
+ const source = testUtils.valueSource();
176
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.ifElseIf, source);
177
+
178
+ source.sendValue('counter', 2, 0);
179
+
180
+ engine.validateFilter([
181
+ [200, 0],
182
+ ]);
183
+ });
184
+
185
+ it('if/else-if without else: passes through when neither matches', function () {
186
+ const engine = new EngineV2(config);
187
+ const builder = new Builder(config);
188
+ builder.build(engine);
189
+
190
+ const source = testUtils.valueSource();
191
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.ifElseIf, source);
192
+
193
+ source.sendValue('counter', 5, 0);
194
+
195
+ engine.validateFilter([
196
+ [5, 0],
197
+ ]);
198
+ });
199
+
200
+ it('two else-ifs: selects each branch correctly', function () {
201
+ const engine = new EngineV2(config);
202
+ const builder = new Builder(config);
203
+ builder.build(engine);
204
+
205
+ const source = testUtils.valueSource();
206
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.twoElseIfs, source);
207
+
208
+ source.sendValue('counter', 1, 0);
209
+ source.sendValue('counter', 2, 1);
210
+ source.sendValue('counter', 3, 2);
211
+ source.sendValue('counter', 5, 3);
212
+
213
+ engine.validateFilter([[10, 0], [20, 1], [30, 2], [0, 3]]);
214
+ });
215
+
216
+ it('if/else/if: two consecutive branches', function () {
217
+ const engine = new EngineV2(config);
218
+ const builder = new Builder(config);
219
+ builder.build(engine);
220
+
221
+ const source = testUtils.valueSource();
222
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.ifElseIfConsecutive, source);
223
+
224
+ source.sendValue('counter', 1, 0);
225
+ source.sendValue('counter', 2, 1);
226
+ source.sendValue('counter', 3, 2);
227
+
228
+ engine.validateFilter([[100, 0], [200, 1], [2, 2]]);
229
+ });
230
+
231
+ it('expression: this in else branch passes through input value', function () {
232
+ const engine = new EngineV2(config);
233
+ const builder = new Builder(config);
234
+ builder.build(engine);
235
+
236
+ const source = testUtils.valueSource();
237
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branchWithThis, source);
238
+
239
+ source.sendValue('counter', 1, 0);
240
+ source.sendValue('counter', 5, 1);
241
+
242
+ engine.validateFilter([[100, 0], [5, 1]]);
243
+ });
244
+
245
+ it('unavailable in taken branch propagates; unavailable in not-taken branch has no effect', function () {
246
+ const engine = new EngineV2(config);
247
+ const builder = new Builder(config);
248
+ builder.build(engine);
249
+
250
+ const source = testUtils.valueSource();
251
+ engine.addValueSource(source);
252
+
253
+ const filterUpdates = [];
254
+ const endFilter = engine.variablePool.branchUnavailable.chainEnd();
255
+ endFilter.on('update', (value, time) => {
256
+ filterUpdates.push({ time, value });
257
+ });
258
+ endFilter.on('unavailable', (time) => {
259
+ filterUpdates.push({ time, value: 'UNAVAILABLE' });
260
+ });
261
+
262
+ const expectFilter = (expected) => {
263
+ const ref = expected.map(([value, time]) => ({ time, value }));
264
+ expect(filterUpdates.length).to.eq(ref.length);
265
+ ref.forEach((r, i) => {
266
+ expect(filterUpdates[i].time).to.be.closeTo(r.time, 0.000001);
267
+ expect(filterUpdates[i].value).to.deep.eq(r.value);
268
+ });
269
+ };
270
+
271
+ source.sendValue('x', 10, 0);
272
+ source.sendValue('counter', 1, 0);
273
+ source.sendValue('x', 'UNAVAILABLE', 1);
274
+ source.sendValue('counter', 5, 2);
275
+ source.sendValue('x', 'UNAVAILABLE', 2);
276
+
277
+ expectFilter([['UNAVAILABLE', 0], [11, 0], ['UNAVAILABLE', 1], ['UNAVAILABLE', 1], [5, 2]]);
278
+ });
279
+
280
+ it('nested if/else inside branch', function () {
281
+ const engine = new EngineV2(config);
282
+ const builder = new Builder(config);
283
+ builder.build(engine);
284
+
285
+ const source = testUtils.valueSource();
286
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.nested, source);
287
+
288
+ source.sendValue('x', 0, 0);
289
+ source.sendValue('y', 150, 0);
290
+ source.sendValue('z', 200, 0);
291
+
292
+ source.sendValue('counter', 0, 0);
293
+ source.sendValue('counter', 1, 1);
294
+ source.sendValue('counter', 2, 2);
295
+ source.sendValue('counter', 3, 3);
296
+
297
+ engine.validateFilter([[0, 0], [150, 1], [150, 2], [200, 3]]);
298
+ });
299
+
300
+ it('nested branch re-evaluates when expression variable in taken path updates', function () {
301
+ const engine = new EngineV2(config);
302
+ const builder = new Builder(config);
303
+ builder.build(engine);
304
+
305
+ const source = testUtils.valueSource();
306
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.nested, source);
307
+
308
+ source.sendValue('x', 0, 0);
309
+ source.sendValue('y', 150, 0);
310
+ source.sendValue('z', 200, 0);
311
+
312
+ source.sendValue('counter', 0, 0);
313
+ source.sendValue('x', 1, 1);
314
+ source.sendValue('counter', 1, 2);
315
+ source.sendValue('y', 151, 3);
316
+ source.sendValue('counter', 3, 4);
317
+ source.sendValue('z', 201, 5);
318
+
319
+ engine.validateFilter([[0, 0], [1, 1], [150, 2], [151, 3], [200, 4], [201, 5]]);
320
+ });
321
+
322
+ it('branchWithInnerVar selects branch by obj.a and emits obj.a or obj.b', function () {
323
+ const engine = new EngineV2(config);
324
+ const builder = new Builder(config);
325
+ builder.build(engine);
326
+
327
+ const source = testUtils.valueSource();
328
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.branchWithInnerVar, source);
329
+
330
+ source.sendValue('obj', { a: 10, b: 1 }, 0);
331
+ source.sendValue('counter', 1, 0);
332
+ source.sendValue('obj', { a: 3, b: 2 }, 1);
333
+ source.sendValue('counter', 2, 1);
334
+
335
+ engine.validateFilter([[10, 0], [2, 1]]);
336
+ });
337
+
338
+ it('nestedInnerVar selects nested branches by this and obj.a, obj.b', function () {
339
+ const engine = new EngineV2(config);
340
+ const builder = new Builder(config);
341
+ builder.build(engine);
342
+
343
+ const source = testUtils.valueSource();
344
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.nestedInnerVar, source);
345
+
346
+ source.sendValue('x', 0, 0);
347
+ source.sendValue('y', 150, 0);
348
+ source.sendValue('obj', { a: 10, b: 5 }, 0);
349
+
350
+ source.sendValue('counter', 0, 0);
351
+ source.sendValue('counter', 1, 1);
352
+ source.sendValue('obj', { a: 3, b: 8 }, 2);
353
+ source.sendValue('counter', 2, 2);
354
+
355
+ engine.validateFilter([[0, 0], [15, 1], [150, 2]]);
356
+ });
357
+ });