agent updates
This commit is contained in:
@@ -16,7 +16,7 @@ class Assertions {
|
|||||||
assertNoNaN(arr, label = "array") {
|
assertNoNaN(arr, label = "array") {
|
||||||
if (Array.isArray(arr)) {
|
if (Array.isArray(arr)) {
|
||||||
for (const el of arr) {
|
for (const el of arr) {
|
||||||
assertNoNaN(el, label);
|
this.assertNoNaN(el, label);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (Number.isNaN(arr)) {
|
if (Number.isNaN(arr)) {
|
||||||
@@ -26,4 +26,4 @@ class Assertions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Assertions;
|
module.exports = Assertions;
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ const Logger = require("./logger");
|
|||||||
|
|
||||||
class ConfigUtils {
|
class ConfigUtils {
|
||||||
constructor(defaultConfig, IloggerEnabled , IloggerLevel) {
|
constructor(defaultConfig, IloggerEnabled , IloggerLevel) {
|
||||||
const loggerEnabled = IloggerEnabled || true;
|
const loggerEnabled = IloggerEnabled ?? true;
|
||||||
const loggerLevel = IloggerLevel || "warn";
|
const loggerLevel = IloggerLevel ?? "warn";
|
||||||
this.logger = new Logger(loggerEnabled, loggerLevel, 'ConfigUtils');
|
this.logger = new Logger(loggerEnabled, loggerLevel, 'ConfigUtils');
|
||||||
this.defaultConfig = defaultConfig;
|
this.defaultConfig = defaultConfig;
|
||||||
this.validationUtils = new ValidationUtils(loggerEnabled, loggerLevel);
|
this.validationUtils = new ValidationUtils(loggerEnabled, loggerLevel);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class Logger {
|
|||||||
if (this.levels.includes(level)) {
|
if (this.levels.includes(level)) {
|
||||||
this.logLevel = level;
|
this.logLevel = level;
|
||||||
} else {
|
} else {
|
||||||
console.error(`[ERROR ${nameModule}]: Invalid log level: ${level}`);
|
console.error(`[ERROR] -> ${this.nameModule}: Invalid log level: ${level}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,4 +54,4 @@ class Logger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Logger;
|
module.exports = Logger;
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ const Logger = require("./logger");
|
|||||||
|
|
||||||
class ValidationUtils {
|
class ValidationUtils {
|
||||||
constructor(IloggerEnabled, IloggerLevel) {
|
constructor(IloggerEnabled, IloggerLevel) {
|
||||||
const loggerEnabled = IloggerEnabled || true;
|
const loggerEnabled = IloggerEnabled ?? true;
|
||||||
const loggerLevel = IloggerLevel || "warn";
|
const loggerLevel = IloggerLevel ?? "warn";
|
||||||
this.logger = new Logger(loggerEnabled, loggerLevel, 'ValidationUtils');
|
this.logger = new Logger(loggerEnabled, loggerLevel, 'ValidationUtils');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ class ValidationUtils {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if("default" in v){
|
if(v && typeof v === "object" && "default" in v){
|
||||||
//put the default value in the object
|
//put the default value in the object
|
||||||
newObj[k] = v.default;
|
newObj[k] = v.default;
|
||||||
continue;
|
continue;
|
||||||
@@ -496,6 +496,11 @@ class ValidationUtils {
|
|||||||
return fieldSchema.default;
|
return fieldSchema.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof configValue !== "string") {
|
||||||
|
this.logger.warn(`${name}.${key} is not a valid enum string. Using default value.`);
|
||||||
|
return fieldSchema.default;
|
||||||
|
}
|
||||||
|
|
||||||
const validValues = rules.values.map(e => e.value.toLowerCase());
|
const validValues = rules.values.map(e => e.value.toLowerCase());
|
||||||
|
|
||||||
//remove caps
|
//remove caps
|
||||||
|
|||||||
@@ -69,8 +69,10 @@ class Measurement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getLaggedValue(lag){
|
getLaggedValue(lag){
|
||||||
if(this.values.length <= lag) return null;
|
if (lag < 0) throw new Error('lag must be >= 0');
|
||||||
return this.values[this.values.length - lag];
|
const index = this.values.length - 1 - lag;
|
||||||
|
if (index < 0) return null;
|
||||||
|
return this.values[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
getLaggedSample(lag){
|
getLaggedSample(lag){
|
||||||
@@ -178,7 +180,7 @@ class Measurement {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const convertedValues = this.values.map(value =>
|
const convertedValues = this.values.map(value =>
|
||||||
convertModule.convert(value).from(this.unit).to(targetUnit)
|
convertModule(value).from(this.unit).to(targetUnit)
|
||||||
);
|
);
|
||||||
|
|
||||||
const newMeasurement = new Measurement(
|
const newMeasurement = new Measurement(
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ class MeasurementContainer {
|
|||||||
// Convert if needed
|
// Convert if needed
|
||||||
if (measurement.unit && requestedUnit !== measurement.unit) {
|
if (measurement.unit && requestedUnit !== measurement.unit) {
|
||||||
try {
|
try {
|
||||||
const convertedValue = convertModule(value).from(measurement.unit).to(requestedUnit);
|
const convertedValue = convertModule(sample.value).from(measurement.unit).to(requestedUnit);
|
||||||
//replace old value in sample and return obj
|
//replace old value in sample and return obj
|
||||||
sample.value = convertedValue ;
|
sample.value = convertedValue ;
|
||||||
sample.unit = requestedUnit;
|
sample.unit = requestedUnit;
|
||||||
@@ -364,7 +364,7 @@ class MeasurementContainer {
|
|||||||
// Convert if needed
|
// Convert if needed
|
||||||
if (measurement.unit && requestedUnit !== measurement.unit) {
|
if (measurement.unit && requestedUnit !== measurement.unit) {
|
||||||
try {
|
try {
|
||||||
const convertedValue = convertModule(value).from(measurement.unit).to(requestedUnit);
|
const convertedValue = convertModule(sample.value).from(measurement.unit).to(requestedUnit);
|
||||||
//replace old value in sample and return obj
|
//replace old value in sample and return obj
|
||||||
sample.value = convertedValue ;
|
sample.value = convertedValue ;
|
||||||
sample.unit = requestedUnit;
|
sample.unit = requestedUnit;
|
||||||
@@ -619,16 +619,16 @@ class MeasurementContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_convertPositionNum2Str(positionValue) {
|
_convertPositionNum2Str(positionValue) {
|
||||||
switch (positionValue) {
|
if (positionValue === 0) {
|
||||||
case 0:
|
|
||||||
return "atEquipment";
|
return "atEquipment";
|
||||||
case (positionValue < 0):
|
|
||||||
return "upstream";
|
|
||||||
case (positionValue > 0):
|
|
||||||
return "downstream";
|
|
||||||
default:
|
|
||||||
console.log(`Invalid position provided: ${positionValue}`);
|
|
||||||
}
|
}
|
||||||
|
if (positionValue < 0) {
|
||||||
|
return "upstream";
|
||||||
|
}
|
||||||
|
if (positionValue > 0) {
|
||||||
|
return "downstream";
|
||||||
|
}
|
||||||
|
console.log(`Invalid position provided: ${positionValue}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,21 @@ class MenuManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const config = this.configManager.getConfig(nodeName);
|
const config = this.configManager.getConfig(nodeName);
|
||||||
return config?.functionality?.softwareType || nodeName;
|
const softwareType = config?.functionality?.softwareType;
|
||||||
|
|
||||||
|
if (typeof softwareType === 'string' && softwareType.trim()) {
|
||||||
|
return softwareType;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
softwareType &&
|
||||||
|
typeof softwareType === 'object' &&
|
||||||
|
typeof softwareType.default === 'string' &&
|
||||||
|
softwareType.default.trim()
|
||||||
|
) {
|
||||||
|
return softwareType.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeName;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Unable to determine softwareType for ${nodeName}: ${error.message}`);
|
console.warn(`Unable to determine softwareType for ${nodeName}: ${error.message}`);
|
||||||
return nodeName;
|
return nodeName;
|
||||||
|
|||||||
42
test/00-barrel-contract.test.js
Normal file
42
test/00-barrel-contract.test.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const barrel = require('../index.js');
|
||||||
|
|
||||||
|
test('barrel exports expected public members', () => {
|
||||||
|
const expected = [
|
||||||
|
'predict',
|
||||||
|
'interpolation',
|
||||||
|
'configManager',
|
||||||
|
'assetApiConfig',
|
||||||
|
'outputUtils',
|
||||||
|
'configUtils',
|
||||||
|
'logger',
|
||||||
|
'validation',
|
||||||
|
'assertions',
|
||||||
|
'MeasurementContainer',
|
||||||
|
'nrmse',
|
||||||
|
'state',
|
||||||
|
'coolprop',
|
||||||
|
'convert',
|
||||||
|
'MenuManager',
|
||||||
|
'childRegistrationUtils',
|
||||||
|
'loadCurve',
|
||||||
|
'loadModel',
|
||||||
|
'gravity',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const key of expected) {
|
||||||
|
assert.ok(key in barrel, `missing export: ${key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('barrel types are callable where expected', () => {
|
||||||
|
assert.equal(typeof barrel.logger, 'function');
|
||||||
|
assert.equal(typeof barrel.validation, 'function');
|
||||||
|
assert.equal(typeof barrel.configUtils, 'function');
|
||||||
|
assert.equal(typeof barrel.outputUtils, 'function');
|
||||||
|
assert.equal(typeof barrel.MeasurementContainer, 'function');
|
||||||
|
assert.equal(typeof barrel.convert, 'function');
|
||||||
|
assert.equal(typeof barrel.gravity.getStandardGravity, 'function');
|
||||||
|
});
|
||||||
14
test/assertions.test.js
Normal file
14
test/assertions.test.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const Assertions = require('../src/helper/assertionUtils.js');
|
||||||
|
|
||||||
|
test('assertNoNaN does not throw for valid nested arrays', () => {
|
||||||
|
const assertions = new Assertions();
|
||||||
|
assert.doesNotThrow(() => assertions.assertNoNaN([1, [2, 3, [4]]]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('assertNoNaN throws when NaN exists in nested arrays', () => {
|
||||||
|
const assertions = new Assertions();
|
||||||
|
assert.throws(() => assertions.assertNoNaN([1, [2, NaN]]), /NaN detected/);
|
||||||
|
});
|
||||||
55
test/child-registration-utils.test.js
Normal file
55
test/child-registration-utils.test.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const ChildRegistrationUtils = require('../src/helper/childRegistrationUtils.js');
|
||||||
|
|
||||||
|
function makeMainClass() {
|
||||||
|
return {
|
||||||
|
logger: {
|
||||||
|
debug() {},
|
||||||
|
info() {},
|
||||||
|
warn() {},
|
||||||
|
error() {},
|
||||||
|
},
|
||||||
|
child: {},
|
||||||
|
registerChildCalls: [],
|
||||||
|
registerChild(child, softwareType) {
|
||||||
|
this.registerChildCalls.push({ child, softwareType });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('registerChild wires parent, measurement context, and storage', async () => {
|
||||||
|
const mainClass = makeMainClass();
|
||||||
|
const utils = new ChildRegistrationUtils(mainClass);
|
||||||
|
|
||||||
|
const measurementContext = {
|
||||||
|
childId: null,
|
||||||
|
childName: null,
|
||||||
|
parentRef: null,
|
||||||
|
setChildId(v) { this.childId = v; },
|
||||||
|
setChildName(v) { this.childName = v; },
|
||||||
|
setParentRef(v) { this.parentRef = v; },
|
||||||
|
};
|
||||||
|
|
||||||
|
const child = {
|
||||||
|
config: {
|
||||||
|
functionality: { softwareType: 'measurement' },
|
||||||
|
general: { name: 'PT1', id: 'child-1' },
|
||||||
|
asset: { category: 'sensor' },
|
||||||
|
},
|
||||||
|
measurements: measurementContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
await utils.registerChild(child, 'upstream');
|
||||||
|
|
||||||
|
assert.deepEqual(child.parent, [mainClass]);
|
||||||
|
assert.equal(child.positionVsParent, 'upstream');
|
||||||
|
assert.equal(measurementContext.childId, 'child-1');
|
||||||
|
assert.equal(measurementContext.childName, 'PT1');
|
||||||
|
assert.equal(measurementContext.parentRef, mainClass);
|
||||||
|
|
||||||
|
assert.equal(mainClass.child.measurement.sensor.length, 1);
|
||||||
|
assert.equal(utils.getChildById('child-1'), child);
|
||||||
|
assert.equal(mainClass.registerChildCalls.length, 1);
|
||||||
|
});
|
||||||
33
test/config-manager.test.js
Normal file
33
test/config-manager.test.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const ConfigManager = require('../src/configs/index.js');
|
||||||
|
|
||||||
|
test('can read known config and report existence', () => {
|
||||||
|
const manager = new ConfigManager('.');
|
||||||
|
assert.equal(manager.hasConfig('measurement'), true);
|
||||||
|
|
||||||
|
const config = manager.getConfig('measurement');
|
||||||
|
assert.ok(config.functionality);
|
||||||
|
assert.ok(config.functionality.softwareType);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getAvailableConfigs includes known names', () => {
|
||||||
|
const manager = new ConfigManager('.');
|
||||||
|
const configs = manager.getAvailableConfigs();
|
||||||
|
assert.ok(configs.includes('measurement'));
|
||||||
|
assert.ok(configs.includes('rotatingMachine'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createEndpoint creates executable JS payload shell', () => {
|
||||||
|
const manager = new ConfigManager('.');
|
||||||
|
const script = manager.createEndpoint('measurement');
|
||||||
|
|
||||||
|
assert.match(script, /window\.EVOLV\.nodes\.measurement/);
|
||||||
|
assert.match(script, /config loaded and endpoint created/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getConfig throws on missing config', () => {
|
||||||
|
const manager = new ConfigManager('.');
|
||||||
|
assert.throws(() => manager.getConfig('definitely-not-real'), /Failed to load config/);
|
||||||
|
});
|
||||||
51
test/config-utils.test.js
Normal file
51
test/config-utils.test.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const ConfigUtils = require('../src/helper/configUtils.js');
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
functionality: {
|
||||||
|
softwareType: {
|
||||||
|
default: 'measurement',
|
||||||
|
rules: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
logging: {
|
||||||
|
enabled: { default: true, rules: { type: 'boolean' } },
|
||||||
|
logLevel: {
|
||||||
|
default: 'info',
|
||||||
|
rules: {
|
||||||
|
type: 'enum',
|
||||||
|
values: [{ value: 'debug' }, { value: 'info' }, { value: 'warn' }, { value: 'error' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: { default: 'default-name', rules: { type: 'string' } },
|
||||||
|
},
|
||||||
|
scaling: {
|
||||||
|
absMin: { default: 0, rules: { type: 'number' } },
|
||||||
|
absMax: { default: 100, rules: { type: 'number' } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test('initConfig applies defaults', () => {
|
||||||
|
const cfg = new ConfigUtils(defaultConfig, false, 'error');
|
||||||
|
const result = cfg.initConfig({});
|
||||||
|
assert.equal(result.general.name, 'default-name');
|
||||||
|
assert.equal(result.scaling.absMax, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updateConfig merges nested overrides and revalidates', () => {
|
||||||
|
const cfg = new ConfigUtils(defaultConfig, false, 'error');
|
||||||
|
const base = cfg.initConfig({ general: { name: 'sensor-a' } });
|
||||||
|
const updated = cfg.updateConfig(base, { scaling: { absMax: 150 } });
|
||||||
|
|
||||||
|
assert.equal(updated.general.name, 'sensor-a');
|
||||||
|
assert.equal(updated.scaling.absMax, 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('constructor respects explicit logger disabled flag', () => {
|
||||||
|
const cfg = new ConfigUtils(defaultConfig, false, 'error');
|
||||||
|
assert.equal(cfg.logger.logging, false);
|
||||||
|
});
|
||||||
21
test/gravity.test.js
Normal file
21
test/gravity.test.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const gravity = require('../src/helper/gravity.js');
|
||||||
|
|
||||||
|
test('standard gravity constant is available', () => {
|
||||||
|
assert.ok(Math.abs(gravity.getStandardGravity() - 9.80665) < 1e-9);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('local gravity decreases with elevation', () => {
|
||||||
|
const seaLevel = gravity.getLocalGravity(45, 0);
|
||||||
|
const high = gravity.getLocalGravity(45, 1000);
|
||||||
|
assert.ok(high < seaLevel);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pressureHead and weightForce use local gravity', () => {
|
||||||
|
const dp = gravity.pressureHead(1000, 5, 45, 0);
|
||||||
|
const force = gravity.weightForce(2, 45, 0);
|
||||||
|
assert.ok(dp > 0);
|
||||||
|
assert.ok(force > 0);
|
||||||
|
});
|
||||||
24
test/helpers.js
Normal file
24
test/helpers.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
function makeLogger() {
|
||||||
|
return {
|
||||||
|
debug() {},
|
||||||
|
info() {},
|
||||||
|
warn() {},
|
||||||
|
error() {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function near(actual, expected, epsilon = 1e-6) {
|
||||||
|
return Math.abs(actual - expected) <= epsilon;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixturePath(...segments) {
|
||||||
|
return path.join(__dirname, ...segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
makeLogger,
|
||||||
|
near,
|
||||||
|
fixturePath,
|
||||||
|
};
|
||||||
65
test/logger.test.js
Normal file
65
test/logger.test.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const Logger = require('../src/helper/logger.js');
|
||||||
|
|
||||||
|
function withPatchedConsole(fn) {
|
||||||
|
const original = {
|
||||||
|
debug: console.debug,
|
||||||
|
info: console.info,
|
||||||
|
warn: console.warn,
|
||||||
|
error: console.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
const calls = [];
|
||||||
|
console.debug = (...args) => calls.push(['debug', ...args]);
|
||||||
|
console.info = (...args) => calls.push(['info', ...args]);
|
||||||
|
console.warn = (...args) => calls.push(['warn', ...args]);
|
||||||
|
console.error = (...args) => calls.push(['error', ...args]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fn(calls);
|
||||||
|
} finally {
|
||||||
|
console.debug = original.debug;
|
||||||
|
console.info = original.info;
|
||||||
|
console.warn = original.warn;
|
||||||
|
console.error = original.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('respects log level threshold', () => {
|
||||||
|
withPatchedConsole((calls) => {
|
||||||
|
const logger = new Logger(true, 'warn', 'T');
|
||||||
|
logger.debug('a');
|
||||||
|
logger.info('b');
|
||||||
|
logger.warn('c');
|
||||||
|
logger.error('d');
|
||||||
|
|
||||||
|
const levels = calls.map((c) => c[0]);
|
||||||
|
assert.deepEqual(levels, ['warn', 'error']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toggleLogging disables output', () => {
|
||||||
|
withPatchedConsole((calls) => {
|
||||||
|
const logger = new Logger(true, 'debug', 'T');
|
||||||
|
logger.toggleLogging();
|
||||||
|
logger.debug('x');
|
||||||
|
logger.error('y');
|
||||||
|
assert.equal(calls.length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setLogLevel updates to valid level', () => {
|
||||||
|
const logger = new Logger(true, 'debug', 'T');
|
||||||
|
logger.setLogLevel('error');
|
||||||
|
assert.equal(logger.logLevel, 'error');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setLogLevel with invalid value should not throw', () => {
|
||||||
|
withPatchedConsole(() => {
|
||||||
|
const logger = new Logger(true, 'debug', 'T');
|
||||||
|
assert.doesNotThrow(() => logger.setLogLevel('invalid-level'));
|
||||||
|
assert.equal(logger.logLevel, 'debug');
|
||||||
|
});
|
||||||
|
});
|
||||||
29
test/measurement-builder.test.js
Normal file
29
test/measurement-builder.test.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const MeasurementBuilder = require('../src/measurements/MeasurementBuilder.js');
|
||||||
|
|
||||||
|
test('builder requires mandatory fields', () => {
|
||||||
|
assert.throws(() => new MeasurementBuilder().build(), /Measurement type is required/);
|
||||||
|
assert.throws(() => new MeasurementBuilder().setType('flow').build(), /Measurement variant is required/);
|
||||||
|
assert.throws(
|
||||||
|
() => new MeasurementBuilder().setType('flow').setVariant('measured').build(),
|
||||||
|
/Measurement position is required/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('builder creates measurement with provided config', () => {
|
||||||
|
const measurement = new MeasurementBuilder()
|
||||||
|
.setType('flow')
|
||||||
|
.setVariant('measured')
|
||||||
|
.setPosition('upstream')
|
||||||
|
.setWindowSize(25)
|
||||||
|
.setDistance(3.2)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assert.equal(measurement.type, 'flow');
|
||||||
|
assert.equal(measurement.variant, 'measured');
|
||||||
|
assert.equal(measurement.position, 'upstream');
|
||||||
|
assert.equal(measurement.windowSize, 25);
|
||||||
|
assert.equal(measurement.distance, 3.2);
|
||||||
|
});
|
||||||
61
test/measurement-container-core.test.js
Normal file
61
test/measurement-container-core.test.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const MeasurementContainer = require('../src/measurements/MeasurementContainer.js');
|
||||||
|
|
||||||
|
function makeContainer() {
|
||||||
|
return new MeasurementContainer({
|
||||||
|
windowSize: 10,
|
||||||
|
defaultUnits: {
|
||||||
|
flow: 'm3/h',
|
||||||
|
pressure: 'mbar',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('stores and retrieves measurements via chain API', () => {
|
||||||
|
const c = makeContainer();
|
||||||
|
c.type('flow').variant('measured').position('upstream').value(100, 1, 'm3/h');
|
||||||
|
|
||||||
|
assert.equal(c.type('flow').variant('measured').position('upstream').getCurrentValue(), 100);
|
||||||
|
assert.equal(c.type('flow').variant('measured').position('upstream').exists(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('distance(null) auto-derives from position mapping', () => {
|
||||||
|
const c = makeContainer();
|
||||||
|
c.type('pressure').variant('measured').position('upstream').distance(null).value(5, 1, 'mbar');
|
||||||
|
|
||||||
|
const m = c.type('pressure').variant('measured').position('upstream').get();
|
||||||
|
assert.equal(m.distance, Number.POSITIVE_INFINITY);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getLaggedSample with requested unit converts sample value', () => {
|
||||||
|
const c = makeContainer();
|
||||||
|
c.type('flow').variant('measured').position('upstream').value(3.6, 1, 'm3/h');
|
||||||
|
c.type('flow').variant('measured').position('upstream').value(7.2, 2, 'm3/h');
|
||||||
|
|
||||||
|
const previous = c.type('flow').variant('measured').position('upstream').getLaggedSample(1, 'm3/s');
|
||||||
|
assert.ok(previous);
|
||||||
|
assert.equal(previous.unit, 'm3/s');
|
||||||
|
assert.ok(Math.abs(previous.value - 0.001) < 1e-8);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('difference computes current and average delta between positions', () => {
|
||||||
|
const c = makeContainer();
|
||||||
|
c.type('pressure').variant('measured').position('downstream').value(120, 1, 'mbar');
|
||||||
|
c.type('pressure').variant('measured').position('downstream').value(130, 2, 'mbar');
|
||||||
|
c.type('pressure').variant('measured').position('upstream').value(100, 1, 'mbar');
|
||||||
|
c.type('pressure').variant('measured').position('upstream').value(110, 2, 'mbar');
|
||||||
|
|
||||||
|
const diff = c.type('pressure').variant('measured').difference();
|
||||||
|
assert.equal(diff.value, 20);
|
||||||
|
assert.equal(diff.avgDiff, 20);
|
||||||
|
assert.equal(diff.unit, 'mbar');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_convertPositionNum2Str maps signs to labels', () => {
|
||||||
|
const c = makeContainer();
|
||||||
|
assert.equal(c._convertPositionNum2Str(0), 'atEquipment');
|
||||||
|
assert.equal(c._convertPositionNum2Str(1), 'downstream');
|
||||||
|
assert.equal(c._convertPositionNum2Str(-1), 'upstream');
|
||||||
|
});
|
||||||
49
test/measurement.test.js
Normal file
49
test/measurement.test.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const Measurement = require('../src/measurements/Measurement.js');
|
||||||
|
const { near } = require('./helpers.js');
|
||||||
|
|
||||||
|
test('maintains rolling window and exposes stats', () => {
|
||||||
|
const m = new Measurement('flow', 'measured', 'upstream', 3);
|
||||||
|
m.setValue(10, 1).setValue(20, 2).setValue(30, 3).setValue(40, 4);
|
||||||
|
|
||||||
|
assert.deepEqual(m.getAllValues().values, [20, 30, 40]);
|
||||||
|
assert.deepEqual(m.getAllValues().timestamps, [2, 3, 4]);
|
||||||
|
assert.equal(m.getCurrentValue(), 40);
|
||||||
|
assert.equal(m.getAverage(), 30);
|
||||||
|
assert.equal(m.getMin(), 20);
|
||||||
|
assert.equal(m.getMax(), 40);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lag semantics: lag=1 is previous sample', () => {
|
||||||
|
const m = new Measurement('flow', 'measured', 'upstream', 5);
|
||||||
|
m.setValue(10, 100).setValue(20, 200).setValue(30, 300);
|
||||||
|
|
||||||
|
assert.equal(m.getLaggedSample(0).value, 30);
|
||||||
|
assert.equal(m.getLaggedSample(1).value, 20);
|
||||||
|
assert.equal(m.getLaggedValue(1), 20);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('convertTo converts values to target unit', () => {
|
||||||
|
const m = new Measurement('flow', 'measured', 'upstream', 5);
|
||||||
|
m.setUnit('m3/h');
|
||||||
|
m.setValue(3.6, 1);
|
||||||
|
|
||||||
|
const converted = m.convertTo('m3/s');
|
||||||
|
assert.ok(near(converted.getCurrentValue(), 0.001, 1e-8));
|
||||||
|
assert.equal(converted.unit, 'm3/s');
|
||||||
|
assert.equal(converted.getLatestTimestamp(), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createDifference aligns timestamps and subtracts downstream from upstream', () => {
|
||||||
|
const up = new Measurement('pressure', 'measured', 'upstream', 10).setUnit('mbar');
|
||||||
|
const down = new Measurement('pressure', 'measured', 'downstream', 10).setUnit('mbar');
|
||||||
|
|
||||||
|
up.setValue(120, 1).setValue(140, 2);
|
||||||
|
down.setValue(100, 2).setValue(95, 3);
|
||||||
|
|
||||||
|
const diff = Measurement.createDifference(up, down);
|
||||||
|
assert.deepEqual(diff.getAllValues().timestamps, [2]);
|
||||||
|
assert.deepEqual(diff.getAllValues().values, [40]);
|
||||||
|
});
|
||||||
20
test/menu-manager.test.js
Normal file
20
test/menu-manager.test.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const MenuManager = require('../src/menu/index.js');
|
||||||
|
|
||||||
|
test('createEndpoint returns script including initEditor and menuData', () => {
|
||||||
|
const manager = new MenuManager();
|
||||||
|
const script = manager.createEndpoint('measurement', ['asset', 'logger', 'position']);
|
||||||
|
|
||||||
|
assert.match(script, /window\.EVOLV\.nodes\.measurement\.initEditor/);
|
||||||
|
assert.match(script, /window\.EVOLV\.nodes\.measurement\.menuData/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_getSoftwareType resolves to string identifier', () => {
|
||||||
|
const manager = new MenuManager();
|
||||||
|
const softwareType = manager._getSoftwareType('measurement');
|
||||||
|
|
||||||
|
assert.equal(typeof softwareType, 'string');
|
||||||
|
assert.equal(softwareType, 'measurement');
|
||||||
|
});
|
||||||
37
test/nrmse.test.js
Normal file
37
test/nrmse.test.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const ErrorMetrics = require('../src/nrmse/errorMetrics.js');
|
||||||
|
const { makeLogger } = require('./helpers.js');
|
||||||
|
|
||||||
|
test('MSE and RMSE calculations are correct', () => {
|
||||||
|
const m = new ErrorMetrics({}, makeLogger());
|
||||||
|
const predicted = [1, 2, 3];
|
||||||
|
const measured = [1, 3, 5];
|
||||||
|
|
||||||
|
assert.ok(Math.abs(m.meanSquaredError(predicted, measured) - 5 / 3) < 1e-9);
|
||||||
|
assert.ok(Math.abs(m.rootMeanSquaredError(predicted, measured) - Math.sqrt(5 / 3)) < 1e-9);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizeUsingRealtime throws when range is zero', () => {
|
||||||
|
const m = new ErrorMetrics({}, makeLogger());
|
||||||
|
assert.throws(() => m.normalizeUsingRealtime([1, 1, 1], [1, 1, 1]), /Invalid process range/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('longTermNRMSD returns 0 before 100 samples and value after', () => {
|
||||||
|
const m = new ErrorMetrics({}, makeLogger());
|
||||||
|
for (let i = 0; i < 99; i++) {
|
||||||
|
assert.equal(m.longTermNRMSD(0.1), 0);
|
||||||
|
}
|
||||||
|
assert.notEqual(m.longTermNRMSD(0.2), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('assessDrift returns expected result envelope', () => {
|
||||||
|
const m = new ErrorMetrics({}, makeLogger());
|
||||||
|
const out = m.assessDrift([100, 101, 102], [99, 100, 103], 90, 110);
|
||||||
|
|
||||||
|
assert.equal(typeof out.nrmse, 'number');
|
||||||
|
assert.equal(typeof out.longTermNRMSD, 'number');
|
||||||
|
assert.ok('immediateLevel' in out);
|
||||||
|
assert.ok('longTermLevel' in out);
|
||||||
|
});
|
||||||
42
test/output-utils.test.js
Normal file
42
test/output-utils.test.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const OutputUtils = require('../src/helper/outputUtils.js');
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
functionality: { softwareType: 'measurement', role: 'sensor' },
|
||||||
|
general: { id: 'abc', unit: 'mbar' },
|
||||||
|
asset: {
|
||||||
|
uuid: 'u1',
|
||||||
|
tagcode: 't1',
|
||||||
|
geoLocation: { lat: 51.6, lon: 4.7 },
|
||||||
|
category: 'measurement',
|
||||||
|
type: 'pressure',
|
||||||
|
model: 'M1',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test('process format emits message with changed fields only', () => {
|
||||||
|
const out = new OutputUtils();
|
||||||
|
|
||||||
|
const first = out.formatMsg({ a: 1, b: 2 }, config, 'process');
|
||||||
|
assert.equal(first.topic, 'measurement_abc');
|
||||||
|
assert.deepEqual(first.payload, { a: 1, b: 2 });
|
||||||
|
|
||||||
|
const second = out.formatMsg({ a: 1, b: 2 }, config, 'process');
|
||||||
|
assert.equal(second, undefined);
|
||||||
|
|
||||||
|
const third = out.formatMsg({ a: 1, b: 3, c: { x: 1 } }, config, 'process');
|
||||||
|
assert.deepEqual(third.payload, { b: 3, c: JSON.stringify({ x: 1 }) });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('influx format flattens tags and stringifies tag values', () => {
|
||||||
|
const out = new OutputUtils();
|
||||||
|
const msg = out.formatMsg({ value: 10 }, config, 'influxdb');
|
||||||
|
|
||||||
|
assert.equal(msg.topic, 'measurement_abc');
|
||||||
|
assert.equal(msg.payload.measurement, 'measurement_abc');
|
||||||
|
assert.equal(msg.payload.tags.geoLocation_lat, '51.6');
|
||||||
|
assert.equal(msg.payload.tags.geoLocation_lon, '4.7');
|
||||||
|
assert.ok(msg.payload.timestamp instanceof Date);
|
||||||
|
});
|
||||||
62
test/validation-utils.test.js
Normal file
62
test/validation-utils.test.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const ValidationUtils = require('../src/helper/validationUtils.js');
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
functionality: {
|
||||||
|
softwareType: {
|
||||||
|
default: 'measurement',
|
||||||
|
rules: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enabled: {
|
||||||
|
default: true,
|
||||||
|
rules: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
default: 'auto',
|
||||||
|
rules: {
|
||||||
|
type: 'enum',
|
||||||
|
values: [{ value: 'auto' }, { value: 'manual' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
default: 'sensor',
|
||||||
|
rules: { type: 'string' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test('validateSchema applies defaults and type coercion where supported', () => {
|
||||||
|
const validation = new ValidationUtils(false, 'error');
|
||||||
|
const result = validation.validateSchema({ enabled: 'true', name: 'SENSOR' }, schema, 'test');
|
||||||
|
|
||||||
|
assert.equal(result.enabled, true);
|
||||||
|
assert.equal(result.name, 'sensor');
|
||||||
|
assert.equal(result.mode, 'auto');
|
||||||
|
assert.equal(result.functionality.softwareType, 'measurement');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enum with non-string value falls back to default', () => {
|
||||||
|
const validation = new ValidationUtils(false, 'error');
|
||||||
|
const result = validation.validateSchema({ mode: 123 }, schema, 'test');
|
||||||
|
assert.equal(result.mode, 'auto');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('curve validation falls back to default for invalid dimension structure', () => {
|
||||||
|
const validation = new ValidationUtils(false, 'error');
|
||||||
|
const defaultCurve = { 1: { x: [1, 2], y: [10, 20] } };
|
||||||
|
const invalid = { 1: { x: [2, 1], y: [20, 10] } };
|
||||||
|
const curve = validation.validateCurve(invalid, defaultCurve);
|
||||||
|
assert.deepEqual(curve, defaultCurve);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removeUnwantedKeys handles primitive values without throwing', () => {
|
||||||
|
const validation = new ValidationUtils(false, 'error');
|
||||||
|
const input = {
|
||||||
|
a: { default: 1, rules: { type: 'number' } },
|
||||||
|
b: 2,
|
||||||
|
c: 'x',
|
||||||
|
};
|
||||||
|
assert.doesNotThrow(() => validation.removeUnwantedKeys(input));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user