1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * Unit tests for the JS serial service client.
7 *
8 * These test that configuration and data are correctly transmitted between the
9 * client and the service. They are launched by
10 * extensions/renderer/api/serial/serial_api_unittest.cc.
11 */
12
13var test = require('test').binding;
14var serial = require('serial').binding;
15var unittestBindings = require('test_environment_specific_bindings');
16var utils = require('utils');
17
18var timeoutManager = new unittestBindings.TimeoutManager();
19timeoutManager.installGlobals();
20
21var BUFFER_SIZE = 10;
22
23var connectionId = null;
24
25function connect(callback, options) {
26  options = options || {
27    name: 'test connection',
28    bufferSize: BUFFER_SIZE,
29    receiveTimeout: 12345,
30    sendTimeout: 6789,
31    persistent: true,
32  };
33  serial.connect('device', options, test.callbackPass(function(connectionInfo) {
34    connectionId = connectionInfo.connectionId;
35    if (callback)
36      callback(connectionInfo);
37  }));
38}
39
40// Sets a function to be called once when data is received. Returns a promise
41// that will resolve once the hook is installed.
42function addReceiveHook(callback) {
43  return requireAsync('serial_service').then(function(serialService) {
44    var called = false;
45    var dataReceived = serialService.Connection.prototype.onDataReceived_;
46    serialService.Connection.prototype.onDataReceived_ = function() {
47      var result = $Function.apply(dataReceived, this, arguments);
48      if (!called)
49        callback();
50      called = true;
51      return result;
52    };
53  });
54}
55
56// Sets a function to be called once when a receive error is received. Returns a
57// promise that will resolve once the hook is installed.
58function addReceiveErrorHook(callback) {
59  return requireAsync('serial_service').then(function(serialService) {
60    var called = false;
61    var receiveError = serialService.Connection.prototype.onReceiveError_;
62    serialService.Connection.prototype.onReceiveError_ = function() {
63      var result = $Function.apply(receiveError, this, arguments);
64      if (!called)
65        callback();
66      called = true;
67      return result;
68    };
69  });
70}
71
72function disconnect() {
73  serial.disconnect(connectionId, test.callbackPass(function(success) {
74    test.assertTrue(success);
75    connectionId = null;
76  }));
77}
78
79function checkClientConnectionInfo(connectionInfo) {
80  test.assertFalse(connectionInfo.persistent);
81  test.assertEq('test connection', connectionInfo.name);
82  test.assertEq(12345, connectionInfo.receiveTimeout);
83  test.assertEq(6789, connectionInfo.sendTimeout);
84  test.assertEq(BUFFER_SIZE, connectionInfo.bufferSize);
85  test.assertFalse(connectionInfo.paused);
86}
87
88function checkServiceConnectionInfo(connectionInfo) {
89  test.assertEq(9600, connectionInfo.bitrate);
90  test.assertEq('eight', connectionInfo.dataBits);
91  test.assertEq('no', connectionInfo.parityBit);
92  test.assertEq('one', connectionInfo.stopBits);
93  test.assertFalse(connectionInfo.ctsFlowControl);
94}
95
96function checkConnectionInfo(connectionInfo) {
97  checkClientConnectionInfo(connectionInfo);
98  checkServiceConnectionInfo(connectionInfo);
99}
100
101function runReceiveErrorTest(expectedError) {
102  connect();
103  test.listenOnce(serial.onReceiveError, function(result) {
104    serial.getInfo(connectionId, test.callbackPass(function(connectionInfo) {
105      disconnect();
106      test.assertTrue(connectionInfo.paused);
107    }));
108    test.assertEq(connectionId, result.connectionId);
109    test.assertEq(expectedError, result.error);
110  });
111}
112
113function runSendErrorTest(expectedError) {
114  connect(function() {
115    var buffer = new ArrayBuffer(1);
116    serial.send(connectionId, buffer, test.callbackPass(function(sendInfo) {
117      disconnect();
118      test.assertEq(0, sendInfo.bytesSent);
119      test.assertEq(expectedError, sendInfo.error);
120    }));
121  });
122}
123
124function sendData() {
125  var data = 'data';
126  var buffer = new ArrayBuffer(data.length);
127  var byteBuffer = new Int8Array(buffer);
128  for (var i = 0; i < data.length; i++) {
129    byteBuffer[i] = data.charCodeAt(i);
130  }
131  return utils.promise(serial.send, connectionId, buffer);
132}
133
134function checkReceivedData(result) {
135  var data = 'data';
136  test.assertEq(connectionId, result.connectionId);
137  test.assertEq(data.length, result.data.byteLength);
138  var resultByteBuffer = new Int8Array(result.data);
139  for (var i = 0; i < data.length; i++) {
140    test.assertEq(data.charCodeAt(i), resultByteBuffer[i]);
141  }
142}
143
144unittestBindings.exportTests([
145  // Test that getDevices correctly transforms the data returned by the
146  // SerialDeviceEnumerator.
147  function testGetDevices() {
148    serial.getDevices(test.callbackPass(function(devices) {
149      test.assertEq(3, devices.length);
150      test.assertEq(4, $Object.keys(devices[0]).length);
151      test.assertEq('device', devices[0].path);
152      test.assertEq(1234, devices[0].vendorId);
153      test.assertEq(5678, devices[0].productId);
154      test.assertEq('foo', devices[0].displayName);
155      test.assertEq(1, $Object.keys(devices[1]).length);
156      test.assertEq('another_device', devices[1].path);
157      test.assertEq(1, $Object.keys(devices[2]).length);
158      test.assertEq('', devices[2].path);
159    }));
160  },
161
162  // Test that the correct error message is returned when an error occurs in
163  // connecting to the port. This test uses an IoHandler that fails to connect.
164  function testConnectFail() {
165    serial.connect('device',
166                   test.callbackFail('Failed to connect to the port.'));
167  },
168
169  // Test that the correct error message is returned when an error occurs in
170  // calling getPortInfo after connecting to the port. This test uses an
171  // IoHandler that fails on calls to GetPortInfo.
172  function testGetInfoFailOnConnect() {
173    serial.connect('device',
174                   test.callbackFail('Failed to connect to the port.'));
175  },
176
177  // Test that the correct error message is returned when an invalid bit-rate
178  // value is passed to connect.
179  function testConnectInvalidBitrate() {
180    serial.connect('device', {bitrate: -1}, test.callbackFail(
181        'Failed to connect to the port.'));
182  },
183
184  // Test that a successful connect returns the expected connection info.
185  function testConnect() {
186    connect(function(connectionInfo) {
187      disconnect();
188      checkConnectionInfo(connectionInfo);
189    });
190  },
191
192  // Test that a connection created with no options has the correct default
193  // options.
194  function testConnectDefaultOptions() {
195    connect(function(connectionInfo) {
196      disconnect();
197      test.assertEq(9600, connectionInfo.bitrate);
198      test.assertEq('eight', connectionInfo.dataBits);
199      test.assertEq('no', connectionInfo.parityBit);
200      test.assertEq('one', connectionInfo.stopBits);
201      test.assertFalse(connectionInfo.ctsFlowControl);
202      test.assertFalse(connectionInfo.persistent);
203      test.assertEq('', connectionInfo.name);
204      test.assertEq(0, connectionInfo.receiveTimeout);
205      test.assertEq(0, connectionInfo.sendTimeout);
206      test.assertEq(4096, connectionInfo.bufferSize);
207    }, {});
208  },
209
210  // Test that a getInfo call correctly converts the service-side info from the
211  // Mojo format and returns both it and the client-side configuration.
212  function testGetInfo() {
213    connect(function() {
214      serial.getInfo(connectionId,
215                     test.callbackPass(function(connectionInfo) {
216        disconnect();
217        checkConnectionInfo(connectionInfo);
218      }));
219    });
220  },
221
222  // Test that only client-side options are returned when the service fails a
223  // getInfo call. This test uses an IoHandler that fails GetPortInfo calls
224  // after the initial call during connect.
225  function testGetInfoFailToGetPortInfo() {
226    connect(function() {
227      serial.getInfo(connectionId,
228                     test.callbackPass(function(connectionInfo) {
229        disconnect();
230        checkClientConnectionInfo(connectionInfo);
231        test.assertFalse('bitrate' in connectionInfo);
232        test.assertFalse('dataBits' in connectionInfo);
233        test.assertFalse('parityBit' in connectionInfo);
234        test.assertFalse('stopBit' in connectionInfo);
235        test.assertFalse('ctsFlowControl' in connectionInfo);
236      }));
237    });
238  },
239
240  // Test that getConnections returns an array containing the open connection.
241  function testGetConnections() {
242    connect(function() {
243      serial.getConnections(test.callbackPass(function(connections) {
244        disconnect();
245        test.assertEq(1, connections.length);
246        checkConnectionInfo(connections[0]);
247      }));
248    });
249  },
250
251  // Test that getControlSignals correctly converts the Mojo format result. This
252  // test uses an IoHandler that returns values matching the pattern being
253  // tested.
254  function testGetControlSignals() {
255    connect(function() {
256      var calls = 0;
257      function checkControlSignals(signals) {
258        if (calls == 15) {
259          disconnect();
260        } else {
261          serial.getControlSignals(
262              connectionId,
263              test.callbackPass(checkControlSignals));
264        }
265        test.assertEq(!!(calls & 1), signals.dcd);
266        test.assertEq(!!(calls & 2), signals.cts);
267        test.assertEq(!!(calls & 4), signals.ri);
268        test.assertEq(!!(calls & 8), signals.dsr);
269        calls++;
270      }
271      serial.getControlSignals(connectionId,
272                               test.callbackPass(checkControlSignals));
273    });
274  },
275
276  // Test that setControlSignals correctly converts to the Mojo format result.
277  // This test uses an IoHandler that returns values following the same table of
278  // values as |signalsValues|.
279  function testSetControlSignals() {
280    connect(function() {
281      var signalsValues = [
282        {},
283        {dtr: false},
284        {dtr: true},
285        {rts: false},
286        {dtr: false, rts: false},
287        {dtr: true, rts: false},
288        {rts: true},
289        {dtr: false, rts: true},
290        {dtr: true, rts: true},
291      ];
292      var calls = 0;
293      function setControlSignals(success) {
294        if (calls == signalsValues.length) {
295          disconnect();
296        } else {
297          serial.setControlSignals(connectionId,
298                                   signalsValues[calls++],
299                                   test.callbackPass(setControlSignals));
300        }
301        test.assertTrue(success);
302      }
303      setControlSignals(true);
304    });
305  },
306
307  // Test that update correctly passes values to the service only for
308  // service-side options and that all update calls are reflected by the result
309  // of getInfo calls. This test uses an IoHandler that expects corresponding
310  // ConfigurePort calls.
311  function testUpdate() {
312    connect(function() {
313      var optionsValues = [
314        {},  // SetPortOptions is called once during connection.
315        {bitrate: 57600},
316        {dataBits: 'seven'},
317        {dataBits: 'eight'},
318        {parityBit: 'no'},
319        {parityBit: 'odd'},
320        {parityBit: 'even'},
321        {stopBits: 'one'},
322        {stopBits: 'two'},
323        {ctsFlowControl: false},
324        {ctsFlowControl: true},
325        {bufferSize: 1},
326        {sendTimeout: 0},
327        {receiveTimeout: 0},
328        {persistent: false},
329        {name: 'name'},
330      ];
331      var calls = 0;
332      function checkInfo(info) {
333        for (var key in optionsValues[calls]) {
334          test.assertEq(optionsValues[calls][key], info[key]);
335        }
336        setOptions();
337      }
338      function setOptions() {
339        if (++calls == optionsValues.length) {
340          disconnect();
341        } else {
342          serial.update(connectionId,
343                        optionsValues[calls],
344                        test.callbackPass(function(success) {
345            serial.getInfo(connectionId, test.callbackPass(checkInfo));
346            test.assertTrue(success);
347          }));
348        }
349      }
350      setOptions();
351    });
352  },
353
354  // Test that passing an invalid bit-rate reslts in an error.
355  function testUpdateInvalidBitrate() {
356    connect(function() {
357      serial.update(connectionId,
358                    {bitrate: -1},
359                    test.callbackPass(function(success) {
360        disconnect();
361        test.assertFalse(success);
362      }));
363    });
364  },
365
366  // Test flush. This test uses an IoHandler that counts the number of flush
367  // calls.
368  function testFlush() {
369    connect(function() {
370      serial.flush(connectionId, test.callbackPass(function(success) {
371        disconnect();
372        test.assertTrue(success);
373      }));
374    });
375  },
376
377  // Test that setPaused values are reflected by the results returned by getInfo
378  // calls.
379  function testSetPaused() {
380    connect(function() {
381      serial.setPaused(connectionId, true, test.callbackPass(function() {
382        serial.getInfo(connectionId, test.callbackPass(function(info) {
383          serial.setPaused(connectionId, false, test.callbackPass(function() {
384            serial.getInfo(connectionId, test.callbackPass(function(info) {
385              test.assertFalse(info.paused);
386              disconnect();
387            }));
388          }));
389          test.assertTrue(info.paused);
390        }));
391      }));
392    });
393  },
394
395  // Test that a send and a receive correctly echoes data. This uses an
396  // IoHandler that echoes data sent to it.
397  function testEcho() {
398    connect(function() {
399      sendData().then(test.callbackPass(function(sendInfo) {
400        test.assertEq(4, sendInfo.bytesSent);
401        test.assertEq(undefined, sendInfo.error);
402      }));
403      test.listenOnce(serial.onReceive, function(result) {
404        checkReceivedData(result);
405        disconnect();
406      });
407    });
408  },
409
410  // Test that a send while another send is in progress returns a pending error.
411  function testSendDuringExistingSend() {
412    connect(function() {
413      sendData().then(test.callbackPass(function(sendInfo) {
414        test.assertEq(4, sendInfo.bytesSent);
415        test.assertEq(undefined, sendInfo.error);
416        disconnect();
417      }));
418      sendData().then(test.callbackPass(function(sendInfo) {
419        test.assertEq(0, sendInfo.bytesSent);
420        test.assertEq('pending', sendInfo.error);
421      }));
422    });
423  },
424
425  // Test that a second send after the first finishes is successful. This uses
426  // an IoHandler that echoes data sent to it.
427  function testSendAfterSuccessfulSend() {
428    connect(function() {
429      sendData().then(test.callbackPass(function(sendInfo) {
430        test.assertEq(4, sendInfo.bytesSent);
431        test.assertEq(undefined, sendInfo.error);
432        return sendData();
433      })).then(test.callbackPass(function(sendInfo) {
434        test.assertEq(4, sendInfo.bytesSent);
435        test.assertEq(undefined, sendInfo.error);
436      }));
437      // Check that the correct data is echoed twice.
438      test.listenOnce(serial.onReceive, function(result) {
439        checkReceivedData(result);
440        test.listenOnce(serial.onReceive, function(result) {
441          checkReceivedData(result);
442          disconnect();
443        });
444      });
445    });
446  },
447
448  // Test that a second send after the first fails is successful. This uses an
449  // IoHandler that returns system_error for only the first send.
450  function testSendPartialSuccessWithError() {
451    connect(function() {
452      sendData().then(test.callbackPass(function(sendInfo) {
453        test.assertEq(2, sendInfo.bytesSent);
454        test.assertEq('system_error', sendInfo.error);
455        return sendData();
456      })).then(test.callbackPass(function(sendInfo) {
457        test.assertEq(4, sendInfo.bytesSent);
458        test.assertEq(undefined, sendInfo.error);
459        disconnect();
460      }));
461    });
462  },
463
464  // Test that a timed-out send returns a timeout error and that changing the
465  // send timeout during a send does not affect its timeout. This test uses an
466  // IoHandle that never completes sends.
467  function testSendTimeout() {
468    connect(function() {
469      sendData().then(test.callbackPass(function(sendInfo) {
470        test.assertEq(0, sendInfo.bytesSent);
471        test.assertEq('timeout', sendInfo.error);
472        test.assertEq(5, timeoutManager.currentTime);
473        disconnect();
474      }));
475      serial.update(connectionId, {sendTimeout: 10}, test.callbackPass(
476          timeoutManager.run.bind(timeoutManager, 1)));
477    }, {sendTimeout: 5});
478  },
479
480  // Test that a timed-out send returns a timeout error and that disabling the
481  // send timeout during a send does not affect its timeout. This test uses an
482  // IoHandle that never completes sends.
483  function testDisableSendTimeout() {
484    connect(function() {
485      sendData().then(test.callbackPass(function(sendInfo) {
486        test.assertEq(0, sendInfo.bytesSent);
487        test.assertEq('timeout', sendInfo.error);
488        test.assertEq(6, timeoutManager.currentTime);
489        disconnect();
490      }));
491      serial.update(connectionId, {sendTimeout: 0}, test.callbackPass(
492          timeoutManager.run.bind(timeoutManager, 1)));
493    }, {sendTimeout: 6});
494  },
495
496  // Test that data received while the connection is paused is queued and
497  // dispatched once the connection is unpaused.
498  function testPausedReceive() {
499    // Wait until the receive hook is installed, then start the test.
500    addReceiveHook(function() {
501      // Unpause the connection after the connection has queued the received
502      // data to ensure the queued data is dispatched when the connection is
503      // unpaused.
504      serial.setPaused(connectionId, false, test.callbackPass());
505      // Check that setPaused(false) is idempotent.
506      serial.setPaused(connectionId, false, test.callbackPass());
507    }).then(function() {
508      connect(function() {
509        // Check that setPaused(true) is idempotent.
510        serial.setPaused(connectionId, true, test.callbackPass());
511        serial.setPaused(connectionId, true, test.callbackPass());
512      });
513    });
514    test.listenOnce(serial.onReceive, function(result) {
515      checkReceivedData(result);
516      disconnect();
517    });
518  },
519
520  // Test that a receive error received while the connection is paused is queued
521  // and dispatched once the connection is unpaused.
522  function testPausedReceiveError() {
523    addReceiveErrorHook(function() {
524      // Unpause the connection after the connection has queued the receive
525      // error to ensure the queued error is dispatched when the connection is
526      // unpaused.
527      serial.setPaused(connectionId, false, test.callbackPass());
528    }).then(test.callbackPass(function() {
529      connect(function() {
530        serial.setPaused(connectionId, true, test.callbackPass());
531      });
532    }));
533
534    test.listenOnce(serial.onReceiveError, function(result) {
535      serial.getInfo(connectionId, test.callbackPass(function(connectionInfo) {
536        disconnect();
537        test.assertTrue(connectionInfo.paused);
538      }));
539      test.assertEq(connectionId, result.connectionId);
540      test.assertEq('device_lost', result.error);
541    });
542    serial.onReceive.addListener(function() {
543      test.fail('unexpected onReceive event');
544    });
545  },
546
547  // Test that receive timeouts trigger after the timeout time elapses and that
548  // changing the receive timeout does not affect a wait in progress.
549  function testReceiveTimeout() {
550    connect(function() {
551      test.listenOnce(serial.onReceiveError, function(result) {
552        test.assertEq(connectionId, result.connectionId);
553        test.assertEq('timeout', result.error);
554        test.assertEq(20, timeoutManager.currentTime);
555        serial.getInfo(connectionId, test.callbackPass(
556            function(connectionInfo) {
557          test.assertFalse(connectionInfo.paused);
558          disconnect();
559        }));
560      });
561      // Changing the timeout does not take effect until the current timeout
562      // expires or a receive completes.
563      serial.update(connectionId, {receiveTimeout: 10}, test.callbackPass(
564          timeoutManager.run.bind(timeoutManager, 1)));
565    }, {receiveTimeout: 20});
566  },
567
568  // Test that receive timeouts trigger after the timeout time elapses and that
569  // disabling the receive timeout does not affect a wait in progress.
570  function testDisableReceiveTimeout() {
571    connect(function() {
572      test.listenOnce(serial.onReceiveError, function(result) {
573        test.assertEq(connectionId, result.connectionId);
574        test.assertEq('timeout', result.error);
575        test.assertEq(30, timeoutManager.currentTime);
576        serial.getInfo(connectionId, test.callbackPass(
577            function(connectionInfo) {
578          disconnect();
579          test.assertFalse(connectionInfo.paused);
580        }));
581      });
582      // Disabling the timeout does not take effect until the current timeout
583      // expires or a receive completes.
584      serial.update(connectionId, {receiveTimeout: 0}, test.callbackPass(
585          timeoutManager.run.bind(timeoutManager, 1)));
586    }, {receiveTimeout: 30});
587  },
588
589  // Test that a receive error from the service is correctly dispatched. This
590  // test uses an IoHandler that only reports 'disconnected' receive errors.
591  function testReceiveErrorDisconnected() {
592    runReceiveErrorTest('disconnected');
593  },
594
595  // Test that a receive error from the service is correctly dispatched. This
596  // test uses an IoHandler that only reports 'device_lost' receive errors.
597  function testReceiveErrorDeviceLost() {
598    runReceiveErrorTest('device_lost');
599  },
600
601  // Test that a receive from error the service is correctly dispatched. This
602  // test uses an IoHandler that only reports 'system_error' receive errors.
603  function testReceiveErrorSystemError() {
604    runReceiveErrorTest('system_error');
605  },
606
607  // Test that a send error from the service is correctly returned as the send
608  // result. This test uses an IoHandler that only reports 'disconnected' send
609  // errors.
610  function testSendErrorDisconnected() {
611    runSendErrorTest('disconnected');
612  },
613
614  // Test that a send error from the service is correctly returned as the send
615  // result. This test uses an IoHandler that only reports 'system_error' send
616  // errors.
617  function testSendErrorSystemError() {
618    runSendErrorTest('system_error');
619  },
620
621  // Test that disconnect returns the correct error for a connection ID that
622  // does not exist.
623  function testDisconnectUnknownConnectionId() {
624    serial.disconnect(-1, test.callbackFail('Serial connection not found.'));
625  },
626
627  // Test that getInfo returns the correct error for a connection ID that does
628  // not exist.
629  function testGetInfoUnknownConnectionId() {
630    serial.getInfo(-1, test.callbackFail('Serial connection not found.'));
631  },
632
633  // Test that update returns the correct error for a connection ID that does
634  // not exist.
635  function testUpdateUnknownConnectionId() {
636    serial.update(-1, {}, test.callbackFail('Serial connection not found.'));
637  },
638
639  // Test that setControlSignals returns the correct error for a connection ID
640  // that does not exist.
641  function testSetControlSignalsUnknownConnectionId() {
642    serial.setControlSignals(-1, {}, test.callbackFail(
643        'Serial connection not found.'));
644  },
645
646  // Test that getControlSignals returns the correct error for a connection ID
647  // that does not exist.
648  function testGetControlSignalsUnknownConnectionId() {
649    serial.getControlSignals(-1, test.callbackFail(
650        'Serial connection not found.'));
651  },
652
653  // Test that flush returns the correct error for a connection ID that does not
654  // exist.
655  function testFlushUnknownConnectionId() {
656    serial.flush(-1, test.callbackFail('Serial connection not found.'));
657  },
658
659  // Test that setPaused returns the correct error for a connection ID that does
660  // not exist.
661  function testSetPausedUnknownConnectionId() {
662    serial.setPaused(
663        -1, true, test.callbackFail('Serial connection not found.'));
664    serial.setPaused(
665        -1, false, test.callbackFail('Serial connection not found.'));
666  },
667
668  // Test that send returns the correct error for a connection ID that does not
669  // exist.
670  function testSendUnknownConnectionId() {
671    var buffer = new ArrayBuffer(1);
672    serial.send(-1, buffer, test.callbackFail('Serial connection not found.'));
673  },
674], test.runTests, exports);
675