Files
MARTe-Integrated-Components/Client/udpstreamer/test/calibration.test.js
T
Martino FerrariandClaude Sonnet 4.6 5917ab1bf2 Restore calibration parity: revert unit-stripping from normaliseCal, fix CSV quoting at export
Finding 1: revert the comma/quote strip added in 3cb998c from
normaliseCal() in calibration.js. The strip broke byte-identical parity
with Go CalConfig.Normalise and C++ StreamHub::SetCalibrationEntry, both
of which only trim whitespace and cap at 16 UTF-8 bytes. Delete the
companion test that asserted the now-removed behaviour (suite returns to
18 tests).

Finding 2: fix the actual CSV-safety problem at the point of use in
exportAllCSV() in app.js. Header cells (time column and signal columns)
are now RFC 4180-quoted: wrapped in double quotes with any embedded
double quote doubled. This safely handles units or signal names that
contain commas or quotes without touching normaliseCal.

Finding 3: update two stale comments in app.js that called the trigger
threshold or rawFromNorm result 'raw' — Task 9 moved trig.threshold into
calibrated units throughout, so the comments now say 'calibrated'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-17 01:15:13 +02:00

153 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const test = require('node:test');
const assert = require('node:assert');
const C = require('../static/calibration.js');
test('baseSignalName strips an element suffix', () => {
assert.strictEqual(C.baseSignalName('Adc'), 'Adc');
assert.strictEqual(C.baseSignalName('Adc[3]'), 'Adc');
assert.strictEqual(C.baseSignalName('Adc[12]'), 'Adc');
assert.strictEqual(C.baseSignalName('A[1]B'), 'A[1]B');
assert.strictEqual(C.baseSignalName(''), '');
});
test('calKey is stable and separates the two fields', () => {
assert.strictEqual(C.calKey('a', 'b'), C.calKey('a', 'b'));
assert.notStrictEqual(C.calKey('ab', 'c'), C.calKey('a', 'bc'));
});
test('normaliseCal accepts a valid entry and fills defaults', () => {
assert.deepStrictEqual(
C.normaliseCal({source: ' wave ', signal: ' Adc ', scale: 2, offset: -1, unit: ' V '}),
{source: 'wave', signal: 'Adc', scale: 2, offset: -1, unit: 'V'});
assert.deepStrictEqual(
C.normaliseCal({source: 'wave', signal: 'Adc'}),
{source: 'wave', signal: 'Adc', scale: 1, offset: 0, unit: ''});
});
test('normaliseCal strips an element suffix from the signal name', () => {
assert.strictEqual(C.normaliseCal({source: 'w', signal: 'Adc[3]'}).signal, 'Adc');
});
test('normaliseCal truncates an over-long unit', () => {
const long = 'abcdefghijklmnopqrstuvwxyz';
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', unit: long}).unit,
long.slice(0, C.MAX_UNIT_LEN));
});
test('normaliseCal leaves short non-ASCII units untouched', () => {
// 'Ω' is U+03A9, 2 UTF-8 bytes — well within 16 bytes.
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', unit: 'Ω'}).unit, 'Ω');
// 'µs' is U+00B5 + U+0073, 3 UTF-8 bytes.
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', unit: 'µs'}).unit, 'µs');
// '°C' is U+00B0 + U+0043, 3 UTF-8 bytes.
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', unit: '°C'}).unit, '°C');
});
test('normaliseCal truncates an over-long ASCII unit to exactly 16 bytes', () => {
// 20 ASCII characters — each 1 byte, so cut at character 16.
const long = 'abcdefghijklmnopqrst'; // 20 chars
const result = C.normaliseCal({source: 'w', signal: 's', unit: long}).unit;
assert.strictEqual(result, 'abcdefghijklmnop'); // first 16 bytes/chars
assert.strictEqual(new TextEncoder().encode(result).length, 16);
});
test('normaliseCal cuts a mid-rune byte boundary back to the last complete rune', () => {
// Each 'Ω' (U+03A9) is 2 UTF-8 bytes (CE A9).
// 8 × 'Ω' = 16 bytes exactly — fits without truncation.
const fits = 'ΩΩΩΩΩΩΩΩ';
assert.strictEqual(new TextEncoder().encode(fits).length, 16);
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', unit: fits}).unit, fits);
// 9 × 'Ω' = 18 bytes. Slicing at 16 bytes lands in the middle of the 9th
// 'Ω' (only 1 of its 2 bytes is in the window) so only 8 'Ω' should survive.
// No U+FFFD replacement character must appear.
const toolong = 'ΩΩΩΩΩΩΩΩΩ';
const result = C.normaliseCal({source: 'w', signal: 's', unit: toolong}).unit;
assert.strictEqual(result, 'ΩΩΩΩΩΩΩΩ');
assert.ok(!result.includes('\uFFFD'), 'must not contain U+FFFD replacement character');
assert.strictEqual(new TextEncoder().encode(result).length, 16);
});
test('normaliseCal leaves a unit that is exactly 16 bytes ending on a complete multi-byte rune untouched', () => {
// 'abcdefgΩhijklµ' → 7 ASCII + 'Ω' (2 bytes) + 5 ASCII + 'µ' (2 bytes) = 16 bytes
const u = 'abcdefgΩhijklµ';
assert.strictEqual(new TextEncoder().encode(u).length, 16);
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', unit: u}).unit, u);
});
test('normaliseCal rejects invalid entries', () => {
assert.strictEqual(C.normaliseCal(null), null);
assert.strictEqual(C.normaliseCal({signal: 's'}), null);
assert.strictEqual(C.normaliseCal({source: 'w'}), null);
assert.strictEqual(C.normaliseCal({source: ' ', signal: 's'}), null);
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', scale: 0}), null);
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', scale: NaN}), null);
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', scale: Infinity}), null);
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', offset: NaN}), null);
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', scale: '2'}), null);
});
test('applyCal and invertCal round-trip', () => {
const cal = {scale: 0.5, offset: -1.25, unit: 'V'};
assert.strictEqual(C.applyCal(10, cal), 3.75);
assert.strictEqual(C.invertCal(3.75, cal), 10);
assert.strictEqual(C.applyCal(7, C.IDENTITY), 7);
assert.strictEqual(C.invertCal(7, C.IDENTITY), 7);
});
test('applyCal passes non-finite samples through untouched', () => {
assert.ok(Number.isNaN(C.applyCal(NaN, {scale: 2, offset: 1, unit: ''})));
});
test('calRange re-orders when the scale is negative', () => {
assert.deepStrictEqual(C.calRange(0, 10, {scale: 2, offset: 1, unit: ''}), [1, 21]);
assert.deepStrictEqual(C.calRange(0, 10, {scale: -2, offset: 1, unit: ''}), [-19, 1]);
});
test('CalTable.get returns IDENTITY for an unknown signal', () => {
const t = new C.CalTable();
assert.deepStrictEqual(t.get('w', 'Adc'), C.IDENTITY);
});
test('CalTable.get resolves an element name to its base signal', () => {
const t = new C.CalTable();
t.set({source: 'w', signal: 'Adc', scale: 3, offset: 0, unit: ''});
assert.strictEqual(t.get('w', 'Adc[7]').scale, 3);
});
test('CalTable.set stores, overwrites, and deletes identity entries', () => {
const t = new C.CalTable();
assert.strictEqual(t.set({source: 'w', signal: 'Adc', scale: 2}), true);
assert.strictEqual(t.get('w', 'Adc').scale, 2);
t.set({source: 'w', signal: 'Adc', scale: 5});
assert.strictEqual(t.get('w', 'Adc').scale, 5);
assert.strictEqual(t.list().length, 1);
// Resetting to identity removes the entry entirely.
assert.strictEqual(t.set({source: 'w', signal: 'Adc', scale: 1, offset: 0, unit: ''}), true);
assert.strictEqual(t.list().length, 0);
// An invalid entry is refused and changes nothing.
assert.strictEqual(t.set({source: 'w', signal: 'Adc', scale: 0}), false);
assert.strictEqual(t.list().length, 0);
});
test('CalTable.replaceAll drops the previous contents', () => {
const t = new C.CalTable();
t.set({source: 'w', signal: 'Old', scale: 2});
t.replaceAll([
{source: 'w', signal: 'B', scale: 2},
{source: 'w', signal: 'A', scale: 3},
{source: 'w', signal: 'Bad', scale: 0},
{source: 'w', signal: 'Ident', scale: 1, offset: 0, unit: ''},
]);
assert.deepStrictEqual(t.list().map(e => e.signal), ['A', 'B']);
});
test('CalTable.list is sorted by source then signal', () => {
const t = new C.CalTable();
t.set({source: 'z', signal: 'a', scale: 2});
t.set({source: 'a', signal: 'z', scale: 2});
t.set({source: 'a', signal: 'b', scale: 2});
assert.deepStrictEqual(t.list().map(e => e.source + '/' + e.signal),
['a/b', 'a/z', 'z/a']);
});