1305 lines
44 KiB
JavaScript
1305 lines
44 KiB
JavaScript
'use strict';
|
|
|
|
var tap = require('tap');
|
|
var nodeOsc = require('node-osc');
|
|
|
|
tap.test('encode and decode: simple message', (t) => {
|
|
const message = new nodeOsc.Message('/test', 42, 'hello', 3.14);
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
t.ok(Buffer.isBuffer(buffer), 'encode should return a Buffer');
|
|
|
|
const decoded = nodeOsc.decode(buffer);
|
|
t.equal(decoded.oscType, 'message', 'should decode as message');
|
|
t.equal(decoded.address, '/test', 'should preserve address');
|
|
t.equal(decoded.args.length, 3, 'should have 3 arguments');
|
|
t.equal(decoded.args[0].value, 42, 'should preserve integer argument');
|
|
t.equal(decoded.args[1].value, 'hello', 'should preserve string argument');
|
|
t.ok(Math.abs(decoded.args[2].value - 3.14) < 0.001, 'should preserve float argument');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: bundle', (t) => {
|
|
const bundle = new nodeOsc.Bundle(
|
|
['/test1', 100],
|
|
['/test2', 'world']
|
|
);
|
|
|
|
const buffer = nodeOsc.encode(bundle);
|
|
t.ok(Buffer.isBuffer(buffer), 'encode should return a Buffer');
|
|
|
|
const decoded = nodeOsc.decode(buffer);
|
|
t.equal(decoded.oscType, 'bundle', 'should decode as bundle');
|
|
t.equal(decoded.timetag, 0, 'should have timetag of 0');
|
|
t.equal(decoded.elements.length, 2, 'should have 2 elements');
|
|
t.equal(decoded.elements[0].oscType, 'message', 'first element should be message');
|
|
t.equal(decoded.elements[0].address, '/test1', 'first element should have correct address');
|
|
t.equal(decoded.elements[0].args[0].value, 100, 'first element should have correct argument');
|
|
t.equal(decoded.elements[1].address, '/test2', 'second element should have correct address');
|
|
t.equal(decoded.elements[1].args[0].value, 'world', 'second element should have correct argument');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: nested bundle', (t) => {
|
|
const innerBundle = new nodeOsc.Bundle(['/inner', 42]);
|
|
const outerBundle = new nodeOsc.Bundle(10, ['/outer', 'test']);
|
|
outerBundle.append(innerBundle);
|
|
|
|
const buffer = nodeOsc.encode(outerBundle);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.oscType, 'bundle', 'should decode as bundle');
|
|
t.ok(decoded.timetag > 0, 'should have non-zero timetag');
|
|
t.equal(decoded.elements.length, 2, 'should have 2 elements');
|
|
t.equal(decoded.elements[0].oscType, 'message', 'first element should be message');
|
|
t.equal(decoded.elements[1].oscType, 'bundle', 'second element should be bundle');
|
|
t.equal(decoded.elements[1].elements[0].address, '/inner', 'nested bundle should preserve address');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: round-trip with boolean values', (t) => {
|
|
const message = new nodeOsc.Message('/booleans');
|
|
message.append(true);
|
|
message.append(false);
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, true, 'should preserve true value');
|
|
t.equal(decoded.args[1].value, false, 'should preserve false value');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: round-trip with blob', (t) => {
|
|
const blobData = Buffer.from([0x01, 0x02, 0x03, 0x04]);
|
|
const message = new nodeOsc.Message('/blob', { type: 'blob', value: blobData });
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Buffer.isBuffer(decoded.args[0].value), 'should decode as Buffer');
|
|
t.same(decoded.args[0].value, blobData, 'should preserve blob data');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: message with mixed types', (t) => {
|
|
const message = new nodeOsc.Message('/mixed');
|
|
message.append(42); // integer
|
|
message.append(3.14); // float
|
|
message.append('hello'); // string
|
|
message.append(true); // boolean
|
|
message.append({ type: 'blob', value: Buffer.from([0x01, 0x02]) }); // blob
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args.length, 5, 'should have 5 arguments');
|
|
t.equal(decoded.args[0].value, 42, 'should preserve integer');
|
|
t.ok(Math.abs(decoded.args[1].value - 3.14) < 0.001, 'should preserve float');
|
|
t.equal(decoded.args[2].value, 'hello', 'should preserve string');
|
|
t.equal(decoded.args[3].value, true, 'should preserve boolean');
|
|
t.ok(Buffer.isBuffer(decoded.args[4].value), 'should preserve blob as Buffer');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('decode: raw buffer from external source', (t) => {
|
|
// Simulate receiving a raw OSC message buffer from an external source
|
|
// This is a hand-crafted OSC message for "/test" with integer 123
|
|
const rawBuffer = Buffer.from([
|
|
0x2f, 0x74, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, // "/test\0\0\0"
|
|
0x2c, 0x69, 0x00, 0x00, // ",i\0\0"
|
|
0x00, 0x00, 0x00, 0x7b // 123
|
|
]);
|
|
|
|
const decoded = nodeOsc.decode(rawBuffer);
|
|
|
|
t.equal(decoded.oscType, 'message', 'should decode as message');
|
|
t.equal(decoded.address, '/test', 'should decode correct address');
|
|
t.equal(decoded.args.length, 1, 'should have 1 argument');
|
|
t.equal(decoded.args[0].value, 123, 'should decode correct value');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode: message for external consumption', (t) => {
|
|
const message = new nodeOsc.Message('/oscillator/frequency', 440);
|
|
const buffer = nodeOsc.encode(message);
|
|
|
|
// Verify buffer is suitable for sending over network
|
|
t.ok(Buffer.isBuffer(buffer), 'should be a Buffer');
|
|
t.ok(buffer.length > 0, 'should have non-zero length');
|
|
|
|
// Verify it can be decoded back
|
|
const decoded = nodeOsc.decode(buffer);
|
|
t.equal(decoded.address, '/oscillator/frequency', 'should preserve address');
|
|
t.equal(decoded.args[0].value, 440, 'should preserve value');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: MIDI messages with Buffer', (t) => {
|
|
const midiBuffer = Buffer.from([0x00, 0x90, 0x3C, 0x7F]); // port 0, note on, note 60, velocity 127
|
|
const message = new nodeOsc.Message('/midi', { type: 'midi', value: midiBuffer });
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Buffer.isBuffer(decoded.args[0].value), 'should decode as Buffer');
|
|
t.same(decoded.args[0].value, midiBuffer, 'should preserve MIDI data');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: MIDI messages with object', (t) => {
|
|
const midiObj = { port: 0, status: 0x90, data1: 0x3C, data2: 0x7F };
|
|
const message = new nodeOsc.Message('/midi', { type: 'midi', value: midiObj });
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Buffer.isBuffer(decoded.args[0].value), 'should decode as Buffer');
|
|
t.equal(decoded.args[0].value[0], 0, 'port should be 0');
|
|
t.equal(decoded.args[0].value[1], 0x90, 'status should be 0x90');
|
|
t.equal(decoded.args[0].value[2], 0x3C, 'data1 should be 0x3C');
|
|
t.equal(decoded.args[0].value[3], 0x7F, 'data2 should be 0x7F');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: null type tag', (t) => {
|
|
// Create a message with null value by manually constructing the buffer
|
|
// OSC format: address + type tags + no data for 'N' type
|
|
const addressBuf = Buffer.from('/test\0\0\0', 'utf8');
|
|
const typeTagsBuf = Buffer.from(',N\0\0', 'utf8');
|
|
const buffer = Buffer.concat([addressBuf, typeTagsBuf]);
|
|
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.oscType, 'message', 'should decode as message');
|
|
t.equal(decoded.address, '/test', 'should have correct address');
|
|
t.equal(decoded.args[0].value, null, 'should decode null value');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode: error on unknown argument type', (t) => {
|
|
const message = new nodeOsc.Message('/test', { type: 'unknown', value: 42 });
|
|
|
|
t.throws(() => {
|
|
nodeOsc.encode(message);
|
|
}, /Unknown argument type: unknown/, 'should throw on unknown type');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode: error on unencodable argument', (t) => {
|
|
const message = new nodeOsc.Message('/test');
|
|
message.args.push(() => {}); // Functions can't be encoded
|
|
|
|
t.throws(() => {
|
|
nodeOsc.encode(message);
|
|
}, /Don't know how to encode argument/, 'should throw on unencodable argument');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('decode: error on unknown type tag', (t) => {
|
|
// Create a message with an unknown type tag 'X'
|
|
const addressBuf = Buffer.from('/test\0\0\0', 'utf8');
|
|
const typeTagsBuf = Buffer.from(',X\0\0', 'utf8');
|
|
const buffer = Buffer.concat([addressBuf, typeTagsBuf]);
|
|
|
|
t.throws(() => {
|
|
nodeOsc.decode(buffer);
|
|
}, /I don't understand the argument code X/, 'should throw on unknown type tag');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode: MIDI error on wrong buffer length', (t) => {
|
|
const wrongBuffer = Buffer.from([0x90, 0x3C]); // Only 2 bytes, should be 4
|
|
const message = new nodeOsc.Message('/midi', { type: 'midi', value: wrongBuffer });
|
|
|
|
t.throws(() => {
|
|
nodeOsc.encode(message);
|
|
}, /MIDI message must be exactly 4 bytes/, 'should throw on wrong buffer length');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode: MIDI error on invalid value type', (t) => {
|
|
const message = new nodeOsc.Message('/midi', { type: 'midi', value: 'not a buffer' });
|
|
|
|
t.throws(() => {
|
|
nodeOsc.encode(message);
|
|
}, /MIDI value must be a 4-byte Buffer/, 'should throw on invalid MIDI value');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('decode: MIDI error on insufficient buffer', (t) => {
|
|
// Create a message with MIDI type tag but not enough data
|
|
const addressBuf = Buffer.from('/test\0\0\0', 'utf8');
|
|
const typeTagsBuf = Buffer.from(',m\0\0', 'utf8');
|
|
const dataBuf = Buffer.from([0x90, 0x3C]); // Only 2 bytes, should be 4
|
|
const buffer = Buffer.concat([addressBuf, typeTagsBuf, dataBuf]);
|
|
|
|
t.throws(() => {
|
|
nodeOsc.decode(buffer);
|
|
}, /Not enough bytes for MIDI message/, 'should throw on insufficient MIDI data');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: Buffer argument (inferred blob type)', (t) => {
|
|
const bufferArg = Buffer.from([0x01, 0x02, 0x03, 0x04]);
|
|
const message = new nodeOsc.Message('/buffer', bufferArg);
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Buffer.isBuffer(decoded.args[0].value), 'should decode as Buffer');
|
|
t.same(decoded.args[0].value, bufferArg, 'should preserve Buffer data');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: double type (treated as float)', (t) => {
|
|
const message = new nodeOsc.Message('/double', { type: 'double', value: 3.141592653589793 });
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
// Doubles are encoded as floats in OSC 1.0, so precision is reduced
|
|
t.ok(Math.abs(decoded.args[0].value - 3.141592653589793) < 0.001, 'should preserve approximate value');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('decode: error on malformed type tags (no leading comma)', (t) => {
|
|
// Create a message with malformed type tags (missing comma)
|
|
const addressBuf = Buffer.from('/test\0\0\0', 'utf8');
|
|
const typeTagsBuf = Buffer.from('iXX\0', 'utf8'); // Should start with comma
|
|
const buffer = Buffer.concat([addressBuf, typeTagsBuf]);
|
|
|
|
t.throws(() => {
|
|
nodeOsc.decode(buffer);
|
|
}, /Malformed Packet/, 'should throw on malformed type tags');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('decode: error on truncated blob data', (t) => {
|
|
const addressBuf = Buffer.from('/b\0\0', 'ascii');
|
|
const typeTagsBuf = Buffer.from(',b\0\0', 'ascii');
|
|
const lengthBuf = Buffer.alloc(4);
|
|
lengthBuf.writeInt32BE(4, 0);
|
|
const dataBuf = Buffer.from([0x01, 0x02]);
|
|
const buffer = Buffer.concat([addressBuf, typeTagsBuf, lengthBuf, dataBuf]);
|
|
|
|
t.throws(() => {
|
|
nodeOsc.decode(buffer);
|
|
}, /Malformed Packet: Not enough bytes for blob/, 'should throw when blob data is truncated');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('decode: error on missing blob padding', (t) => {
|
|
const addressBuf = Buffer.from('/b\0\0', 'ascii');
|
|
const typeTagsBuf = Buffer.from(',b\0\0', 'ascii');
|
|
const lengthBuf = Buffer.alloc(4);
|
|
lengthBuf.writeInt32BE(3, 0);
|
|
const dataBuf = Buffer.from([0x01, 0x02, 0x03]);
|
|
const buffer = Buffer.concat([addressBuf, typeTagsBuf, lengthBuf, dataBuf]);
|
|
|
|
t.throws(() => {
|
|
nodeOsc.decode(buffer);
|
|
}, /Malformed Packet: Not enough bytes for blob padding/, 'should throw when blob padding is missing');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('decode: error on truncated float32', (t) => {
|
|
const addressBuf = Buffer.from('/f\0\0', 'ascii');
|
|
const typeTagsBuf = Buffer.from(',f\0\0', 'ascii');
|
|
const dataBuf = Buffer.from([0x3f, 0x80, 0x00]);
|
|
const buffer = Buffer.concat([addressBuf, typeTagsBuf, dataBuf]);
|
|
|
|
t.throws(() => {
|
|
nodeOsc.decode(buffer);
|
|
}, /Malformed Packet: Not enough bytes for float32/, 'should throw when float32 data is truncated');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('decode: error on truncated int32', (t) => {
|
|
const addressBuf = Buffer.from('/i\0\0', 'ascii');
|
|
const typeTagsBuf = Buffer.from(',i\0\0', 'ascii');
|
|
const dataBuf = Buffer.from([0x00, 0x01]);
|
|
const buffer = Buffer.concat([addressBuf, typeTagsBuf, dataBuf]);
|
|
|
|
t.throws(() => {
|
|
nodeOsc.decode(buffer);
|
|
}, /Malformed Packet: Not enough bytes for int32/, 'should throw when int32 data is truncated');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('decode: error on negative blob length', (t) => {
|
|
const addressBuf = Buffer.from('/b\0\0', 'ascii');
|
|
const typeTagsBuf = Buffer.from(',b\0\0', 'ascii');
|
|
const lengthBuf = Buffer.alloc(4);
|
|
lengthBuf.writeInt32BE(-1, 0);
|
|
const buffer = Buffer.concat([addressBuf, typeTagsBuf, lengthBuf]);
|
|
|
|
t.throws(() => {
|
|
nodeOsc.decode(buffer);
|
|
}, /Malformed Packet: Invalid blob length/, 'should throw when blob length is negative');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('decode: error on bundle element size overflow', (t) => {
|
|
const bundleHeader = Buffer.from('#bundle\0', 'ascii');
|
|
const timetag = Buffer.alloc(8);
|
|
const sizeBuf = Buffer.alloc(4);
|
|
sizeBuf.writeInt32BE(0, 0);
|
|
const buffer = Buffer.concat([bundleHeader, timetag, sizeBuf]);
|
|
|
|
t.throws(() => {
|
|
nodeOsc.decode(buffer);
|
|
}, /Malformed Packet/, 'should throw when bundle element size is invalid');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('decode: error on truncated bundle timetag', (t) => {
|
|
const bundleHeader = Buffer.from('#bundle\0', 'ascii');
|
|
const timetag = Buffer.alloc(4);
|
|
const buffer = Buffer.concat([bundleHeader, timetag]);
|
|
|
|
t.throws(() => {
|
|
nodeOsc.decode(buffer);
|
|
}, /Malformed Packet: Not enough bytes for timetag/, 'should throw when timetag is truncated');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: nested bundle with message and bundle elements', (t) => {
|
|
// Test the else branch in encodeBundleToBuffer for message elements
|
|
const innerBundle = new nodeOsc.Bundle(['/inner/message', 123]);
|
|
const outerBundle = new nodeOsc.Bundle(['/outer/message', 'test'], innerBundle);
|
|
|
|
const buffer = nodeOsc.encode(outerBundle);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.oscType, 'bundle', 'should be a bundle');
|
|
t.equal(decoded.elements.length, 2, 'should have 2 elements');
|
|
t.equal(decoded.elements[0].oscType, 'message', 'first element should be message');
|
|
t.equal(decoded.elements[1].oscType, 'bundle', 'second element should be bundle');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: MIDI with all zero values', (t) => {
|
|
// Test MIDI encoding with object where all values are 0 or falsy (covers || branches)
|
|
const message = new nodeOsc.Message('/midi', {
|
|
type: 'midi',
|
|
value: {
|
|
port: 0,
|
|
status: 0,
|
|
data1: 0,
|
|
data2: 0
|
|
}
|
|
});
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Buffer.isBuffer(decoded.args[0].value), 'should decode as Buffer');
|
|
t.equal(decoded.args[0].value[0], 0, 'port should be 0');
|
|
t.equal(decoded.args[0].value[1], 0, 'status should be 0');
|
|
t.equal(decoded.args[0].value[2], 0, 'data1 should be 0');
|
|
t.equal(decoded.args[0].value[3], 0, 'data2 should be 0');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: MIDI with undefined values defaulting', (t) => {
|
|
// Test MIDI encoding where values are undefined (triggers || default to 0)
|
|
const message = new nodeOsc.Message('/midi', {
|
|
type: 'midi',
|
|
value: {
|
|
// All undefined, should default to 0
|
|
}
|
|
});
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Buffer.isBuffer(decoded.args[0].value), 'should decode as Buffer');
|
|
t.equal(decoded.args[0].value[0], 0, 'port should default to 0');
|
|
t.equal(decoded.args[0].value[1], 0, 'status should default to 0');
|
|
t.equal(decoded.args[0].value[2], 0, 'data1 should default to 0');
|
|
t.equal(decoded.args[0].value[3], 0, 'data2 should default to 0');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: MIDI with only port set', (t) => {
|
|
// Test MIDI where only port is set, others should default
|
|
const message = new nodeOsc.Message('/midi', {
|
|
type: 'midi',
|
|
value: {
|
|
port: 3
|
|
// status, data1, data2 undefined
|
|
}
|
|
});
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value[0], 3, 'port should be 3');
|
|
t.equal(decoded.args[0].value[1], 0, 'status should default to 0');
|
|
t.equal(decoded.args[0].value[2], 0, 'data1 should default to 0');
|
|
t.equal(decoded.args[0].value[3], 0, 'data2 should default to 0');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: MIDI with only status set', (t) => {
|
|
// Test MIDI where only status is set
|
|
const message = new nodeOsc.Message('/midi', {
|
|
type: 'midi',
|
|
value: {
|
|
status: 0x90
|
|
// port, data1, data2 undefined
|
|
}
|
|
});
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value[0], 0, 'port should default to 0');
|
|
t.equal(decoded.args[0].value[1], 0x90, 'status should be 0x90');
|
|
t.equal(decoded.args[0].value[2], 0, 'data1 should default to 0');
|
|
t.equal(decoded.args[0].value[3], 0, 'data2 should default to 0');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: MIDI with only data1 set', (t) => {
|
|
// Test MIDI where only data1 is set
|
|
const message = new nodeOsc.Message('/midi', {
|
|
type: 'midi',
|
|
value: {
|
|
data1: 0x3C
|
|
// port, status, data2 undefined
|
|
}
|
|
});
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value[0], 0, 'port should default to 0');
|
|
t.equal(decoded.args[0].value[1], 0, 'status should default to 0');
|
|
t.equal(decoded.args[0].value[2], 0x3C, 'data1 should be 0x3C');
|
|
t.equal(decoded.args[0].value[3], 0, 'data2 should default to 0');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: MIDI with only data2 set', (t) => {
|
|
// Test MIDI where only data2 is set
|
|
const message = new nodeOsc.Message('/midi', {
|
|
type: 'midi',
|
|
value: {
|
|
data2: 0x7F
|
|
// port, status, data1 undefined
|
|
}
|
|
});
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value[0], 0, 'port should default to 0');
|
|
t.equal(decoded.args[0].value[1], 0, 'status should default to 0');
|
|
t.equal(decoded.args[0].value[2], 0, 'data1 should default to 0');
|
|
t.equal(decoded.args[0].value[3], 0x7F, 'data2 should be 0x7F');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: explicit integer type name', (t) => {
|
|
// Test with 'integer' type name (alternate for 'i')
|
|
const message = new nodeOsc.Message('/test', { type: 'integer', value: 999 });
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 999, 'should encode and decode integer');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: explicit float type name', (t) => {
|
|
// Test with 'float' type name (alternate for 'f')
|
|
const message = new nodeOsc.Message('/test', { type: 'float', value: 2.718 });
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Math.abs(decoded.args[0].value - 2.718) < 0.001, 'should encode and decode float');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: explicit string type name', (t) => {
|
|
// Test with 'string' type name (alternate for 's')
|
|
const message = new nodeOsc.Message('/test', { type: 'string', value: 'alternate' });
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 'alternate', 'should encode and decode string');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: explicit blob type name', (t) => {
|
|
// Test with 'blob' type name (alternate for 'b')
|
|
const blobData = Buffer.from([0xDE, 0xAD, 0xBE, 0xEF]);
|
|
const message = new nodeOsc.Message('/test', { type: 'blob', value: blobData });
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Buffer.isBuffer(decoded.args[0].value), 'should decode as Buffer');
|
|
t.same(decoded.args[0].value, blobData, 'should preserve blob data');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: explicit boolean type name', (t) => {
|
|
// Test with 'boolean' type name (alternate for 'T'/'F')
|
|
const message1 = new nodeOsc.Message('/test', { type: 'boolean', value: true });
|
|
const message2 = new nodeOsc.Message('/test', { type: 'boolean', value: false });
|
|
|
|
const buffer1 = nodeOsc.encode(message1);
|
|
const buffer2 = nodeOsc.encode(message2);
|
|
const decoded1 = nodeOsc.decode(buffer1);
|
|
const decoded2 = nodeOsc.decode(buffer2);
|
|
|
|
t.equal(decoded1.args[0].value, true, 'should encode and decode boolean true');
|
|
t.equal(decoded2.args[0].value, false, 'should encode and decode boolean false');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: explicit T type tag', (t) => {
|
|
// Test with 'T' type tag directly (not 'boolean')
|
|
const message = new nodeOsc.Message('/test', { type: 'T', value: true });
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, true, 'should encode and decode true with T tag');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: explicit double type name', (t) => {
|
|
// Test with 'double' type name
|
|
const message = new nodeOsc.Message('/test', { type: 'double', value: 3.141592653589793 });
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Math.abs(decoded.args[0].value - 3.141592653589793) < 0.001, 'should encode double as float');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: raw message with float type', (t) => {
|
|
// Send raw message object directly to hit the 'float' case label
|
|
const rawMessage = {
|
|
oscType: 'message',
|
|
address: '/float',
|
|
args: [{ type: 'float', value: 1.414 }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(rawMessage);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Math.abs(decoded.args[0].value - 1.414) < 0.001, 'should handle float type');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: raw message with blob type', (t) => {
|
|
// Send raw message object directly to hit the 'blob' case label
|
|
const blobData = Buffer.from([1, 2, 3, 4]);
|
|
const rawMessage = {
|
|
oscType: 'message',
|
|
address: '/blob',
|
|
args: [{ type: 'blob', value: blobData }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(rawMessage);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.same(decoded.args[0].value, blobData, 'should handle blob type');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: raw message with double type', (t) => {
|
|
// Send raw message object directly to hit the 'double' case label
|
|
const rawMessage = {
|
|
oscType: 'message',
|
|
address: '/double',
|
|
args: [{ type: 'double', value: 2.71828 }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(rawMessage);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Math.abs(decoded.args[0].value - 2.71828) < 0.001, 'should handle double type');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: raw message with T type', (t) => {
|
|
// Send raw message object directly to hit the 'T' case label
|
|
const rawMessage = {
|
|
oscType: 'message',
|
|
address: '/bool',
|
|
args: [{ type: 'T', value: true }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(rawMessage);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, true, 'should handle T type');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: raw message with midi type', (t) => {
|
|
// Send raw message object directly to hit the 'midi' case label
|
|
const midiData = Buffer.from([0x01, 0x90, 0x3C, 0x7F]);
|
|
const rawMessage = {
|
|
oscType: 'message',
|
|
address: '/midi',
|
|
args: [{ type: 'midi', value: midiData }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(rawMessage);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Buffer.isBuffer(decoded.args[0].value), 'should handle midi type');
|
|
t.equal(decoded.args[0].value.length, 4, 'should have 4 bytes');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: blob with length multiple of 4', (t) => {
|
|
// Test blob where length % 4 === 0 (padding === 4, should use 0 padding)
|
|
const blobData = Buffer.from([0x00, 0x01, 0x02, 0x03]); // length 4, multiple of 4
|
|
const message = new nodeOsc.Message('/blob4', { type: 'b', value: blobData });
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.same(decoded.args[0].value, blobData, 'should handle blob with length multiple of 4');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: blob with length not multiple of 4', (t) => {
|
|
// Test blob where length % 4 !== 0 (padding < 4)
|
|
const blobData = Buffer.from([0xAA, 0xBB, 0xCC]); // length 3, not multiple of 4
|
|
const message = new nodeOsc.Message('/blob3', { type: 'b', value: blobData });
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.same(decoded.args[0].value, blobData, 'should handle blob with length not multiple of 4');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: blob with length 1', (t) => {
|
|
// Test blob with length 1 (padding will be 3)
|
|
const blobData = Buffer.from([0xFF]); // length 1
|
|
const message = new nodeOsc.Message('/blob1', { type: 'b', value: blobData });
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.same(decoded.args[0].value, blobData, 'should handle blob with length 1');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: blob with length 2', (t) => {
|
|
// Test blob with length 2 (padding will be 2)
|
|
const blobData = Buffer.from([0xDE, 0xAD]); // length 2
|
|
const message = new nodeOsc.Message('/blob2', { type: 'b', value: blobData });
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.same(decoded.args[0].value, blobData, 'should handle blob with length 2');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: blob with length 8', (t) => {
|
|
// Test blob with length 8 (multiple of 4, padding === 4)
|
|
const blobData = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); // length 8
|
|
const message = new nodeOsc.Message('/blob8', { type: 'b', value: blobData });
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.same(decoded.args[0].value, blobData, 'should handle blob with length 8');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: MUST hit float case label directly', (t) => {
|
|
// This test MUST hit the 'float' case label (line 139) in dist/lib/osc.js
|
|
// We import from 'node-osc' which uses dist/lib/osc.js in CJS
|
|
// We use type: 'float' explicitly (not 'f')
|
|
const rawMessage = {
|
|
oscType: 'message',
|
|
address: '/float-label-test',
|
|
args: [{ type: 'float', value: 123.456 }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(rawMessage);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Math.abs(decoded.args[0].value - 123.456) < 0.001, 'should encode/decode float');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: MUST hit double case label directly', (t) => {
|
|
// This test MUST hit the 'double' case label (line 148) in dist/lib/osc.js
|
|
const rawMessage = {
|
|
oscType: 'message',
|
|
address: '/double-label-test',
|
|
args: [{ type: 'double', value: 987.654 }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(rawMessage);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Math.abs(decoded.args[0].value - 987.654) < 0.001, 'should encode/decode double');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: MUST hit midi case label directly', (t) => {
|
|
// This test MUST hit the 'midi' case label (line 155) in dist/lib/osc.js
|
|
const midiBuffer = Buffer.from([0x02, 0xA0, 0x50, 0x60]);
|
|
const rawMessage = {
|
|
oscType: 'message',
|
|
address: '/midi-label-test',
|
|
args: [{ type: 'midi', value: midiBuffer }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(rawMessage);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Buffer.isBuffer(decoded.args[0].value), 'should encode/decode midi');
|
|
t.equal(decoded.args[0].value.length, 4, 'should have 4 bytes');
|
|
t.end();
|
|
});
|
|
|
|
// Tests for explicit type name coverage (both short and long forms)
|
|
tap.test('encode and decode: type "f" (short form for float)', (t) => {
|
|
const msg = new nodeOsc.Message('/test', { type: 'f', value: 1.23 });
|
|
const buffer = nodeOsc.encode(msg);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Math.abs(decoded.args[0].value - 1.23) < 0.001);
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: type "float" (long form)', (t) => {
|
|
const msg = new nodeOsc.Message('/test', { type: 'float', value: 3.14 });
|
|
const buffer = nodeOsc.encode(msg);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Math.abs(decoded.args[0].value - 3.14) < 0.001);
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: type "d" (short form for double)', (t) => {
|
|
const msg = new nodeOsc.Message('/test', { type: 'd', value: 4.56 });
|
|
const buffer = nodeOsc.encode(msg);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Math.abs(decoded.args[0].value - 4.56) < 0.001);
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: type "m" (short form for MIDI)', (t) => {
|
|
const msg = new nodeOsc.Message('/test', { type: 'm', value: Buffer.from([5, 6, 7, 8]) });
|
|
const buffer = nodeOsc.encode(msg);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Buffer.isBuffer(decoded.args[0].value));
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: type "midi" (long form)', (t) => {
|
|
const msg = new nodeOsc.Message('/test', { type: 'midi', value: Buffer.from([1, 2, 3, 4]) });
|
|
const buffer = nodeOsc.encode(msg);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Buffer.isBuffer(decoded.args[0].value));
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: type "i" (short form for integer)', (t) => {
|
|
const msg = new nodeOsc.Message('/test', { type: 'i', value: 42 });
|
|
const buffer = nodeOsc.encode(msg);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 42);
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: type "integer" (long form)', (t) => {
|
|
const msg = new nodeOsc.Message('/test', { type: 'integer', value: 999 });
|
|
const buffer = nodeOsc.encode(msg);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 999);
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: type "s" (short form for string)', (t) => {
|
|
const msg = new nodeOsc.Message('/test', { type: 's', value: 'hello' });
|
|
const buffer = nodeOsc.encode(msg);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 'hello');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: type "string" (long form)', (t) => {
|
|
const msg = new nodeOsc.Message('/test', { type: 'string', value: 'world' });
|
|
const buffer = nodeOsc.encode(msg);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 'world');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: type "b" (short form for blob)', (t) => {
|
|
const msg = new nodeOsc.Message('/test', { type: 'b', value: Buffer.from([0xAA, 0xBB]) });
|
|
const buffer = nodeOsc.encode(msg);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Buffer.isBuffer(decoded.args[0].value));
|
|
t.same(decoded.args[0].value, Buffer.from([0xAA, 0xBB]));
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: type "blob" (long form)', (t) => {
|
|
const msg = new nodeOsc.Message('/test', { type: 'blob', value: Buffer.from([0xCC, 0xDD]) });
|
|
const buffer = nodeOsc.encode(msg);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.ok(Buffer.isBuffer(decoded.args[0].value));
|
|
t.same(decoded.args[0].value, Buffer.from([0xCC, 0xDD]));
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: type "T" (explicit true)', (t) => {
|
|
const msg = new nodeOsc.Message('/test', { type: 'T', value: true });
|
|
const buffer = nodeOsc.encode(msg);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, true);
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: type "F" (explicit false)', (t) => {
|
|
const msg = new nodeOsc.Message('/test', { type: 'F', value: false });
|
|
const buffer = nodeOsc.encode(msg);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, false);
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: type "boolean" with true value', (t) => {
|
|
const msg = new nodeOsc.Message('/test', { type: 'boolean', value: true });
|
|
const buffer = nodeOsc.encode(msg);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, true);
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: type "boolean" with false value', (t) => {
|
|
const msg = new nodeOsc.Message('/test', { type: 'boolean', value: false });
|
|
const buffer = nodeOsc.encode(msg);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, false);
|
|
t.end();
|
|
});
|
|
|
|
// Tests for UTF-8 string padding to 4-byte boundaries
|
|
// The padString function ensures OSC strings are padded based on byte length
|
|
// (not character count) to handle multi-byte UTF-8 correctly
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - ASCII 1 char', (t) => {
|
|
// 1 byte + 1 null terminator = 2 bytes, needs 2 padding bytes to reach 4-byte boundary
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: 'a' }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 'a', 'should correctly encode and decode single ASCII character');
|
|
|
|
// Verify the string is properly padded in the buffer
|
|
// Address "/test" is 5 bytes + 1 null = 6 bytes, padded to 8 bytes
|
|
// Type tag ",s" is 2 bytes + 1 null = 3 bytes, padded to 4 bytes
|
|
// String "a" is 1 byte + 1 null = 2 bytes, padded to 4 bytes
|
|
const expectedMinLength = 8 + 4 + 4; // 16 bytes minimum
|
|
t.ok(buffer.length >= expectedMinLength, 'buffer should contain properly padded string');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - ASCII 2 chars', (t) => {
|
|
// 2 bytes + 1 null = 3 bytes, needs 1 padding byte to reach 4-byte boundary
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: 'ab' }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 'ab', 'should correctly encode and decode 2-byte string');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - ASCII 3 chars', (t) => {
|
|
// 3 bytes + 1 null terminator = 4 bytes, needs 0 padding (already aligned)
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: 'abc' }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 'abc', 'should correctly encode and decode 3-char ASCII string');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - ASCII 5 chars', (t) => {
|
|
// 5 bytes + 1 null terminator = 6 bytes, needs 2 padding bytes to reach 8-byte boundary
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: 'hello' }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 'hello', 'should correctly encode and decode 5-char ASCII string');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - ASCII 6 chars', (t) => {
|
|
// 6 bytes + 1 null = 7 bytes, needs 1 padding byte to reach 8-byte boundary
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: 'abcdef' }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 'abcdef', 'should correctly encode and decode 6-byte string');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - ASCII 7 chars', (t) => {
|
|
// 7 bytes + 1 null terminator = 8 bytes, needs 0 padding (already aligned)
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: 'testing' }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 'testing', 'should correctly encode and decode 7-char ASCII string');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - empty string', (t) => {
|
|
// 0 bytes + 1 null terminator = 1 byte, needs 3 padding bytes to reach 4-byte boundary
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: '' }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, '', 'should correctly encode and decode empty string');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - emoji character', (t) => {
|
|
// Emoji '😀' is 4 bytes in UTF-8 + 1 null = 5 bytes, needs 3 padding to reach 8-byte boundary
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: '😀' }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, '😀', 'should correctly encode and decode emoji character');
|
|
|
|
// Verify byte length calculation is correct
|
|
const emojiByteLength = Buffer.byteLength('😀');
|
|
t.equal(emojiByteLength, 4, 'emoji should be 4 bytes in UTF-8');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - Japanese character', (t) => {
|
|
// Japanese 'あ' is 3 bytes in UTF-8 + 1 null = 4 bytes, needs 0 padding (already aligned)
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: 'あ' }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 'あ', 'should correctly encode and decode Japanese character');
|
|
|
|
const japaneseByteLength = Buffer.byteLength('あ');
|
|
t.equal(japaneseByteLength, 3, 'Japanese character should be 3 bytes in UTF-8');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - Chinese character', (t) => {
|
|
// Chinese '中' is 3 bytes in UTF-8 + 1 null = 4 bytes, needs 0 padding (already aligned)
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: '中' }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, '中', 'should correctly encode and decode Chinese character');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - mixed ASCII and emoji', (t) => {
|
|
// 'a' (1 byte) + '😀' (4 bytes) + 'b' (1 byte) = 6 bytes + 1 null = 7 bytes
|
|
// needs 1 padding byte to reach 8-byte boundary
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: 'a😀b' }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 'a😀b', 'should correctly encode and decode mixed ASCII and emoji');
|
|
|
|
const mixedByteLength = Buffer.byteLength('a😀b');
|
|
t.equal(mixedByteLength, 6, 'mixed string should be 6 bytes in UTF-8');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - Japanese string', (t) => {
|
|
// 'こんにちは' (Hello in Japanese) - 5 characters, each 3 bytes = 15 bytes
|
|
// 15 bytes + 1 null = 16 bytes, needs 0 padding (already aligned)
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: 'こんにちは' }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 'こんにちは', 'should correctly encode and decode Japanese string');
|
|
|
|
const japaneseStringByteLength = Buffer.byteLength('こんにちは');
|
|
t.equal(japaneseStringByteLength, 15, 'Japanese string should be 15 bytes in UTF-8');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - accented characters', (t) => {
|
|
// 'café' - 4 characters but 'é' is 2 bytes in UTF-8
|
|
// 'c' (1) + 'a' (1) + 'f' (1) + 'é' (2) = 5 bytes + 1 null = 6 bytes
|
|
// needs 2 padding bytes to reach 8-byte boundary
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: 'café' }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, 'café', 'should correctly encode and decode accented string');
|
|
|
|
const accentedByteLength = Buffer.byteLength('café');
|
|
t.equal(accentedByteLength, 5, 'café should be 5 bytes in UTF-8');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - multiple strings', (t) => {
|
|
// Test multiple strings with different byte lengths in one message
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/multi',
|
|
args: [
|
|
{ type: 'string', value: 'a' }, // 1 byte + null
|
|
{ type: 'string', value: '😀' }, // 4 bytes + null
|
|
{ type: 'string', value: 'abc' }, // 3 bytes + null
|
|
{ type: 'string', value: '' } // 0 bytes + null
|
|
]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args.length, 4, 'should have 4 arguments');
|
|
t.equal(decoded.args[0].value, 'a', 'first string should be correct');
|
|
t.equal(decoded.args[1].value, '😀', 'second string should be correct');
|
|
t.equal(decoded.args[2].value, 'abc', 'third string should be correct');
|
|
t.equal(decoded.args[3].value, '', 'fourth string should be correct');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - address with emoji', (t) => {
|
|
// OSC addresses can also contain UTF-8 characters and must be properly padded
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test/😀',
|
|
args: [{ type: 'string', value: 'data' }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.address, '/test/😀', 'should correctly encode and decode address with emoji');
|
|
t.equal(decoded.args[0].value, 'data', 'should correctly encode and decode argument');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - long mixed string', (t) => {
|
|
// Test a longer string with mixed content
|
|
const longString = 'Hello 世界 🌍! Testing UTF-8 encoding with café and naïve.';
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: longString }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, longString, 'should correctly encode and decode long mixed UTF-8 string');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - special characters', (t) => {
|
|
// Test various special characters that may have different byte lengths
|
|
const specialChars = '!@#$%^&*()_+-=[]{}|;:,.<>?/~`';
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: specialChars }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, specialChars, 'should correctly encode and decode special ASCII characters');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - control characters', (t) => {
|
|
// Test control characters
|
|
const controlChars = 'line1\nline2\ttab';
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: controlChars }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, controlChars, 'should correctly encode and decode strings with newlines and tabs');
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - surrogate pairs', (t) => {
|
|
// Test various emoji that are 4-byte UTF-8 sequences
|
|
const emojis = '🎉🎊🎈🎁';
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: emojis }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, emojis, 'should correctly encode and decode multiple 4-byte emoji');
|
|
|
|
const emojisByteLength = Buffer.byteLength(emojis);
|
|
t.equal(emojisByteLength, 16, 'four 4-byte emoji should total 16 bytes');
|
|
|
|
t.end();
|
|
});
|
|
|
|
tap.test('encode and decode: UTF-8 string padding - zero-width characters', (t) => {
|
|
// Test zero-width joiner and other special Unicode characters
|
|
const zwj = 'a\u200Db'; // zero-width joiner
|
|
const message = {
|
|
oscType: 'message',
|
|
address: '/test',
|
|
args: [{ type: 'string', value: zwj }]
|
|
};
|
|
|
|
const buffer = nodeOsc.encode(message);
|
|
const decoded = nodeOsc.decode(buffer);
|
|
|
|
t.equal(decoded.args[0].value, zwj, 'should correctly encode and decode strings with zero-width characters');
|
|
t.end();
|
|
});
|