michael@0: /** michael@0: * Test TCPSocket.js by creating an XPCOM-style server socket, then sending michael@0: * data in both directions and making sure each side receives their data michael@0: * correctly and with the proper events. michael@0: * michael@0: * This test is derived from netwerk/test/unit/test_socks.js, except we don't michael@0: * involve a subprocess. michael@0: * michael@0: * Future work: michael@0: * - SSL. see https://bugzilla.mozilla.org/show_bug.cgi?id=466524 michael@0: * https://bugzilla.mozilla.org/show_bug.cgi?id=662180 michael@0: * Alternatively, mochitests could be used. michael@0: * - Testing overflow logic. michael@0: * michael@0: **/ michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: const Cu = Components.utils; michael@0: const CC = Components.Constructor; michael@0: michael@0: /** michael@0: * michael@0: * Constants michael@0: * michael@0: */ michael@0: michael@0: // Some binary data to send. michael@0: const DATA_ARRAY = [0, 255, 254, 0, 1, 2, 3, 0, 255, 255, 254, 0], michael@0: DATA_ARRAY_BUFFER = new ArrayBuffer(DATA_ARRAY.length), michael@0: TYPED_DATA_ARRAY = new Uint8Array(DATA_ARRAY_BUFFER), michael@0: HELLO_WORLD = "hlo wrld. ", michael@0: BIG_ARRAY = new Array(65539), michael@0: BIG_ARRAY_2 = new Array(65539); michael@0: michael@0: TYPED_DATA_ARRAY.set(DATA_ARRAY, 0); michael@0: michael@0: for (var i_big = 0; i_big < BIG_ARRAY.length; i_big++) { michael@0: BIG_ARRAY[i_big] = Math.floor(Math.random() * 256); michael@0: BIG_ARRAY_2[i_big] = Math.floor(Math.random() * 256); michael@0: } michael@0: michael@0: const BIG_ARRAY_BUFFER = new ArrayBuffer(BIG_ARRAY.length), michael@0: BIG_ARRAY_BUFFER_2 = new ArrayBuffer(BIG_ARRAY_2.length); michael@0: const BIG_TYPED_ARRAY = new Uint8Array(BIG_ARRAY_BUFFER), michael@0: BIG_TYPED_ARRAY_2 = new Uint8Array(BIG_ARRAY_BUFFER_2); michael@0: BIG_TYPED_ARRAY.set(BIG_ARRAY); michael@0: BIG_TYPED_ARRAY_2.set(BIG_ARRAY_2); michael@0: michael@0: const ServerSocket = CC("@mozilla.org/network/server-socket;1", michael@0: "nsIServerSocket", michael@0: "init"), michael@0: InputStreamPump = CC("@mozilla.org/network/input-stream-pump;1", michael@0: "nsIInputStreamPump", michael@0: "init"), michael@0: BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", michael@0: "nsIBinaryInputStream", michael@0: "setInputStream"), michael@0: BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1", michael@0: "nsIBinaryOutputStream", michael@0: "setOutputStream"), michael@0: TCPSocket = new (CC("@mozilla.org/tcp-socket;1", michael@0: "nsIDOMTCPSocket"))(); michael@0: michael@0: const gInChild = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) michael@0: .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: /** michael@0: * michael@0: * Helper functions michael@0: * michael@0: */ michael@0: michael@0: function get_platform() { michael@0: var xulRuntime = Components.classes["@mozilla.org/xre/app-info;1"] michael@0: .getService(Components.interfaces.nsIXULRuntime); michael@0: return xulRuntime.OS; michael@0: } michael@0: michael@0: function is_content() { michael@0: return this._inChild = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) michael@0: .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; michael@0: } michael@0: michael@0: /** michael@0: * Spin up a listening socket and associate at most one live, accepted socket michael@0: * with ourselves. michael@0: */ michael@0: function TestServer() { michael@0: this.listener = ServerSocket(-1, true, -1); michael@0: do_print('server: listening on', this.listener.port); michael@0: this.listener.asyncListen(this); michael@0: michael@0: this.binaryInput = null; michael@0: this.input = null; michael@0: this.binaryOutput = null; michael@0: this.output = null; michael@0: michael@0: this.onconnect = null; michael@0: this.ondata = null; michael@0: this.onclose = null; michael@0: } michael@0: michael@0: TestServer.prototype = { michael@0: onSocketAccepted: function(socket, trans) { michael@0: if (this.input) michael@0: do_throw("More than one live connection!?"); michael@0: michael@0: do_print('server: got client connection'); michael@0: this.input = trans.openInputStream(0, 0, 0); michael@0: this.binaryInput = new BinaryInputStream(this.input); michael@0: this.output = trans.openOutputStream(0, 0, 0); michael@0: this.binaryOutput = new BinaryOutputStream(this.output); michael@0: michael@0: new InputStreamPump(this.input, -1, -1, 0, 0, false).asyncRead(this, null); michael@0: michael@0: if (this.onconnect) michael@0: this.onconnect(); michael@0: else michael@0: do_throw("Received unexpected connection!"); michael@0: }, michael@0: michael@0: onStopListening: function(socket) { michael@0: }, michael@0: michael@0: onDataAvailable: function(request, context, inputStream, offset, count) { michael@0: var readData = this.binaryInput.readByteArray(count); michael@0: if (this.ondata) { michael@0: try { michael@0: this.ondata(readData); michael@0: } catch(ex) { michael@0: // re-throw if this is from do_throw michael@0: if (ex === Cr.NS_ERROR_ABORT) michael@0: throw ex; michael@0: // log if there was a test problem michael@0: do_print('Caught exception: ' + ex + '\n' + ex.stack); michael@0: do_throw('test is broken; bad ondata handler; see above'); michael@0: } michael@0: } else { michael@0: do_throw('Received ' + count + ' bytes of unexpected data!'); michael@0: } michael@0: }, michael@0: michael@0: onStartRequest: function(request, context) { michael@0: }, michael@0: michael@0: onStopRequest: function(request, context, status) { michael@0: if (this.onclose) michael@0: this.onclose(); michael@0: else michael@0: do_throw("Received unexpected close!"); michael@0: }, michael@0: michael@0: close: function() { michael@0: this.binaryInput.close(); michael@0: this.binaryOutput.close(); michael@0: }, michael@0: michael@0: /** michael@0: * Forget about the socket we knew about before. michael@0: */ michael@0: reset: function() { michael@0: this.binaryInput = null; michael@0: this.input = null; michael@0: this.binaryOutput = null; michael@0: this.output = null; michael@0: }, michael@0: }; michael@0: michael@0: function makeSuccessCase(name) { michael@0: return function() { michael@0: do_print('got expected: ' + name); michael@0: run_next_test(); michael@0: }; michael@0: } michael@0: michael@0: function makeJointSuccess(names) { michael@0: let funcs = {}, successCount = 0; michael@0: names.forEach(function(name) { michael@0: funcs[name] = function() { michael@0: do_print('got expected: ' + name); michael@0: if (++successCount === names.length) michael@0: run_next_test(); michael@0: }; michael@0: }); michael@0: return funcs; michael@0: } michael@0: michael@0: function makeFailureCase(name) { michael@0: return function() { michael@0: let argstr; michael@0: if (arguments.length) { michael@0: argstr = '(args: ' + michael@0: Array.map(arguments, function(x) { return x.data + ""; }).join(" ") + ')'; michael@0: } michael@0: else { michael@0: argstr = '(no arguments)'; michael@0: } michael@0: do_throw('got unexpected: ' + name + ' ' + argstr); michael@0: }; michael@0: } michael@0: michael@0: function makeExpectData(name, expectedData, fromEvent, callback) { michael@0: let dataBuffer = fromEvent ? null : [], done = false; michael@0: let dataBufferView = null; michael@0: return function(receivedData) { michael@0: if (receivedData.data) { michael@0: receivedData = receivedData.data; michael@0: } michael@0: let recvLength = receivedData.byteLength !== undefined ? michael@0: receivedData.byteLength : receivedData.length; michael@0: michael@0: if (fromEvent) { michael@0: if (dataBuffer) { michael@0: let newBuffer = new ArrayBuffer(dataBuffer.byteLength + recvLength); michael@0: let newBufferView = new Uint8Array(newBuffer); michael@0: newBufferView.set(dataBufferView, 0); michael@0: newBufferView.set(receivedData, dataBuffer.byteLength); michael@0: dataBuffer = newBuffer; michael@0: dataBufferView = newBufferView; michael@0: } michael@0: else { michael@0: dataBuffer = receivedData; michael@0: dataBufferView = new Uint8Array(dataBuffer); michael@0: } michael@0: } michael@0: else { michael@0: dataBuffer = dataBuffer.concat(receivedData); michael@0: } michael@0: do_print(name + ' received ' + recvLength + ' bytes'); michael@0: michael@0: if (done) michael@0: do_throw(name + ' Received data event when already done!'); michael@0: michael@0: let dataView = dataBuffer.byteLength !== undefined ? new Uint8Array(dataBuffer) : dataBuffer; michael@0: if (dataView.length >= expectedData.length) { michael@0: // check the bytes are equivalent michael@0: for (let i = 0; i < expectedData.length; i++) { michael@0: if (dataView[i] !== expectedData[i]) { michael@0: do_throw(name + ' Received mismatched character at position ' + i); michael@0: } michael@0: } michael@0: if (dataView.length > expectedData.length) michael@0: do_throw(name + ' Received ' + dataView.length + ' bytes but only expected ' + michael@0: expectedData.length + ' bytes.'); michael@0: michael@0: done = true; michael@0: if (callback) { michael@0: callback(); michael@0: } else { michael@0: run_next_test(); michael@0: } michael@0: } michael@0: }; michael@0: } michael@0: michael@0: var server = null, sock = null, failure_drain = null; michael@0: michael@0: /** michael@0: * michael@0: * Test functions michael@0: * michael@0: */ michael@0: michael@0: /** michael@0: * Connect the socket to the server. This test is added as the first michael@0: * test, and is also added after every test which results in the socket michael@0: * being closed. michael@0: */ michael@0: michael@0: function connectSock() { michael@0: server.reset(); michael@0: var yayFuncs = makeJointSuccess(['serveropen', 'clientopen']); michael@0: michael@0: sock = TCPSocket.open( michael@0: '127.0.0.1', server.listener.port, michael@0: { binaryType: 'arraybuffer' }); michael@0: michael@0: sock.onopen = yayFuncs.clientopen; michael@0: sock.ondrain = null; michael@0: sock.ondata = makeFailureCase('data'); michael@0: sock.onerror = makeFailureCase('error'); michael@0: sock.onclose = makeFailureCase('close'); michael@0: michael@0: server.onconnect = yayFuncs.serveropen; michael@0: server.ondata = makeFailureCase('serverdata'); michael@0: server.onclose = makeFailureCase('serverclose'); michael@0: } michael@0: michael@0: /** michael@0: * Test that sending a small amount of data works, and that buffering michael@0: * does not take place for this small amount of data. michael@0: */ michael@0: michael@0: function sendData() { michael@0: server.ondata = makeExpectData('serverdata', DATA_ARRAY); michael@0: if (!sock.send(DATA_ARRAY_BUFFER)) { michael@0: do_throw("send should not have buffered such a small amount of data"); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Test that sending a large amount of data works, that buffering michael@0: * takes place (send returns true), and that ondrain is called once michael@0: * the data has been sent. michael@0: */ michael@0: michael@0: function sendBig() { michael@0: var yays = makeJointSuccess(['serverdata', 'clientdrain']), michael@0: amount = 0; michael@0: michael@0: server.ondata = function (data) { michael@0: amount += data.length; michael@0: if (amount === BIG_TYPED_ARRAY.length) { michael@0: yays.serverdata(); michael@0: } michael@0: }; michael@0: sock.ondrain = function(evt) { michael@0: if (sock.bufferedAmount) { michael@0: do_throw("sock.bufferedAmount was > 0 in ondrain"); michael@0: } michael@0: yays.clientdrain(evt); michael@0: } michael@0: if (sock.send(BIG_ARRAY_BUFFER)) { michael@0: do_throw("expected sock.send to return false on large buffer send"); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Test that data sent from the server correctly fires the ondata michael@0: * callback on the client side. michael@0: */ michael@0: michael@0: function receiveData() { michael@0: server.ondata = makeFailureCase('serverdata'); michael@0: sock.ondata = makeExpectData('data', DATA_ARRAY, true); michael@0: michael@0: server.binaryOutput.writeByteArray(DATA_ARRAY, DATA_ARRAY.length); michael@0: } michael@0: michael@0: /** michael@0: * Test that when the server closes the connection, the onclose callback michael@0: * is fired on the client side. michael@0: */ michael@0: michael@0: function serverCloses() { michael@0: // we don't really care about the server's close event, but we do want to michael@0: // make sure it happened for sequencing purposes. michael@0: var yayFuncs = makeJointSuccess(['clientclose', 'serverclose']); michael@0: sock.ondata = makeFailureCase('data'); michael@0: sock.onclose = yayFuncs.clientclose; michael@0: server.onclose = yayFuncs.serverclose; michael@0: michael@0: server.close(); michael@0: } michael@0: michael@0: /** michael@0: * Test that when the client closes the connection, the onclose callback michael@0: * is fired on the server side. michael@0: */ michael@0: michael@0: function clientCloses() { michael@0: // we want to make sure the server heard the close and also that the client's michael@0: // onclose event fired for consistency. michael@0: var yayFuncs = makeJointSuccess(['clientclose', 'serverclose']); michael@0: server.onclose = yayFuncs.serverclose; michael@0: sock.onclose = yayFuncs.clientclose; michael@0: michael@0: sock.close(); michael@0: } michael@0: michael@0: /** michael@0: * Send a large amount of data and immediately call close michael@0: */ michael@0: michael@0: function bufferedClose() { michael@0: var yays = makeJointSuccess(['serverdata', 'clientclose', 'serverclose']); michael@0: server.ondata = makeExpectData( michael@0: "ondata", BIG_TYPED_ARRAY, false, yays.serverdata); michael@0: server.onclose = yays.serverclose; michael@0: sock.onclose = yays.clientclose; michael@0: sock.send(BIG_ARRAY_BUFFER); michael@0: sock.close(); michael@0: } michael@0: michael@0: /** michael@0: * Connect to a port we know is not listening so an error is assured, michael@0: * and make sure that onerror and onclose are fired on the client side. michael@0: */ michael@0: michael@0: function badConnect() { michael@0: // There's probably nothing listening on tcp port 2. michael@0: sock = TCPSocket.open('127.0.0.1', 2); michael@0: michael@0: sock.onopen = makeFailureCase('open'); michael@0: sock.ondata = makeFailureCase('data'); michael@0: michael@0: let success = makeSuccessCase('error'); michael@0: let gotError = false; michael@0: sock.onerror = function(event) { michael@0: do_check_eq(event.data.name, 'ConnectionRefusedError'); michael@0: gotError = true; michael@0: }; michael@0: sock.onclose = function() { michael@0: if (!gotError) michael@0: do_throw('got close without error!'); michael@0: else michael@0: success(); michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Test that calling send with enough data to buffer causes ondrain to michael@0: * be invoked once the data has been sent, and then test that calling send michael@0: * and buffering again causes ondrain to be fired again. michael@0: */ michael@0: michael@0: function drainTwice() { michael@0: let yays = makeJointSuccess( michael@0: ['ondrain', 'ondrain2', michael@0: 'ondata', 'ondata2', michael@0: 'serverclose', 'clientclose']); michael@0: let ondrainCalled = false, michael@0: ondataCalled = false; michael@0: michael@0: function maybeSendNextData() { michael@0: if (!ondrainCalled || !ondataCalled) { michael@0: // make sure server got data and client got ondrain. michael@0: return; michael@0: } michael@0: michael@0: server.ondata = makeExpectData( michael@0: "ondata2", BIG_TYPED_ARRAY_2, false, yays.ondata2); michael@0: michael@0: sock.ondrain = yays.ondrain2; michael@0: michael@0: if (sock.send(BIG_ARRAY_BUFFER_2)) { michael@0: do_throw("sock.send(BIG_TYPED_ARRAY_2) did not return false to indicate buffering"); michael@0: } michael@0: michael@0: sock.close(); michael@0: } michael@0: michael@0: function clientOndrain() { michael@0: yays.ondrain(); michael@0: ondrainCalled = true; michael@0: maybeSendNextData(); michael@0: } michael@0: michael@0: function serverSideCallback() { michael@0: yays.ondata(); michael@0: ondataCalled = true; michael@0: maybeSendNextData(); michael@0: } michael@0: michael@0: server.onclose = yays.serverclose; michael@0: server.ondata = makeExpectData( michael@0: "ondata", BIG_TYPED_ARRAY, false, serverSideCallback); michael@0: michael@0: sock.onclose = yays.clientclose; michael@0: sock.ondrain = clientOndrain; michael@0: michael@0: if (sock.send(BIG_ARRAY_BUFFER)) { michael@0: throw new Error("sock.send(BIG_TYPED_ARRAY) did not return false to indicate buffering"); michael@0: } michael@0: } michael@0: michael@0: function cleanup() { michael@0: do_print("Cleaning up"); michael@0: sock.close(); michael@0: if (!gInChild) michael@0: Services.prefs.clearUserPref('dom.mozTCPSocket.enabled'); michael@0: run_next_test(); michael@0: } michael@0: michael@0: /** michael@0: * Test that calling send with enough data to buffer twice in a row without michael@0: * waiting for ondrain still results in ondrain being invoked at least once. michael@0: */ michael@0: michael@0: function bufferTwice() { michael@0: let yays = makeJointSuccess( michael@0: ['ondata', 'ondrain', 'serverclose', 'clientclose']); michael@0: michael@0: let double_array = new Uint8Array(BIG_ARRAY.concat(BIG_ARRAY_2)); michael@0: server.ondata = makeExpectData( michael@0: "ondata", double_array, false, yays.ondata); michael@0: michael@0: server.onclose = yays.serverclose; michael@0: sock.onclose = yays.clientclose; michael@0: michael@0: sock.ondrain = function () { michael@0: sock.close(); michael@0: yays.ondrain(); michael@0: } michael@0: michael@0: if (sock.send(BIG_ARRAY_BUFFER)) { michael@0: throw new Error("sock.send(BIG_TYPED_ARRAY) did not return false to indicate buffering"); michael@0: } michael@0: if (sock.send(BIG_ARRAY_BUFFER_2)) { michael@0: throw new Error("sock.send(BIG_TYPED_ARRAY_2) did not return false to indicate buffering on second synchronous call to send"); michael@0: } michael@0: } michael@0: michael@0: // Test child behavior when child thinks it's buffering but parent doesn't michael@0: // buffer. michael@0: // 1. set bufferedAmount of content socket to a value that will make next michael@0: // send() call return false. michael@0: // 2. send a small data to make send() return false, but it won't make michael@0: // parent buffer. michael@0: // 3. we should get a ondrain. michael@0: function childbuffered() { michael@0: let yays = makeJointSuccess(['ondrain', 'serverdata', michael@0: 'clientclose', 'serverclose']); michael@0: sock.ondrain = function() { michael@0: yays.ondrain(); michael@0: sock.close(); michael@0: }; michael@0: michael@0: server.ondata = makeExpectData( michael@0: 'ondata', DATA_ARRAY, false, yays.serverdata); michael@0: michael@0: let internalSocket = sock.QueryInterface(Ci.nsITCPSocketInternal); michael@0: internalSocket.updateBufferedAmount(65535, // almost reach buffering threshold michael@0: 0); michael@0: if (sock.send(DATA_ARRAY_BUFFER)) { michael@0: do_throw("expected sock.send to return false."); michael@0: } michael@0: michael@0: sock.onclose = yays.clientclose; michael@0: server.onclose = yays.serverclose; michael@0: } michael@0: michael@0: // Test child's behavior when send() of child return true but parent buffers michael@0: // data. michael@0: // 1. send BIG_ARRAY to make parent buffer. This would make child wait for michael@0: // drain as well. michael@0: // 2. set child's bufferedAmount to zero, so child will no longer wait for michael@0: // drain but parent will dispatch a drain event. michael@0: // 3. wait for 1 second, to make sure there's no ondrain event dispatched in michael@0: // child. michael@0: function childnotbuffered() { michael@0: let yays = makeJointSuccess(['serverdata', 'clientclose', 'serverclose']); michael@0: server.ondata = makeExpectData('ondata', BIG_ARRAY, false, yays.serverdata); michael@0: if (sock.send(BIG_ARRAY_BUFFER)) { michael@0: do_throw("sock.send(BIG_TYPED_ARRAY) did not return false to indicate buffering"); michael@0: } michael@0: let internalSocket = sock.QueryInterface(Ci.nsITCPSocketInternal); michael@0: internalSocket.updateBufferedAmount(0, // setting zero will clear waitForDrain in sock. michael@0: 1); michael@0: michael@0: // shouldn't get ondrain, even after parent have cleared its buffer. michael@0: sock.ondrain = makeFailureCase('drain'); michael@0: sock.onclose = yays.clientclose; michael@0: server.onclose = yays.serverclose; michael@0: do_timeout(1000, function() { michael@0: sock.close(); michael@0: }); michael@0: }; michael@0: michael@0: // - connect, data and events work both ways michael@0: add_test(connectSock); michael@0: add_test(sendData); michael@0: add_test(sendBig); michael@0: add_test(receiveData); michael@0: // - server closes on us michael@0: add_test(serverCloses); michael@0: michael@0: // - connect, we close on the server michael@0: add_test(connectSock); michael@0: add_test(clientCloses); michael@0: michael@0: // - connect, buffer, close michael@0: add_test(connectSock); michael@0: add_test(bufferedClose); michael@0: michael@0: if (get_platform() !== "Darwin") { michael@0: // This test intermittently fails way too often on OS X, for unknown reasons. michael@0: // Please, diagnose and fix it if you can. michael@0: // - get an error on an attempt to connect to a non-listening port michael@0: add_test(badConnect); michael@0: } michael@0: michael@0: // send a buffer, get a drain, send a buffer, get a drain michael@0: add_test(connectSock); michael@0: add_test(drainTwice); michael@0: michael@0: // send a buffer, get a drain, send a buffer, get a drain michael@0: add_test(connectSock); michael@0: add_test(bufferTwice); michael@0: michael@0: if (is_content()) { michael@0: add_test(connectSock); michael@0: add_test(childnotbuffered); michael@0: michael@0: add_test(connectSock); michael@0: add_test(childbuffered); michael@0: } michael@0: michael@0: // clean up michael@0: add_test(cleanup); michael@0: michael@0: function run_test() { michael@0: if (!gInChild) michael@0: Services.prefs.setBoolPref('dom.mozTCPSocket.enabled', true); michael@0: michael@0: server = new TestServer(); michael@0: michael@0: run_next_test(); michael@0: }