diff --git a/src/helper/assertionUtils.js b/src/helper/assertionUtils.js index 2ec2645..1e9214f 100644 --- a/src/helper/assertionUtils.js +++ b/src/helper/assertionUtils.js @@ -16,7 +16,7 @@ class Assertions { assertNoNaN(arr, label = "array") { if (Array.isArray(arr)) { for (const el of arr) { - assertNoNaN(el, label); + this.assertNoNaN(el, label); } } else { if (Number.isNaN(arr)) { @@ -26,4 +26,4 @@ class Assertions { } } -module.exports = Assertions; \ No newline at end of file +module.exports = Assertions; diff --git a/src/helper/configUtils.js b/src/helper/configUtils.js index e81d1d5..58cb334 100644 --- a/src/helper/configUtils.js +++ b/src/helper/configUtils.js @@ -39,8 +39,8 @@ const Logger = require("./logger"); class ConfigUtils { constructor(defaultConfig, IloggerEnabled , IloggerLevel) { - const loggerEnabled = IloggerEnabled || true; - const loggerLevel = IloggerLevel || "warn"; + const loggerEnabled = IloggerEnabled ?? true; + const loggerLevel = IloggerLevel ?? "warn"; this.logger = new Logger(loggerEnabled, loggerLevel, 'ConfigUtils'); this.defaultConfig = defaultConfig; this.validationUtils = new ValidationUtils(loggerEnabled, loggerLevel); diff --git a/src/helper/logger.js b/src/helper/logger.js index 8b4f696..3c2f15b 100644 --- a/src/helper/logger.js +++ b/src/helper/logger.js @@ -44,7 +44,7 @@ class Logger { if (this.levels.includes(level)) { this.logLevel = level; } 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; \ No newline at end of file + module.exports = Logger; diff --git a/src/helper/validationUtils.js b/src/helper/validationUtils.js index ddbba58..c442d59 100644 --- a/src/helper/validationUtils.js +++ b/src/helper/validationUtils.js @@ -36,8 +36,8 @@ const Logger = require("./logger"); class ValidationUtils { constructor(IloggerEnabled, IloggerLevel) { - const loggerEnabled = IloggerEnabled || true; - const loggerLevel = IloggerLevel || "warn"; + const loggerEnabled = IloggerEnabled ?? true; + const loggerLevel = IloggerLevel ?? "warn"; this.logger = new Logger(loggerEnabled, loggerLevel, 'ValidationUtils'); } @@ -191,7 +191,7 @@ class ValidationUtils { continue; } - if("default" in v){ + if(v && typeof v === "object" && "default" in v){ //put the default value in the object newObj[k] = v.default; continue; @@ -496,6 +496,11 @@ class ValidationUtils { 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()); //remove caps diff --git a/src/measurements/Measurement.js b/src/measurements/Measurement.js index 0848657..da35e51 100644 --- a/src/measurements/Measurement.js +++ b/src/measurements/Measurement.js @@ -69,8 +69,10 @@ class Measurement { } getLaggedValue(lag){ - if(this.values.length <= lag) return null; - return this.values[this.values.length - lag]; + if (lag < 0) throw new Error('lag must be >= 0'); + const index = this.values.length - 1 - lag; + if (index < 0) return null; + return this.values[index]; } getLaggedSample(lag){ @@ -178,7 +180,7 @@ class Measurement { try { 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( diff --git a/src/measurements/MeasurementContainer.js b/src/measurements/MeasurementContainer.js index d67786b..21412d0 100644 --- a/src/measurements/MeasurementContainer.js +++ b/src/measurements/MeasurementContainer.js @@ -332,7 +332,7 @@ class MeasurementContainer { // Convert if needed if (measurement.unit && requestedUnit !== measurement.unit) { 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 sample.value = convertedValue ; sample.unit = requestedUnit; @@ -364,7 +364,7 @@ class MeasurementContainer { // Convert if needed if (measurement.unit && requestedUnit !== measurement.unit) { 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 sample.value = convertedValue ; sample.unit = requestedUnit; @@ -619,16 +619,16 @@ class MeasurementContainer { } _convertPositionNum2Str(positionValue) { - switch (positionValue) { - case 0: + if (positionValue === 0) { 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}`); } } diff --git a/src/menu/index.js b/src/menu/index.js index 7fb8ee2..a216548 100644 --- a/src/menu/index.js +++ b/src/menu/index.js @@ -36,7 +36,21 @@ class MenuManager { try { 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) { console.warn(`Unable to determine softwareType for ${nodeName}: ${error.message}`); return nodeName; diff --git a/test/00-barrel-contract.test.js b/test/00-barrel-contract.test.js new file mode 100644 index 0000000..ea7008e --- /dev/null +++ b/test/00-barrel-contract.test.js @@ -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'); +}); diff --git a/test/assertions.test.js b/test/assertions.test.js new file mode 100644 index 0000000..48ce4c0 --- /dev/null +++ b/test/assertions.test.js @@ -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/); +}); diff --git a/test/child-registration-utils.test.js b/test/child-registration-utils.test.js new file mode 100644 index 0000000..ca2ea75 --- /dev/null +++ b/test/child-registration-utils.test.js @@ -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); +}); diff --git a/test/config-manager.test.js b/test/config-manager.test.js new file mode 100644 index 0000000..e7147a1 --- /dev/null +++ b/test/config-manager.test.js @@ -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/); +}); diff --git a/test/config-utils.test.js b/test/config-utils.test.js new file mode 100644 index 0000000..cee7b79 --- /dev/null +++ b/test/config-utils.test.js @@ -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); +}); diff --git a/test/gravity.test.js b/test/gravity.test.js new file mode 100644 index 0000000..f954a72 --- /dev/null +++ b/test/gravity.test.js @@ -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); +}); diff --git a/test/helpers.js b/test/helpers.js new file mode 100644 index 0000000..870df5e --- /dev/null +++ b/test/helpers.js @@ -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, +}; diff --git a/test/logger.test.js b/test/logger.test.js new file mode 100644 index 0000000..c3a3f8c --- /dev/null +++ b/test/logger.test.js @@ -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'); + }); +}); diff --git a/test/measurement-builder.test.js b/test/measurement-builder.test.js new file mode 100644 index 0000000..0ae553a --- /dev/null +++ b/test/measurement-builder.test.js @@ -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); +}); diff --git a/test/measurement-container-core.test.js b/test/measurement-container-core.test.js new file mode 100644 index 0000000..2a4554a --- /dev/null +++ b/test/measurement-container-core.test.js @@ -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'); +}); diff --git a/test/measurement.test.js b/test/measurement.test.js new file mode 100644 index 0000000..a456642 --- /dev/null +++ b/test/measurement.test.js @@ -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]); +}); diff --git a/test/menu-manager.test.js b/test/menu-manager.test.js new file mode 100644 index 0000000..e80e9d7 --- /dev/null +++ b/test/menu-manager.test.js @@ -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'); +}); diff --git a/test/nrmse.test.js b/test/nrmse.test.js new file mode 100644 index 0000000..a52f053 --- /dev/null +++ b/test/nrmse.test.js @@ -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); +}); diff --git a/test/output-utils.test.js b/test/output-utils.test.js new file mode 100644 index 0000000..d901b2d --- /dev/null +++ b/test/output-utils.test.js @@ -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); +}); diff --git a/test/validation-utils.test.js b/test/validation-utils.test.js new file mode 100644 index 0000000..7a5c71e --- /dev/null +++ b/test/validation-utils.test.js @@ -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)); +});