@huddly/device-api-usb
Version:
Huddly SDK device api which uses node-usb wrapper responsible for handling the transport layer of the communication and discovering the physical device/camera
726 lines • 38.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const chai_1 = __importStar(require("chai"));
const sinon_chai_1 = __importDefault(require("sinon-chai"));
const chai_as_promised_1 = __importDefault(require("chai-as-promised"));
const usb_1 = require("usb");
const transport_1 = __importDefault(require("./../src/transport"));
const sinon_1 = __importDefault(require("sinon"));
const messagepacket_1 = __importDefault(require("./../src/messagepacket"));
const crypto_1 = __importDefault(require("crypto"));
const endpoint_1 = require("usb/dist/usb/endpoint");
const rewire_1 = __importDefault(require("rewire"));
chai_1.default.should();
chai_1.default.use(sinon_chai_1.default);
chai_1.default.use(chai_as_promised_1.default);
chai_1.default.use(require('chai-things'));
const MAX_PACKET_SIZE = (16 * 1024);
const createNewMockDevice = () => {
return {
serialNumber: 'B12344678',
open: sinon_1.default.stub(),
close: sinon_1.default.stub(),
interfaces: [
{
claim: sinon_1.default.stub(),
release: sinon_1.default.stub(),
descriptor: {
bInterfaceClass: 255,
},
endpoints: [
sinon_1.default.createStubInstance(endpoint_1.InEndpoint),
sinon_1.default.createStubInstance(endpoint_1.OutEndpoint)
]
}
]
};
};
describe('UsbTransport', () => {
const rewireModule = (0, rewire_1.default)('./../lib/src/transport');
const rewireClass = rewireModule.__get__('NodeUsbTransport');
const rewiredInstance = new rewireClass();
let transport;
let mockedDevice;
beforeEach(() => {
mockedDevice = createNewMockDevice();
transport = new transport_1.default(mockedDevice);
});
describe('#init', () => __awaiter(void 0, void 0, void 0, function* () {
it('set in and out enpoints', () => __awaiter(void 0, void 0, void 0, function* () {
yield transport.init();
(0, chai_1.expect)(transport.inEndpoint).to.be.an.instanceof(endpoint_1.InEndpoint);
(0, chai_1.expect)(transport.outEndpoint).to.be.an.instanceof(endpoint_1.OutEndpoint);
(0, chai_1.expect)(transport.vscInterface).not.be.undefined;
(0, chai_1.expect)(transport.inEndpoint).not.be.undefined;
(0, chai_1.expect)(transport.outEndpoint).not.be.undefined;
}));
it('should reject when no VSC interface is found on the device', () => __awaiter(void 0, void 0, void 0, function* () {
const noVscInterfaceDevice = {
open: () => { },
close: () => { },
interfaces: [{ descriptor: { bInterfaceClass: 25555555 } }]
};
transport = new transport_1.default(noVscInterfaceDevice);
return (0, chai_1.expect)(transport.init()).to.eventually.be.rejectedWith('No VSC Interface present on the usb device!');
}));
it.skip('should close the device when claiming interface fails', () => {
const spy = sinon_1.default.spy(transport_1.default.prototype, 'close');
const unclaimableInterfaceDevice = {
open: () => { },
interfaces: [
{ claim: sinon_1.default.stub().throws({ errno: usb_1.usb.LIBUSB_ERROR_BUSY, message: 'Cant claim it!' }), descriptor: { bInterfaceClass: 255 } }
]
};
transport = new transport_1.default(unclaimableInterfaceDevice);
return (0, chai_1.expect)(transport.init()).to.eventually.be.rejectedWith('Cant claim it!').then(function () {
spy.should.have.been.called;
});
});
it('should reject with proper error message when device is occupied by a different process', () => {
const busyDevice = {
open: sinon_1.default.stub().throws({ errno: usb_1.usb.LIBUSB_ERROR_ACCESS, message: 'Cant claim it!' })
};
transport = new transport_1.default(busyDevice);
return (0, chai_1.expect)(transport.init())
.to.eventually.be.rejectedWith('Unable to claim usb interface. Please make sure the device is not used by another process!');
});
it('should reject if some other error occurs when claiming the interface', () => {
const busyDevice = {
open: sinon_1.default.stub().throws({ errno: usb_1.usb.LIBUSB_ERROR_OTHER, message: 'Cant open it!' })
};
transport = new transport_1.default(busyDevice);
return (0, chai_1.expect)(transport.init())
.to.eventually.be.rejectedWith('Cant open it!');
});
it('should ignore redundant calls', () => __awaiter(void 0, void 0, void 0, function* () {
for (let step = 0; step < 10; step++) {
yield transport.init();
}
(0, chai_1.expect)(mockedDevice.open).to.have.been.calledOnce;
}));
}));
describe('#initEventLoop', () => {
let startListenStub;
const customMaxPcktSize = 1000;
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
yield transport.init();
// @ts-ignore
transport.inEndpoint.descriptor = {
wMaxPacketSize: customMaxPcktSize
};
startListenStub = sinon_1.default.stub(transport, 'startListen');
}));
afterEach(() => {
startListenStub.restore();
});
it('should throw error if InEndpoint is missing packet size info', () => {
transport.inEndpoint.descriptor = undefined;
const badFn = () => transport.initEventLoop();
(0, chai_1.expect)(badFn).throws('InEndpoint does not contain information about max packet size!');
});
it('should start the polling with maxPacketSize defined from wMaxPacketSize', () => {
const stub = transport.inEndpoint.startPoll;
transport.initEventLoop();
(0, chai_1.expect)(stub).to.have.been.calledOnce;
(0, chai_1.expect)(stub.firstCall.args[0]).to.equal(1);
(0, chai_1.expect)(stub.firstCall.args[1]).to.equal(customMaxPcktSize);
(0, chai_1.expect)(startListenStub.callCount).to.equals(1);
});
it('should ignore redundent calls', () => {
const stub = transport.inEndpoint.startPoll;
for (let step = 0; step < 10; step++) {
transport.initEventLoop();
}
(0, chai_1.expect)(stub).to.have.been.calledOnce; // Including the call from the previous test
(0, chai_1.expect)(stub.firstCall.args[0]).to.equal(1);
(0, chai_1.expect)(stub.firstCall.args[1]).to.equal(customMaxPcktSize);
(0, chai_1.expect)(startListenStub.callCount).to.equals(1);
});
});
describe('#performHlinkHandshake', () => {
let outTransfer;
let inTransfer;
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
yield transport.init();
outTransfer = transport.outEndpoint.transfer;
inTransfer = transport.inEndpoint.transfer;
}));
after(() => {
outTransfer.restore();
inTransfer.restore();
});
it('should correctly perform hlink salutation process', () => __awaiter(void 0, void 0, void 0, function* () {
outTransfer.callsFake((message, cb) => {
cb();
});
inTransfer.callsFake((pkgSize, cb) => {
cb(undefined, Buffer.from('HLink v0'));
});
yield transport.performHlinkHandshake();
// Reset Sequence transfer
(0, chai_1.expect)(outTransfer.callCount).to.be.equal(2);
(0, chai_1.expect)(outTransfer.firstCall.args[0].compare(Buffer.alloc(0))).to.be.equal(0);
(0, chai_1.expect)(outTransfer.secondCall.args[0].compare(Buffer.alloc(1, 0x00))).to.be.equal(0);
// Read message
(0, chai_1.expect)(inTransfer.callCount).to.be.equal(1);
(0, chai_1.expect)(inTransfer.firstCall.args[0]).to.be.equal(1024);
}));
it('should fail when salutation message is not "Hlink v0"', () => __awaiter(void 0, void 0, void 0, function* () {
outTransfer.callsFake((message, cb) => {
cb();
});
inTransfer.callsFake((pkgSize, cb) => {
cb(undefined, Buffer.from('N/A'));
});
try {
yield transport.performHlinkHandshake();
(0, chai_1.expect)(true).to.equals(false);
}
catch (error) {
(0, chai_1.expect)(error).to.equal('Hlink handshake has failed! Wrong version. Expected HLink v0, got N/A.');
}
}));
});
describe('#readLoopReset', () => {
it('should reset read loop helper variables', () => {
rewiredInstance.readLoopChunks = [Buffer.from('hi')];
rewiredInstance.readLoopReset();
(0, chai_1.expect)(rewiredInstance.readLoopChunks.length).to.equal(0);
(0, chai_1.expect)(rewiredInstance.currentBufferReadSize).to.equal(0);
(0, chai_1.expect)(rewiredInstance.expectedReadBufferSize).to.equal(-1);
(0, chai_1.expect)(rewiredInstance.currentStateOfReadLoop).to.equal(rewiredInstance.READ_STATES.NEW_READ);
});
});
describe('#parseAndEmitFullyRetrievedMessage', () => {
it('should parse and emit the encoded message received from device', () => {
const encodedMsg = messagepacket_1.default.createMessage('hello-msg', Buffer.from('Greetings!'));
const emitSpy = sinon_1.default.spy();
rewiredInstance.on('hello-msg', emitSpy);
const resetLoopSpy = sinon_1.default.spy(rewiredInstance, 'readLoopReset');
rewiredInstance.readLoopChunks = [encodedMsg];
rewiredInstance.parseAndEmitFullyRetrievedMessage();
(0, chai_1.expect)(emitSpy).to.have.been.called;
(0, chai_1.expect)(emitSpy.getCall(0).args[0]['message']).to.equal('hello-msg');
(0, chai_1.expect)(resetLoopSpy).to.have.been.called;
});
});
describe('#continueReadLogic', () => {
let parseStub;
beforeEach(() => {
parseStub = sinon_1.default.stub(rewiredInstance, 'parseAndEmitFullyRetrievedMessage');
});
afterEach(() => {
parseStub.restore();
});
it('should update readloop state to pending when complete buffer is not read yet', () => {
rewiredInstance.currentBufferReadSize = 1;
rewiredInstance.expectedReadBufferSize = 2;
rewiredInstance.currentStateOfReadLoop = undefined;
rewiredInstance.continueReadLogic();
(0, chai_1.expect)(rewiredInstance.currentStateOfReadLoop).to.equal(rewiredInstance.READ_STATES.PENDING_CHUNK);
});
it('should call #parseAndEmitFullyRetrievedMessage when buffer is fully received', () => {
rewiredInstance.currentBufferReadSize = 2;
rewiredInstance.expectedReadBufferSize = 2;
rewiredInstance.currentStateOfReadLoop = rewiredInstance.READ_STATES.PENDING_CHUNK;
rewiredInstance.continueReadLogic();
(0, chai_1.expect)(parseStub).to.have.been.called;
});
});
describe('#onDataRetrievedHandler', () => {
let continueLogicStub;
beforeEach(() => {
continueLogicStub = sinon_1.default.stub(rewiredInstance, 'continueReadLogic');
rewiredInstance.readLoopChunks = [];
});
afterEach(() => {
continueLogicStub.restore();
});
describe('on new read', () => {
beforeEach(() => {
rewiredInstance.currentStateOfReadLoop = rewiredInstance.READ_STATES.NEW_READ;
});
describe('on "reset sequence" received', () => {
it('should remove data event listener', () => {
rewiredInstance.inEndpoint = {
removeListener: sinon_1.default.stub()
};
const stub = rewiredInstance.inEndpoint.removeListener;
const badFn = () => rewiredInstance.onDataRetrievedHandler(Buffer.from('hi'));
(0, chai_1.expect)(badFn).throws;
try {
badFn();
}
catch (_a) { } // call the function to make sure the stub is invoked
(0, chai_1.expect)(stub).to.have.been.calledOnce;
(0, chai_1.expect)(stub.getCall(0).args[0]).to.equal('data');
});
it('should say that hlink reset seq received when buffer is an hlink reset seq', () => {
const hlinkRes = Buffer.from('HLink v0');
const badFn = () => rewiredInstance.onDataRetrievedHandler(hlinkRes);
(0, chai_1.expect)(badFn).throws('Received a hlink reset sequence. Read loop cannot continue!');
});
it('should throw error telling that an incomplete message was received', () => {
const buff = Buffer.from('0000');
const badFn = () => rewiredInstance.onDataRetrievedHandler(buff);
(0, chai_1.expect)(badFn).throws(`Received an incomplete message on start of read! Message size: ${buff.length}. Unable to proceed, exiting ungracefully!`);
});
});
it('should parse buffer, populate read loop chunk and set the current read size', () => {
const encodedMsg = messagepacket_1.default.createMessage('hello-msg', Buffer.from('Greetings!'));
rewiredInstance.onDataRetrievedHandler(encodedMsg);
(0, chai_1.expect)(rewiredInstance.expectedReadBufferSize).to.equals(35);
(0, chai_1.expect)(rewiredInstance.readLoopChunks).to.deep.equal([encodedMsg]);
(0, chai_1.expect)(rewiredInstance.currentBufferReadSize).to.equal(35);
});
});
describe('on pending read', () => {
const buffersToProcess = [
Buffer.from('hi'),
Buffer.from('you!')
];
beforeEach(() => {
rewiredInstance.currentStateOfReadLoop = rewiredInstance.READ_STATES.PENDING_CHUNK;
rewiredInstance.readLoopChunks = [buffersToProcess[0]];
rewiredInstance.currentBufferReadSize = buffersToProcess[0].length;
});
it('should append new buffer to the read loop chunks and update current read size', () => {
rewiredInstance.onDataRetrievedHandler(buffersToProcess[1]);
(0, chai_1.expect)(rewiredInstance.readLoopChunks.length).to.equal(buffersToProcess.length);
(0, chai_1.expect)(rewiredInstance.readLoopChunks).to.deep.equal(buffersToProcess);
(0, chai_1.expect)(rewiredInstance.currentBufferReadSize).to.equal(buffersToProcess[0].length + buffersToProcess[1].length);
});
});
});
describe('#startListen', () => {
it('should process incoming data and emit result', () => __awaiter(void 0, void 0, void 0, function* () {
const encodedMsg = messagepacket_1.default.createMessage('hello-msg', Buffer.from('Greetings!'));
yield transport.init();
const inEndpointStub = transport.inEndpoint.on;
inEndpointStub.withArgs('data')
.onCall(0).callsFake((msg, cb) => cb(encodedMsg));
const emitSpy = sinon_1.default.spy(transport, 'emit');
transport.startListen();
(0, chai_1.expect)(emitSpy.callCount).to.equals(1);
(0, chai_1.expect)(emitSpy.firstCall.args[0]).to.equals('hello-msg');
(0, chai_1.expect)(emitSpy.firstCall.args[1]).to.deep.equals(messagepacket_1.default.parseMessage(encodedMsg));
}));
it('should process buffer chunks when not received as a whole');
it('should call #close on error message', () => __awaiter(void 0, void 0, void 0, function* () {
const spy = sinon_1.default.spy(transport, 'close');
yield transport.init();
const onDataStub = transport.inEndpoint.once;
onDataStub.withArgs('error').callsFake((msg, cb) => cb('Error -1'));
transport.startListen();
(0, chai_1.expect)(spy).to.have.been.calledOnce;
}));
it('should remove data listener on close event', () => __awaiter(void 0, void 0, void 0, function* () {
yield transport.init();
const removeListenerStub = transport.inEndpoint.removeListener;
transport.startListen();
transport.emit('CLOSED');
(0, chai_1.expect)(removeListenerStub).to.have.been.calledOnce;
(0, chai_1.expect)(removeListenerStub.getCall(0).args[0]).to.equal('data');
}));
it('should call #close on transport error event', () => __awaiter(void 0, void 0, void 0, function* () {
const spy = sinon_1.default.spy(transport, 'close');
yield transport.init();
transport.startListen();
transport.emit('ERROR', { errno: usb_1.usb.LIBUSB_ERROR_INTERRUPTED });
(0, chai_1.expect)(spy).to.have.been.calledOnce;
}));
});
describe('#on', () => {
it('should return an event emitter', () => {
const on = transport.on('message', () => { });
(0, chai_1.expect)(on).to.be.instanceof(transport_1.default);
});
});
describe('#removeListener', () => {
it('should return an event emitter', () => {
const on = transport.removeListener('message', () => { });
(0, chai_1.expect)(on).to.be.instanceof(transport_1.default);
});
});
describe('#removeAllListeners', () => {
it('should return an event emitter', () => {
const on = transport.removeAllListeners('message');
(0, chai_1.expect)(on).to.be.instanceof(transport_1.default);
});
});
describe('#receiveMessage', () => {
let onStub;
let fakeTimer;
beforeEach(() => {
onStub = sinon_1.default.stub(transport, 'once');
fakeTimer = sinon_1.default.useFakeTimers();
});
afterEach(() => {
onStub.restore();
fakeTimer.restore();
});
describe('on success', () => {
it('should resolve `once` the message is emitted from super class', () => __awaiter(void 0, void 0, void 0, function* () {
const msg = { name: 'hello', payload: 'hello_back' };
onStub.callsFake((message, cb) => {
cb(msg);
});
const t = yield transport.receiveMessage('hello', 500);
fakeTimer.tick(510);
(0, chai_1.expect)(t).to.deep.equals(msg);
}));
it('should remove error message listener when main message is emitted from super class', () => __awaiter(void 0, void 0, void 0, function* () {
const removeListenerSpy = sinon_1.default.spy(transport, 'removeListener');
const msg = { name: 'test', payload: 'test 123' };
onStub.callsFake((message, cb) => {
cb(msg);
});
const t = yield transport.receiveMessage('test', 500);
fakeTimer.tick(510);
(0, chai_1.expect)(t).to.deep.equals(msg);
(0, chai_1.expect)(removeListenerSpy.callCount).to.equals(2);
(0, chai_1.expect)(removeListenerSpy.getCall(0).args[0]).to.equals('ERROR');
(0, chai_1.expect)(removeListenerSpy.getCall(1).args[0]).to.equals(msg.name);
}));
});
describe('on timeout', () => {
it('should reject with timeout error message when timeout exceeded waiting for message to be emitted', () => __awaiter(void 0, void 0, void 0, function* () {
const spy = sinon_1.default.spy(transport, 'removeAllListeners');
try {
const p = transport.receiveMessage('timeout_msg', 10);
fakeTimer.tick(100);
yield p;
}
catch (e) {
(0, chai_1.expect)(transport.removeAllListeners).to.have.been.calledWith('timeout_msg');
(0, chai_1.expect)(e).to.equals('Request has timed out! timeout_msg 10');
(0, chai_1.expect)(spy.calledOnce).to.equals(true);
return;
}
throw new Error('Did not reject receiveMessage when msg receiver times out!');
}));
});
describe('on error', () => {
it('should reject with error message and clear listener on main message', () => __awaiter(void 0, void 0, void 0, function* () {
const removeListenerSpy = sinon_1.default.spy(transport, 'removeListener');
onStub.withArgs('ERROR').onCall(0).callsFake((msg, cb) => cb('Error Occurred!'));
try {
const p = transport.receiveMessage('buggy_msg', 10);
fakeTimer.tick(100);
yield p;
}
catch (e) {
(0, chai_1.expect)(e).to.equals('Error Occurred!');
(0, chai_1.expect)(removeListenerSpy.callCount).to.equals(1);
(0, chai_1.expect)(removeListenerSpy.getCall(0).args[0]).to.equals('buggy_msg');
return;
}
throw new Error('Did not reject receiveMessage when camera emits ERROR!');
}));
});
it('should resolve when message comes through, other listeners should stay intact', () => __awaiter(void 0, void 0, void 0, function* () {
const msg = { name: 'hello', payload: 'hello_back' };
const messageSpy = sinon_1.default.spy();
onStub.callsFake((message, cb) => {
cb(msg);
});
transport.on('test-subscribe', messageSpy);
yield transport.receiveMessage('test-subscribe', 500);
fakeTimer.tick(510);
transport.emit('test-subscribe');
(0, chai_1.expect)(messageSpy).to.have.callCount(1);
}));
});
describe('#write', () => {
let transferStub;
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
transferStub = sinon_1.default.stub(transport, 'sendChunk');
transferStub.returns(Promise.resolve());
yield transport.init();
}));
afterEach(() => { transferStub.restore(); });
it('should package the message command and its payload and call transfer', () => __awaiter(void 0, void 0, void 0, function* () {
yield transport.write('echo-test', Buffer.alloc(0));
(0, chai_1.expect)(transferStub).to.have.calledWith(messagepacket_1.default.createMessage('echo-test', Buffer.alloc(0)));
}));
describe('on LIBUSB_ERROR_IO', () => {
let splitStub;
beforeEach(() => {
splitStub = sinon_1.default.stub(transport, 'splitAndSendPayloadInChunks');
splitStub.resolves();
});
afterEach(() => {
splitStub.restore();
});
it('should fallback to splitting data in chunks', () => __awaiter(void 0, void 0, void 0, function* () {
transferStub.rejects(new Error('LIBUSB_ERROR_IO'));
yield transport.write('echo-test', Buffer.alloc(16 * 1024 + 1));
(0, chai_1.expect)(splitStub).to.have.been.calledOnce;
}));
it('should reject if even the fallback method fails with some other error', () => {
transferStub.rejects(new Error('LIBUSB_ERROR_IO'));
splitStub.rejects(new Error('LIBUSB_ERROR_OTHER'));
return (0, chai_1.expect)(transport.write('echo-test', Buffer.alloc(16 * 1024 + 1)))
.to.eventually.be.rejectedWith('LIBUSB_ERROR_OTHER');
});
it('should reject if size of buffer is still below MAX_PACKET_SIZE', () => {
transferStub.rejects(new Error('LIBUSB_ERROR_IO'));
return (0, chai_1.expect)(transport.write('echo-test', Buffer.alloc(1))).to.eventually.be.rejectedWith('LIBUSB_ERROR_IO');
});
});
describe('on other error', () => {
it('should reject with original error', () => {
transferStub.rejects(new Error('LIBUSB_ERROR_OTHER'));
return (0, chai_1.expect)(transport.write('echo-test', Buffer.alloc(1))).to.eventually.be.rejectedWith('LIBUSB_ERROR_OTHER');
});
});
describe('#subscribe', () => {
it('should send a hlink subscribe message', () => __awaiter(void 0, void 0, void 0, function* () {
yield transport.subscribe('test-subscribe');
(0, chai_1.expect)(transferStub).to.have.calledWith(messagepacket_1.default.createMessage('hlink-mb-subscribe', 'test-subscribe'));
}));
});
describe('#unsubscribe', () => {
it('should send a hlink unsubscribe message', () => __awaiter(void 0, void 0, void 0, function* () {
yield transport.unsubscribe('test-unsubscribe');
(0, chai_1.expect)(transferStub).to.have.calledWith(messagepacket_1.default.createMessage('hlink-mb-unsubscribe', 'test-unsubscribe'));
}));
});
});
describe('#splitAndSendPayloadInChunks', () => {
let sendChunkStub;
beforeEach(() => {
sendChunkStub = sinon_1.default.stub(transport_1.default.prototype, 'sendChunk');
});
afterEach(() => sendChunkStub.restore());
it('should transfer buffer in chunks', () => __awaiter(void 0, void 0, void 0, function* () {
const command = 'ECHO';
const payloadLen = (MAX_PACKET_SIZE - command.length - 16) * 2;
const payload = crypto_1.default.randomBytes(payloadLen);
const msg = messagepacket_1.default.createMessage(command, payload);
yield transport.init();
yield transport.splitAndSendPayloadInChunks(msg);
(0, chai_1.expect)(sendChunkStub.callCount).to.be.equal(2);
(0, chai_1.expect)(sendChunkStub.firstCall.args[0].length).to.equal(MAX_PACKET_SIZE);
}));
});
describe('#readChunk', () => {
let inEndpointStub;
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
yield transport.init();
inEndpointStub = transport.inEndpoint.transfer;
}));
afterEach(() => {
inEndpointStub.restore();
});
it('should call transfer on out endpoint', () => __awaiter(void 0, void 0, void 0, function* () {
inEndpointStub.callsFake((packetsize, cb) => {
cb();
});
yield transport.readChunk(MAX_PACKET_SIZE);
(0, chai_1.expect)(inEndpointStub).to.been.called;
(0, chai_1.expect)(inEndpointStub.firstCall.args[0]).to.be.equal(MAX_PACKET_SIZE);
}));
it('should reject when transfer fails', () => {
inEndpointStub.callsFake((packetsize, cb) => {
cb({ errno: usb_1.usb.LIBUSB_ERROR_OVERFLOW, message: 'OVERFLOW!' });
});
return (0, chai_1.expect)(transport.readChunk(1))
.to.eventually.be.rejectedWith(`Unable to read data from device (LibUSBException: ${usb_1.usb.LIBUSB_ERROR_OVERFLOW})! OVERFLOW`);
});
});
describe('#sendChunk', () => {
let outEndpointStub;
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
yield transport.init();
outEndpointStub = transport.outEndpoint.transfer;
}));
afterEach(() => {
outEndpointStub.restore();
});
it('should call transfer on in endpoint', () => __awaiter(void 0, void 0, void 0, function* () {
outEndpointStub.callsFake((message, cb) => {
cb();
});
const bufferToSend = Buffer.from('send-me');
yield transport.sendChunk(bufferToSend);
(0, chai_1.expect)(outEndpointStub).to.been.called;
(0, chai_1.expect)(outEndpointStub.firstCall.args[0]).to.be.equal(bufferToSend);
}));
it('should reject when transfer fails', () => {
outEndpointStub.callsFake((packetsize, cb) => {
cb({ errno: usb_1.usb.LIBUSB_ERROR_OVERFLOW, message: 'OVERFLOW!' });
});
return (0, chai_1.expect)(transport.sendChunk(Buffer.from('send-me')))
.to.eventually.be.rejectedWith(`Unable to write data to device (LibUSBException: ${usb_1.usb.LIBUSB_ERROR_OVERFLOW})! OVERFLOW`);
});
});
describe('#stopUsbEndpointPoll', () => {
it('should call stop poll on inEndpoint', () => __awaiter(void 0, void 0, void 0, function* () {
yield transport.init();
const stopPolStub = transport.inEndpoint.stopPoll;
stopPolStub.callsFake((cb) => { cb(); });
return (0, chai_1.expect)(transport.stopUsbEndpointPoll()).to.eventually.be.fulfilled;
}));
it('should reject if inendpoint emits error before polling is stopped');
it('should ignore LIBUSB_TRANSFER_NO_DEVICE since this happens on device detach');
});
describe('#stopEventLoop', () => {
let removeListenerStub;
let stopPoll;
beforeEach(() => {
removeListenerStub = sinon_1.default.stub(transport_1.default.prototype, 'removeAllListeners');
stopPoll = sinon_1.default.stub(transport_1.default.prototype, 'stopUsbEndpointPoll');
stopPoll.resolves();
});
afterEach(() => {
removeListenerStub.restore();
stopPoll.restore();
});
it('should call remove all listeners on super class', () => __awaiter(void 0, void 0, void 0, function* () {
yield transport.stopEventLoop();
(0, chai_1.expect)(removeListenerStub).to.have.been.called;
}));
describe('stop_poll', () => {
let startPollStub;
let startListenStub;
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
yield transport.init();
startListenStub = sinon_1.default.stub(transport_1.default.prototype, 'startListen');
startPollStub = transport.inEndpoint.startPoll;
startPollStub.returns(true);
// @ts-ignore
transport.inEndpoint.descriptor = { wMaxPacketSize: 1024 };
}));
afterEach(() => {
startListenStub.restore();
startPollStub.restore();
});
it('should stop usb endpoint poll if active', () => __awaiter(void 0, void 0, void 0, function* () {
transport.initEventLoop();
yield transport.stopEventLoop();
(0, chai_1.expect)(stopPoll).to.have.been.called;
}));
it('should reject if #stopUsbEndpointPoll fails', () => __awaiter(void 0, void 0, void 0, function* () {
transport.initEventLoop();
stopPoll.rejects(new Error('Cant do that!'));
return (0, chai_1.expect)(transport.stopEventLoop()).to.eventually.be.rejectedWith('Cant do that!');
}));
});
});
describe('#releaseEndpoints', () => {
let stopPollStub;
let releaseStub;
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
yield transport.init();
stopPollStub = sinon_1.default.stub(transport_1.default.prototype, 'stopUsbEndpointPoll');
stopPollStub.resolves();
releaseStub = transport.vscInterface.release;
}));
afterEach(() => {
stopPollStub.restore();
});
it('should stop endpoint poll and release the vsc interface', () => __awaiter(void 0, void 0, void 0, function* () {
releaseStub.yields(undefined);
yield transport.releaseEndpoints();
(0, chai_1.expect)(stopPollStub).to.have.been.called;
(0, chai_1.expect)(releaseStub).to.have.been.called;
}));
it('should reject if stopping endpoint poll fails', () => {
stopPollStub.rejects(new Error('Uuups, problem!'));
return (0, chai_1.expect)(transport.releaseEndpoints()).to.eventually.be.rejectedWith('Uuups, problem!');
});
it('should ignore LIBUSB_ERROR_NO_DEVICE when releasing endpoint fails', () => {
releaseStub.yields({ errno: usb_1.usb.LIBUSB_ERROR_NO_DEVICE });
return (0, chai_1.expect)(transport.releaseEndpoints()).to.eventually.be.fulfilled;
});
it('should reject for any other libusb errors thrown on endpoint release', () => {
releaseStub.yields({ errno: usb_1.usb.LIBUSB_ERROR_ACCESS, message: 'Device is busy!' });
return (0, chai_1.expect)(transport.releaseEndpoints()).to.eventually.be.rejectedWith('Unable to release vsc interface! UsbError: -3 \nDevice is busy!');
});
});
describe('#close', () => {
let releaseEndpointStub;
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
yield transport.init();
releaseEndpointStub = sinon_1.default.stub(transport_1.default.prototype, 'releaseEndpoints');
releaseEndpointStub.resolves();
}));
afterEach(() => {
releaseEndpointStub.restore();
});
it('should release endpoints', () => __awaiter(void 0, void 0, void 0, function* () {
yield transport.close();
(0, chai_1.expect)(releaseEndpointStub).to.have.been.calledOnce;
}));
it('should attempt to close device', () => __awaiter(void 0, void 0, void 0, function* () {
yield transport.close();
(0, chai_1.expect)(mockedDevice.close).to.have.been.calledOnce;
}));
it('should emit closed event', () => __awaiter(void 0, void 0, void 0, function* () {
const spy = sinon_1.default.spy();
transport.on('CLOSED', spy);
yield transport.close();
(0, chai_1.expect)(spy).to.have.been.calledOnce;
}));
it('should assume closed regardless if close fails', () => {
// Tihs is the case where we want to close a device that is detached/booted
mockedDevice.close.throws(new Error('Pending transfers......'));
return (0, chai_1.expect)(transport.close()).to.eventually.be.fulfilled;
});
it('should reject if releasing endpoints fails', () => {
releaseEndpointStub.rejects(new Error('Undefined'));
return (0, chai_1.expect)(transport.close()).to.eventually.be.rejectedWith('Undefined');
});
});
describe('#deprecated', () => {
describe('#receive', () => {
it('should not be supported', () => (0, chai_1.expect)(transport.receive).to.throw('Method "receive" is no longer supported! Please use "receiveMessage" instead.'));
});
describe('#read', () => {
it('should not be supported', () => (0, chai_1.expect)(transport.read).to.throw('Method "read" is no longer supported! Please use "receiveMessage" instead.'));
});
});
describe('#clear', () => {
it('should resolve', () => __awaiter(void 0, void 0, void 0, function* () {
yield transport.clear();
(0, chai_1.expect)(true).to.equal(true);
}));
});
});
//# sourceMappingURL=transport.spec.js.map