diff --git a/.docs/README.hbs b/.docs/README.hbs index 5cf6c4136..016f1e33e 100644 --- a/.docs/README.hbs +++ b/.docs/README.hbs @@ -330,8 +330,8 @@ Testing is an important feature of any library. To aid in our own tests we've de const SerialPort = require('serialport/test'); const MockBinding = SerialPort.Binding; -// Create a port and disable the echo. -MockBinding.createPort('/dev/ROBOT', { echo: false }); +// Create a port and enable the echo and recording. +MockBinding.createPort('/dev/ROBOT', { echo: true, record: true }) cont port = new SerialPort('/dev/ROBOT') ``` diff --git a/README.md b/README.md index f50bb69b8..aaec35149 100644 --- a/README.md +++ b/README.md @@ -377,8 +377,8 @@ Testing is an important feature of any library. To aid in our own tests we've de const SerialPort = require('serialport/test'); const MockBinding = SerialPort.Binding; -// Create a port and disable the echo. -MockBinding.createPort('/dev/ROBOT', { echo: false }); +// Create a port and enable the echo and recording. +MockBinding.createPort('/dev/ROBOT', { echo: true, record: true }) cont port = new SerialPort('/dev/ROBOT') ``` diff --git a/examples/mocking.js b/examples/mocking.js index 6415cdd91..35bd96ecf 100644 --- a/examples/mocking.js +++ b/examples/mocking.js @@ -8,11 +8,16 @@ const MockBinding = SerialPort.Binding; const portPath = 'COM_ANYTHING'; -// By default the mock bindings pretend to be an arduino with the `arduinoEcho` program loaded. +// The mock bindings can pretend to be an arduino with the `arduinoEcho` program loaded. // This will echo any byte written to the port and will emit "READY" data after opening. +// You enable this by passing `echo: true` -// Create a port and disable the echo. -MockBinding.createPort(portPath, { echo: false }); +// Another additional option is `record`, if `record: true` is present all +// writes will be recorded into a single buffer for the lifetime of the port +// it can be read from `port.binding.recording`. + +// Create a port +MockBinding.createPort(portPath, { echo: false, record: false }); const port = new SerialPort(portPath); port.on('open', () => { diff --git a/lib/bindings/mock.js b/lib/bindings/mock.js index 9180cceb1..6eeb0a996 100644 --- a/lib/bindings/mock.js +++ b/lib/bindings/mock.js @@ -1,8 +1,10 @@ 'use strict'; +const debug = require('debug')('serialport:bindings:mock'); const Buffer = require('safe-buffer').Buffer; const BaseBinding = require('./base'); let ports = {}; +let serialNumber = 0; function resolveNextTick() { return new Promise(resolve => process.nextTick(resolve)); @@ -15,6 +17,7 @@ class MockBinding extends BaseBinding { this.isOpen = false; this.port = null; this.lastWrite = null; + this.recording = new Buffer(0); this.pendingWrite = null; } @@ -25,25 +28,29 @@ class MockBinding extends BaseBinding { // Create a mock port static createPort(path, opt) { + serialNumber++; opt = Object.assign({ - echo: true, + echo: false, + record: false, readyData: Buffer.from('READY') }, opt); ports[path] = { data: Buffer.alloc(0), echo: opt.echo, - readyData: opt.readyData, + record: opt.record, + readyData: Buffer.from(opt.readyData), info: { comName: path, manufacturer: 'The J5 Robotics Company', - serialNumber: undefined, + serialNumber, pnpId: undefined, locationId: undefined, vendorId: undefined, productId: undefined } }; + debug(serialNumber, 'created port', JSON.stringify({ path, opt })); } static list() { @@ -58,8 +65,11 @@ class MockBinding extends BaseBinding { if (!this.isOpen) { throw new Error('Port must be open to pretend to receive data'); } + if (!Buffer.isBuffer(data)) { + data = Buffer.from(data); + } + debug(this.serialNumber, 'emitting data - pending read:', Boolean(this.pendingRead)); this.port.data = Buffer.concat([this.port.data, data]); - if (this.pendingRead) { process.nextTick(this.pendingRead); this.pendingRead = null; @@ -67,12 +77,15 @@ class MockBinding extends BaseBinding { } open(path, opt) { + debug(null, `opening path ${path}`); const port = this.port = ports[path]; return super.open(path, opt) + .then(resolveNextTick) .then(() => { if (!port) { return Promise.reject(new Error(`Port does not exist - please call MockBinding.createPort('${path}') first`)); } + this.serialNumber = port.info.serialNumber; if (port.openOpt && port.openOpt.lock) { return Promise.reject(new Error('Port is locked cannot open')); @@ -84,6 +97,7 @@ class MockBinding extends BaseBinding { port.openOpt = Object.assign({}, opt); this.isOpen = true; + debug(this.serialNumber, 'port is open'); if (port.echo) { process.nextTick(() => { if (this.isOpen) { this.emitData(port.readyData) } @@ -94,6 +108,7 @@ class MockBinding extends BaseBinding { close() { const port = this.port; + debug(this.serialNumber, 'closing port'); if (!port) { return Promise.reject(new Error('close')); } @@ -104,12 +119,15 @@ class MockBinding extends BaseBinding { // reset data on close port.data = Buffer.alloc(0); + debug(this.serialNumber, 'port is closed'); delete this.port; + delete this.serialNumber; this.isOpen = false; }); } read(buffer, offset, length) { + debug(this.serialNumber, 'reading', length, 'bytes'); return super.read(buffer, offset, length) .then(resolveNextTick()) .then(() => { @@ -124,42 +142,52 @@ class MockBinding extends BaseBinding { const data = this.port.data.slice(0, length); const readLength = data.copy(buffer, offset); this.port.data = this.port.data.slice(length); - + debug(this.serialNumber, 'read', readLength, 'bytes'); return readLength; }); } write(buffer) { + debug(this.serialNumber, 'writing'); + if (this.pendingWrite) { + throw new Error('Overlapping writes are not supported and should be queued by the serialport object'); + } this.pendingWrite = super.write(buffer) - .then(resolveNextTick()) + .then(resolveNextTick) .then(() => { if (!this.isOpen) { throw new Error('Write canceled'); } const data = this.lastWrite = Buffer.from(buffer); // copy + if (this.port.record) { + this.recording = Buffer.concat([this.recording, data]); + } if (this.port.echo) { process.nextTick(() => { if (this.isOpen) { this.emitData(data) } }); } this.pendingWrite = null; + debug(this.serialNumber, 'writing finished'); }); return this.pendingWrite; } update(opt) { return super.update(opt) + .then(resolveNextTick) .then(() => { this.port.openOpt.baudRate = opt.baudRate; }); } set(opt) { - return super.set(opt); + return super.set(opt).then(resolveNextTick); } get() { return super.get() + .then(resolveNextTick) .then(() => { return { cts: true, @@ -171,10 +199,10 @@ class MockBinding extends BaseBinding { flush() { return super.flush() + .then(resolveNextTick) .then(() => { this.port.data = Buffer.alloc(0); - }) - .then(resolveNextTick()); + }); } drain() {