upgrades
This commit is contained in:
256
test/monster.specific.test.js
Normal file
256
test/monster.specific.test.js
Normal file
@@ -0,0 +1,256 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const Monster = require('../src/specificClass');
|
||||
const { MeasurementContainer } = require('generalFunctions');
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(`ok - ${name}`);
|
||||
} catch (err) {
|
||||
console.error(`not ok - ${name}`);
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function withMockedDate(iso, fn) {
|
||||
const RealDate = Date;
|
||||
let now = new RealDate(iso).getTime();
|
||||
|
||||
class MockDate extends RealDate {
|
||||
constructor(...args) {
|
||||
if (args.length === 0) {
|
||||
super(now);
|
||||
} else {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
|
||||
static now() {
|
||||
return now;
|
||||
}
|
||||
}
|
||||
|
||||
global.Date = MockDate;
|
||||
try {
|
||||
return fn({
|
||||
advance(ms) {
|
||||
now += ms;
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
global.Date = RealDate;
|
||||
}
|
||||
}
|
||||
|
||||
function buildConfig(overrides = {}) {
|
||||
return {
|
||||
general: {
|
||||
name: 'Monster Test',
|
||||
logging: { enabled: false, logLevel: 'error' }
|
||||
},
|
||||
asset: {
|
||||
emptyWeightBucket: 3
|
||||
},
|
||||
constraints: {
|
||||
samplingtime: 1,
|
||||
minVolume: 5,
|
||||
maxWeight: 23,
|
||||
nominalFlowMin: 1,
|
||||
flowMax: 10
|
||||
},
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
function parseMonsternametijdenCsv(filePath) {
|
||||
const raw = fs.readFileSync(filePath, 'utf8').trim();
|
||||
const lines = raw.split(/\r?\n/);
|
||||
const header = lines.shift();
|
||||
const columns = header.split(',');
|
||||
|
||||
return lines
|
||||
.filter((line) => line && !line.startsWith('-----------'))
|
||||
.map((line) => {
|
||||
const parts = [];
|
||||
let cur = '';
|
||||
let inQ = false;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const ch = line[i];
|
||||
if (ch === '"') {
|
||||
inQ = !inQ;
|
||||
continue;
|
||||
}
|
||||
if (ch === ',' && !inQ) {
|
||||
parts.push(cur);
|
||||
cur = '';
|
||||
} else {
|
||||
cur += ch;
|
||||
}
|
||||
}
|
||||
parts.push(cur);
|
||||
const obj = {};
|
||||
columns.forEach((col, idx) => {
|
||||
obj[col] = parts[idx];
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
test('measured + manual flow averages into effective flow', () => {
|
||||
withMockedDate('2024-10-15T00:00:00Z', ({ advance }) => {
|
||||
const monster = new Monster(buildConfig());
|
||||
|
||||
const child = {
|
||||
config: {
|
||||
general: { id: 'child-1', name: 'FlowSensor' },
|
||||
asset: { type: 'flow' }
|
||||
},
|
||||
measurements: new MeasurementContainer({
|
||||
autoConvert: true,
|
||||
defaultUnits: { flow: 'm3/h' }
|
||||
})
|
||||
};
|
||||
|
||||
monster.registerChild(child, 'measurement');
|
||||
|
||||
child.measurements
|
||||
.type('flow')
|
||||
.variant('measured')
|
||||
.position('downstream')
|
||||
.value(60, Date.now(), 'm3/h');
|
||||
|
||||
monster.handleInput('input_q', { value: 20, unit: 'm3/h' });
|
||||
|
||||
advance(1000);
|
||||
monster.tick();
|
||||
|
||||
assert.strictEqual(monster.q, 40);
|
||||
});
|
||||
});
|
||||
|
||||
test('invalid flow bounds prevent sampling start', () => {
|
||||
const monster = new Monster(buildConfig({
|
||||
constraints: {
|
||||
samplingtime: 1,
|
||||
minVolume: 5,
|
||||
maxWeight: 23,
|
||||
nominalFlowMin: 10,
|
||||
flowMax: 5
|
||||
}
|
||||
}));
|
||||
|
||||
monster.handleInput('i_start', true);
|
||||
monster.sampling_program();
|
||||
|
||||
assert.strictEqual(monster.invalidFlowBounds, true);
|
||||
assert.strictEqual(monster.running, false);
|
||||
assert.strictEqual(monster.i_start, false);
|
||||
});
|
||||
|
||||
test('flowCalc uses elapsed time to compute m3PerTick', () => {
|
||||
withMockedDate('2024-10-15T00:00:00Z', ({ advance }) => {
|
||||
const monster = new Monster(buildConfig());
|
||||
monster.q = 36; // m3/h
|
||||
|
||||
monster.flowCalc();
|
||||
assert.strictEqual(monster.m3PerTick, 0);
|
||||
|
||||
advance(10000);
|
||||
monster.flowCalc();
|
||||
|
||||
const expected = 0.1; // 36 m3/h -> 0.01 m3/s over 10s
|
||||
assert.ok(Math.abs(monster.m3PerTick - expected) < 1e-6);
|
||||
});
|
||||
});
|
||||
|
||||
test('prediction fallback uses nominalFlowMin * sampling_time when rain is stale', () => {
|
||||
const monster = new Monster(buildConfig());
|
||||
monster.nominalFlowMin = 4;
|
||||
monster.flowMax = 10;
|
||||
monster.rainMaxRef = 8;
|
||||
monster.sampling_time = 24;
|
||||
monster.lastRainUpdate = 0;
|
||||
const pred = monster.get_model_prediction();
|
||||
assert.strictEqual(pred, 96);
|
||||
});
|
||||
|
||||
test('pulses increment when running with manual flow and zero nominalFlowMin', () => {
|
||||
withMockedDate('2024-10-15T00:00:00Z', ({ advance }) => {
|
||||
const monster = new Monster(buildConfig({
|
||||
constraints: {
|
||||
samplingtime: 1,
|
||||
minVolume: 5,
|
||||
maxWeight: 23,
|
||||
nominalFlowMin: 0,
|
||||
flowMax: 6000,
|
||||
minSampleIntervalSec: 60,
|
||||
maxRainRef: 10
|
||||
}
|
||||
}));
|
||||
|
||||
monster.handleInput('input_q', { value: 200, unit: 'm3/h' });
|
||||
monster.handleInput('i_start', true);
|
||||
|
||||
for (let i = 0; i < 80; i++) {
|
||||
advance(1000);
|
||||
monster.tick();
|
||||
}
|
||||
|
||||
assert.ok(monster.sumPuls > 0);
|
||||
assert.ok(monster.bucketVol > 0);
|
||||
assert.ok(monster.missedSamples > 0);
|
||||
assert.ok(monster.getSampleCooldownMs() > 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('rain data aggregation produces totals', () => {
|
||||
const monster = new Monster(buildConfig());
|
||||
const rainPath = path.join(__dirname, 'seed_data', 'raindataFormat.json');
|
||||
const rainData = JSON.parse(fs.readFileSync(rainPath, 'utf8'));
|
||||
|
||||
monster.updateRainData(rainData);
|
||||
|
||||
assert.ok(Object.keys(monster.aggregatedOutput).length > 0);
|
||||
assert.ok(monster.sumRain >= 0);
|
||||
assert.ok(monster.avgRain >= 0);
|
||||
});
|
||||
|
||||
test('monsternametijden schedule sets next date', () => {
|
||||
withMockedDate('2024-10-15T00:00:00Z', () => {
|
||||
const monster = new Monster(buildConfig());
|
||||
const csvPath = path.join(__dirname, 'seed_data', 'monsternametijden.csv');
|
||||
const rows = parseMonsternametijdenCsv(csvPath);
|
||||
|
||||
monster.aquonSampleName = '112100';
|
||||
monster.updateMonsternametijden(rows);
|
||||
|
||||
const nextDate = monster.nextDate instanceof Date
|
||||
? monster.nextDate.getTime()
|
||||
: Number(monster.nextDate);
|
||||
|
||||
assert.ok(Number.isFinite(nextDate));
|
||||
assert.ok(nextDate > Date.now());
|
||||
});
|
||||
});
|
||||
|
||||
test('output includes pulse and flow fields', () => {
|
||||
const monster = new Monster(buildConfig());
|
||||
const output = monster.getOutput();
|
||||
|
||||
assert.ok(Object.prototype.hasOwnProperty.call(output, 'pulse'));
|
||||
assert.ok(Object.prototype.hasOwnProperty.call(output, 'q'));
|
||||
assert.ok(Object.prototype.hasOwnProperty.call(output, 'm3PerPuls'));
|
||||
assert.ok(Object.prototype.hasOwnProperty.call(output, 'm3PerPulse'));
|
||||
assert.ok(Object.prototype.hasOwnProperty.call(output, 'pulsesRemaining'));
|
||||
assert.ok(Object.prototype.hasOwnProperty.call(output, 'pulseFraction'));
|
||||
assert.ok(Object.prototype.hasOwnProperty.call(output, 'flowToNextPulseM3'));
|
||||
assert.ok(Object.prototype.hasOwnProperty.call(output, 'timeToNextPulseSec'));
|
||||
assert.ok(Object.prototype.hasOwnProperty.call(output, 'targetVolumeM3'));
|
||||
assert.ok(Object.prototype.hasOwnProperty.call(output, 'targetProgressPct'));
|
||||
assert.ok(Object.prototype.hasOwnProperty.call(output, 'targetDeltaM3'));
|
||||
assert.ok(Object.prototype.hasOwnProperty.call(output, 'predictedRateM3h'));
|
||||
});
|
||||
Reference in New Issue
Block a user