Source: lib/kernel.spec.js

  1. /*
  2. * BSD 3-Clause License
  3. *
  4. * Copyright (c) 2017, Nicolas Riesco and others as credited in the AUTHORS file
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. *
  10. * 1. Redistributions of source code must retain the above copyright notice,
  11. * this list of conditions and the following disclaimer.
  12. *
  13. * 2. Redistributions in binary form must reproduce the above copyright notice,
  14. * this list of conditions and the following disclaimer in the documentation
  15. * and/or other materials provided with the distribution.
  16. *
  17. * 3. Neither the name of the copyright holder nor the names of its contributors
  18. * may be used to endorse or promote products derived from this software without
  19. * specific prior written permission.
  20. *
  21. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  22. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  23. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  24. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
  25. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  26. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  27. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  28. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  29. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  30. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  31. * POSSIBILITY OF SUCH DAMAGE.
  32. *
  33. */
  34. var assert = require("assert");
  35. var console = require("console");
  36. var crypto = require("crypto");
  37. var fs = require("fs");
  38. var os = require("os");
  39. var path = require("path");
  40. var spawn = require("child_process").spawn;
  41. var uuid = require("uuid");
  42. var jmp = require("jmp");
  43. var zmq = jmp.zmq;
  44. // Setup logging helpers
  45. var LABEL = "JP-COFFEE-KERNEL: TEST";
  46. var log;
  47. var dontLog = function dontLog() {};
  48. var doLog = function doLog() {
  49. process.stderr.write(LABEL);
  50. console.error.apply(this, arguments);
  51. };
  52. if (process.env.DEBUG) {
  53. global.DEBUG = true;
  54. try {
  55. doLog = require("debug")(LABEL);
  56. } catch (err) {}
  57. }
  58. log = global.DEBUG ? doLog : dontLog;
  59. /**
  60. * @typedef {Object} Message
  61. * @description IPython/Jupyter message
  62. *
  63. * @property {Array} [message.idents] Identities
  64. * @property {Object} [message.header] Header
  65. * @property {Object} [message.parent_header] Parent header
  66. * @property {Object} [message.metadata] Message metadata
  67. * @property {Object} [message.content] Message content
  68. */
  69. /**
  70. * Callback to handle kernel messages
  71. *
  72. * @callback onMessageCallback
  73. * @param {string} socketName Socket name
  74. * @param {Message} message IPython/Jupyter message
  75. */
  76. /* eslint-disable max-len */
  77. /**
  78. * @class Context
  79. * @classdesc Context shared by tests
  80. * (adapted from MessagingTestEngine in IJavascript)
  81. * @see {@link https://github.com/n-riesco/ijavascript/tree/9c2cf6a94575b5016b4a6f3cbe380ab1fcfbcf76}
  82. * @param {string} protocolVersion Messaging protocol version
  83. */
  84. function Context(protocolVersion) {
  85. /* eslint-disable max-len */
  86. /**
  87. * @member version
  88. * @member {String} version.protocol Messaging protocol version
  89. */
  90. this.version = {
  91. protocol: protocolVersion || "5.1",
  92. };
  93. /**
  94. * @member path
  95. * @member {String} path.node Path to node
  96. * @member {String} path.root Path to package folder
  97. * @member {String} path.kernel Path to kernel
  98. * @member {String} path.connectionFile Path to kernel connection file
  99. */
  100. this.path = {};
  101. /**
  102. * @member connection
  103. * @member {String} connection.transport Transport protocol (e.g. "tcp")
  104. * @member {String} connection.ip IP address (e.g. "127.0.0.1")
  105. * @member {String} connection.signature_scheme
  106. * Signature scheme
  107. * (e.g. "hmac-sha256")
  108. * @member {String} connection.key Hashing key (e.g. uuid.v4())
  109. * @member {Integer} connection.hb_port HeartBeat port
  110. * @member {Integer} connection.shell_port Shell port
  111. * @member {Integer} connection.stdin_port Stdin port
  112. * @member {Integer} connection.iopub_port IOPub port
  113. * @member {Integer} connection.control_port Control port
  114. */
  115. this.connection = {};
  116. /**
  117. * @member socket
  118. * @member {module:jmp~Socket} socket.control Control socket
  119. * @member {module:jmp~Socket} socket.hb HearBeat socket
  120. * @member {module:jmp~Socket} socket.iopub IOPub socket
  121. * @member {module:jmp~Socket} socket.shell Shell socket
  122. * @member {module:jmp~Socket} socket.stdin Stdin socket
  123. */
  124. this.socket = {};
  125. /**
  126. * @member {module:child_process~ChildProcess} kernel
  127. * @description Kernel instance
  128. */
  129. this.kernel = null;
  130. /**
  131. * @member {onMessageCallback} [onMessage]
  132. * @description Callback to handle kernel messages
  133. */
  134. this.onMessage = null;
  135. }
  136. /**
  137. * @method init
  138. * @param {function} done
  139. * @description Initialise the messaging test engine
  140. */
  141. Context.prototype.init = function(done) {
  142. this._setPaths();
  143. this._initSockets();
  144. this._createConnectionFile();
  145. this._initKernel(done);
  146. };
  147. /**
  148. * @method dispose
  149. * @description Dispose the messaging test engine
  150. */
  151. Context.prototype.dispose = function() {
  152. this._disposeKernel();
  153. this._removeConnectionFile();
  154. this._disposeSockets();
  155. };
  156. /**
  157. * @method _setPaths
  158. * @description Set up paths
  159. * @private
  160. */
  161. Context.prototype._setPaths = function() {
  162. log("Setting paths");
  163. this.path.node = process.argv[0];
  164. this.path.root = path.dirname(__dirname);
  165. this.path.kernel = path.join(this.path.root, "lib", "kernel.js");
  166. this.path.connectionFile = path.join(
  167. os.tmpdir(),
  168. "conn-" + uuid.v4() + ".json"
  169. );
  170. };
  171. /**
  172. * @method _createConnectionFile
  173. * @description Create connection file
  174. * @private
  175. */
  176. Context.prototype._createConnectionFile = function() {
  177. log("Creating connection file");
  178. fs.writeFileSync(
  179. this.path.connectionFile,
  180. JSON.stringify(this.connection)
  181. );
  182. };
  183. /**
  184. * @method _removeConnectionFile
  185. * @description Remove connection file
  186. * @private
  187. */
  188. Context.prototype._removeConnectionFile = function() {
  189. log("Removing connection file");
  190. try {
  191. fs.unlinkSync(this.path.connectionFile);
  192. } catch (e) {
  193. console.error(e.message);
  194. }
  195. };
  196. /**
  197. * @method _initSockets
  198. * @description Setup ZMQ sockets and message listeners
  199. * @private
  200. */
  201. Context.prototype._initSockets = function() {
  202. log("Setting up ZMQ sockets");
  203. var transport = "tcp";
  204. var ip = "127.0.0.1";
  205. var address = transport + "://" + ip + ":";
  206. var scheme = "sha256";
  207. var key = crypto.randomBytes(256).toString("base64");
  208. this.connection = {
  209. transport: transport,
  210. ip: ip,
  211. signature_scheme: "hmac-" + scheme,
  212. key: key,
  213. };
  214. var socketNames = ["hb", "shell", "stdin", "iopub", "control"];
  215. var socketTypes = ["req", "dealer", "dealer", "sub", "dealer"];
  216. for (var i = 0, attempts = 0; i < socketNames.length; attempts++) {
  217. var socketName = socketNames[i];
  218. var socketType = socketTypes[i];
  219. var socket = (socketName === "hb") ?
  220. new zmq.Socket(socketType) :
  221. new jmp.Socket(socketType, scheme, key);
  222. var port = Math.floor(1024 + Math.random() * (65536 - 1024));
  223. try {
  224. socket.connect(address + port);
  225. this.connection[socketName + "_port"] = port;
  226. this.socket[socketName] = socket;
  227. i++;
  228. } catch (e) {
  229. log(e.stack);
  230. }
  231. if (attempts >= 100) {
  232. throw new Error("can't bind to any local ports");
  233. }
  234. }
  235. this.socket.iopub.subscribe("");
  236. log("Setting up message listeners");
  237. Object.getOwnPropertyNames(this.socket).forEach((function(socketName) {
  238. this.socket[socketName]
  239. .on("message", this._onMessage.bind(this, socketName));
  240. }).bind(this));
  241. };
  242. /**
  243. * @method _disposeSockets
  244. * @description Dispose ZMQ sockets
  245. * @private
  246. */
  247. Context.prototype._disposeSockets = function() {
  248. log("Disposing ZMQ sockets");
  249. this.socket.control.close();
  250. this.socket.hb.close();
  251. this.socket.iopub.close();
  252. this.socket.shell.close();
  253. this.socket.stdin.close();
  254. };
  255. /**
  256. * @method _initKernel
  257. * @description Setup IJavascript kernel
  258. * @param {function} done
  259. * @private
  260. */
  261. Context.prototype._initKernel = function(done) {
  262. log("Initialising a kernel");
  263. var cmd = this.path.node;
  264. var args = [
  265. this.path.kernel,
  266. "--protocol=" + this.version.protocol,
  267. this.path.connectionFile,
  268. ];
  269. if (global.DEBUG) args.push("--debug");
  270. var config = {
  271. stdio: "inherit"
  272. };
  273. log("spawn", cmd, args, config);
  274. this.kernel = spawn(cmd, args, config);
  275. if (done) {
  276. this._waitUntilConnect(function() {
  277. this._waitUntilKernelIsIdle(done);
  278. }.bind(this));
  279. }
  280. };
  281. /**
  282. * @method _disposeKernel
  283. * @description Dispose IJavascript kernel
  284. * @private
  285. */
  286. Context.prototype._disposeKernel = function() {
  287. log("Disposing IJavascript kernel");
  288. if (this.kernel) {
  289. this.kernel.kill("SIGTERM");
  290. }
  291. };
  292. /**
  293. * @method _waitUntilConnect
  294. * @description Wait for ZMQ sockets to connect
  295. * @param {function} done
  296. * @private
  297. */
  298. Context.prototype._waitUntilConnect = function(done) {
  299. log("Waiting for ZMQ sockets to connect");
  300. var socketNames = ["hb", "shell", "iopub", "control"];
  301. var waitGroup = socketNames.length;
  302. function onConnect() {
  303. waitGroup--;
  304. if (waitGroup === 0) {
  305. for (var i = 0; i < socketNames.length; i++) {
  306. this.socket[socketNames[i]].unmonitor();
  307. }
  308. if (done) done();
  309. }
  310. }
  311. for (var j = 0; j < socketNames.length; j++) {
  312. this.socket[socketNames[j]].on("connect", onConnect.bind(this));
  313. this.socket[socketNames[j]].monitor();
  314. }
  315. };
  316. /**
  317. * @method _waitUntilKernelIsIdle
  318. * @description Wait until kernel is idle
  319. * @param {function} done
  320. * @private
  321. */
  322. Context.prototype._waitUntilKernelIsIdle = function(done) {
  323. log("Waiting until kernel is idle");
  324. var onMessage = this.onMessage;
  325. var request = this.run("", function(socketName, response) {
  326. if (response.parent_header.msg_id !== request.header.msg_id) {
  327. return;
  328. }
  329. if (socketName === "iopub") {
  330. if (response.header.msg_type === "status") {
  331. if (response.content.execution_state === "idle") {
  332. this.onMessage = onMessage;
  333. if (done) done();
  334. }
  335. }
  336. }
  337. });
  338. };
  339. /**
  340. * @method _onMessage
  341. * @description Handle kernel message
  342. * @param {string} socketName Socket name
  343. * @param {Message} message IPython/Jupyter message
  344. * @private
  345. */
  346. Context.prototype._onMessage = function(socketName, message) {
  347. log("Received on " + socketName, message);
  348. if (this.onMessage) this.onMessage(socketName, message);
  349. };
  350. /**
  351. * @method run
  352. * @description Send kernel an execution request
  353. *
  354. * @param {string} code Code to be run by kernel
  355. * @param {onMessageCallback} [onMessage] Kernel message handler
  356. *
  357. * @returns {Message} message IPython/Jupyter message
  358. */
  359. Context.prototype.run = function(code, onMessage) {
  360. log("Running:", code);
  361. var message = {
  362. "header": {
  363. "msg_type": "execute_request"
  364. },
  365. "content": {
  366. "code": code
  367. }
  368. };
  369. if (!(message instanceof jmp.Message)) {
  370. message = new jmp.Message(message);
  371. }
  372. if (!message.header.msg_id) {
  373. message.header.msg_id = uuid.v4();
  374. }
  375. if (!message.header.username) {
  376. message.header.username = "user";
  377. }
  378. if (!message.header.session) {
  379. message.header.session = uuid.v4();
  380. }
  381. if (!message.header.version) {
  382. message.header.version = this.version.protocol;
  383. }
  384. this.onMessage = onMessage;
  385. this.socket.shell.send(message);
  386. return message;
  387. };
  388. describe("jp-coffee-kernel", function() {
  389. var ctx = new Context();
  390. before(function(done) {
  391. this.timeout(5000);
  392. ctx.init(done);
  393. });
  394. after(function() {
  395. ctx.dispose();
  396. });
  397. it("can run: console.log 'Hello, World!'", function(done) {
  398. test("console.log 'Hello, World!'", "Hello, World!\n", done);
  399. });
  400. it("uses coffeescript@2", function(done) {
  401. var code = [
  402. "outer = ->",
  403. " inner = => Array.prototype.slice.call arguments",
  404. " inner()",
  405. "",
  406. "console.log outer(1, 2)",
  407. ].join("\n");
  408. var stdout = "[ 1, 2 ]\n";
  409. test(code, stdout, done);
  410. });
  411. function test(code, stdout, done) {
  412. var receivedExecuteReply = false;
  413. var receivedStdout = false;
  414. var requestMessage = ctx.run(code, function(socketName, message) {
  415. if (message.parent_header.msg_id !== requestMessage.header.msg_id) {
  416. return;
  417. }
  418. var msg_type = message && message.header && message.header.msg_type;
  419. if (socketName === "shell") {
  420. if (msg_type === "execute_reply") {
  421. receivedExecuteReply = true;
  422. }
  423. } else if (socketName === "iopub") {
  424. if (msg_type === "stream") {
  425. var name = message.content.name;
  426. if (name === "stdout") {
  427. var text = (message.content.hasOwnProperty("text")) ?
  428. message.content.text :
  429. message.content.data;
  430. assert.equal(text, stdout, "Unexpected stdout");
  431. receivedStdout = true;
  432. }
  433. }
  434. }
  435. if (receivedExecuteReply && receivedStdout) {
  436. ctx.onMessage = null;
  437. done();
  438. }
  439. });
  440. }
  441. });