UNPKG

@runtimed/jmp

Version:

Node.js module for creating, parsing and replying to messages of the Jupyter Messaging Protocol (JMP)

482 lines (398 loc) 15.3 kB
#!/usr/bin/env node /* * BSD 3-Clause License * * Copyright (c) 2015, Nicolas Riesco and others as credited in the AUTHORS file * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its contributors * may be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * */ var assert = require("assert"); var crypto = require("crypto"); var uuid = require("uuid").v4; var jmp = require(".."); var zmq = jmp.zmq; // Setup logging helpers var log; var dontLog = function dontLog() {}; var doLog = function doLog() { process.stderr.write("JMP: TEST:"); console.error.apply(this, arguments); }; if (process.env.DEBUG) { global.DEBUG = true; try { doLog = require("debug")("JMP: TEST:"); } catch (err) {} } log = global.DEBUG ? doLog : dontLog; /** * @typedef Context * * @property context * @property {String} context.scheme Hashing scheme * @property {String} context.key Hashing key * @property {module:jmp~Socket} context.serverSocket Server socket * @property {module:jmp~Socket} context.clientSocket Client socket * */ describe("Listeners", function() { var context = {}; beforeEach(function() { context.scheme = "sha256"; context.key = crypto.randomBytes(256).toString("base64"); context.serverSocket = new jmp.Socket( "rep", context.scheme, context.key ); context.clientSocket = new jmp.Socket( "req", context.scheme, context.key ); // Assign identity to client socket (only for testing purposes) context.clientSocket.setsockopt( zmq.ZMQ_IDENTITY, new Buffer(uuid(), "ascii") ); // Bind to a random local port bindServerAndClient(context.serverSocket, context.clientSocket); }); it("can be registered, invoked and removed", function(done) { context.serverSocket.on("message", onServerMessageListener1); context.serverSocket.on("message", onServerMessageListener2); context.clientSocket.on("message", onClientMessage); context.clientSocket.on("close", function() {}); context.clientSocket.send(new jmp.Message()); function onServerMessageListener1(message) { log("Running onServerMessageListener1..."); onServerMessageListener1.hasRun = true; if (onServerMessageListener2.hasRun) { message.respond(context.serverSocket); } } function onServerMessageListener2(message) { log("Running onServerMessageListener2..."); onServerMessageListener2.hasRun = true; if (onServerMessageListener1.hasRun) { message.respond(context.serverSocket); } } function onClientMessage() { log("Running onClientMessage..."); context.clientSocket.close(); context.serverSocket.close(); context.serverSocket.removeListener( "message", onServerMessageListener1 ); context.serverSocket.removeListener( "message", onServerMessageListener2 ); context.clientSocket.removeAllListeners(); assert.equal( context.serverSocket.listenerCount("message"), 0, "Failed to remove all 'message' listeners in serverSocket" ); assert.deepEqual( context.serverSocket._jmp._listeners, [], "Failed to removed all message listeners in serverSocket" ); assert.equal( context.clientSocket.listenerCount("message"), 0, "Failed to remove all 'message' listeners in clientSocket" ); assert.deepEqual( context.clientSocket._jmp._listeners, [], "Failed to removed all message listeners in clientSocket" ); done(); } }); it("can be registered to be invoked once", function(done) { context.serverSocket.once("message", onServerMessageListener1); context.serverSocket.on("message", onServerMessageListener2); context.clientSocket.on("message", onClientMessage); context.clientSocket.send(new jmp.Message()); return; function onClientMessage() { log("Running onClientMessage..."); context.clientSocket.send(new jmp.Message()); } function onServerMessageListener1(message) { log("Running onServerMessageListener1..."); assert( message instanceof jmp.Message, "onServerMessageListener1 should receive an instance of Message" ); assert( !onServerMessageListener1.hasRun, "onServerMessageListener1 has been invoked more than once" ); onServerMessageListener1.hasRun = true; } function onServerMessageListener2(message) { log("Running onServerMessageListener2..."); if (!onServerMessageListener2.hasRun) { onServerMessageListener2.hasRun = true; message.respond(context.serverSocket); return; } if (!onServerMessageListener2.hasRunTwice) { onServerMessageListener2.hasRunTwice = true; message.respond(context.serverSocket); return; } assert( onServerMessageListener1.hasRun, "onServerMessageListener1 has not been invoked" ); context.clientSocket.close(); context.serverSocket.close(); done(); } }); }); describe("JMP messages", function() { var context = {}; var versionMajor = Number(process.versions.node.split(".")[0]); // Use to skip a spec in Node.js v0.x var itIfNotNodeV0 = (versionMajor === 0) ? xit : it; before(function() { context.scheme = "sha256"; context.key = crypto.randomBytes(256).toString("base64"); context.serverSocket = new jmp.Socket( "router", context.scheme, context.key ); context.clientSocket = new jmp.Socket( "dealer", context.scheme, context.key ); // Assign identity to client socket (only for testing purposes) context.clientSocket.setsockopt( zmq.ZMQ_IDENTITY, new Buffer(uuid(), "ascii") ); // Bind to a random local port bindServerAndClient(context.serverSocket, context.clientSocket); }); after(function() { context.serverSocket.close(); context.clientSocket.close(); }); // A large `Buffer` makes Node.js v0.x exit with: // FATAL ERROR: CALL_AND_ENTRY_0 Allocation failed - process out of memory itIfNotNodeV0("that throw an error should be dropped", function() { var message = new jmp.Message(); var messageFrames = message._encode( context.scheme, context.key ); // The maximum length of a JS string in V8 is 0x1fffffe8 (536870888) // See issue #35676 https://github.com/nodejs/node/issues/35676 messageFrames.unshift(new Buffer(512 * 1024 * 1024)); jmp.Message._decode( messageFrames, context.scheme, context.key ); }); it("can be validated", function() { var anotherKey = crypto.randomBytes(256).toString("base64"); assert.notEqual( context.key, anotherKey, "Failed to generate a pair of keys" ); var originalMessage = new jmp.Message(); var messageFrames = originalMessage._encode( context.scheme, context.key ); var decodedMessage = jmp.Message._decode( messageFrames, context.scheme, context.key ); assert.deepEqual( decodedMessage, originalMessage, makeErrorMessage( "Failed signature validation", decodedMessage, originalMessage ) ); var malformedMessage = jmp.Message._decode( messageFrames, context.scheme, anotherKey ); assert(!malformedMessage, "Failed to detect a malformed message"); }); it("can be sent and recieved", function(done) { var requestMsgType = "kernel_info_request"; var responseMsgType = "kernel_info_reply"; var requestHeader = { "msg_id": uuid(), "username": "user", "session": uuid(), "msg_type": requestMsgType, "version": "5.0", }; var requestBuffers = [0x2A, "42", Array(42), {42: 42}]; var request = new jmp.Message({ header: requestHeader, buffers: requestBuffers, }); assert.deepEqual( request.header, requestHeader, makeErrorMessage( "request.header is unset", request.header, requestHeader ) ); assert.deepEqual( request.buffers, requestBuffers, makeErrorMessage( "request.buffers is unset", request.buffers, requestBuffers ) ); var responseContent = { "protocol_version": "0.0.0", "implementation": "π", "implementation_version": "0.0.0", "language_info": { "name": "test", "version": "0.0.0", "mimetype": "text/plain", "file_extension": "test", }, "banner": "Test", "help_links": [{ "text": "JMP", "url": "https://github.com/n-riesco/nel", }], }; var responseMetadata = {}; context.serverSocket.on("message", getRequest); context.clientSocket.on("message", getResponse); context.clientSocket.send(request); return; function getRequest(message) { assert.equal( message.buffers.length, request.buffers.length, "Wrong number of frames in message.buffers" ); assert.equal( message.idents[0], context.clientSocket.getsockopt(zmq.ZMQ_IDENTITY), makeErrorMessage( "Wrong request.idents", message.idents[0].toString(), context.clientSocket.getsockopt(zmq.ZMQ_IDENTITY).toString() ) ); assert.deepEqual( message.header, request.header, makeErrorMessage( "Wrong request.header", message.header, request.header ) ); assert.deepEqual( message.parent_header, request.parent_header, makeErrorMessage( "request.parent_header", message.parent_header, request.parent_header ) ); assert.deepEqual( message.metadata, request.metadata, makeErrorMessage( "request.metadata", message.metadata, request.metadata ) ); assert.deepEqual( message.content, request.content, makeErrorMessage( "request.content", message.content, request.content ) ); message.respond( context.serverSocket, responseMsgType, responseContent, responseMetadata ); } function getResponse(message) { assert.equal( message.idents.length, 0, makeErrorMessage( "Wrong response.idents.length", message.idents.length, 0 ) ); assert.deepEqual( message.header.msg_type, responseMsgType, makeErrorMessage( "Wrong response.header.msg_type", message.header.msg_type, responseMsgType ) ); assert.deepEqual( message.parent_header, request.header, makeErrorMessage( "Wrong response.parent_header", message.parent_header, request.header ) ); assert.deepEqual( message.content, responseContent, makeErrorMessage( "Wrong response.content", message.content, responseContent ) ); context.serverSocket.removeListener("message", getRequest); context.clientSocket.removeListener("message", getResponse); done(); } }); }); /** * Bind server and client through a random port * * @param {module:zmq~Socket} serverSocket Server socket * @param {module:zmq~Socket} clientSocket Client socket */ function bindServerAndClient(serverSocket, clientSocket) { for (var attempts = 0; ; attempts++) { var randomPort = Math.floor(1024 + Math.random() * (65536 - 1024)); var address = "tcp://127.0.0.1:" + randomPort; try { serverSocket.bindSync(address); clientSocket.connect(address); break; } catch (e) { console.error(e.stack); } if (attempts >= 100) { throw new Error("can't bind to any local ports"); } } } function makeErrorMessage(errorMessage, obtained, expected) { return [ errorMessage, "Obtained", obtained, "Expected", expected, ].join(": "); }