utils.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import 'jest';
  4. // Distributed under the terms of the Modified BSD License.
  5. import encoding from 'text-encoding';
  6. import WebSocket from 'ws';
  7. import { UUID } from '@lumino/coreutils';
  8. import { JSONObject, JSONPrimitive, PromiseDelegate } from '@lumino/coreutils';
  9. import { Response } from 'node-fetch';
  10. import {
  11. Contents,
  12. Terminal,
  13. ServerConnection,
  14. KernelManager,
  15. SessionManager,
  16. KernelMessage,
  17. Kernel,
  18. Session
  19. } from '../src';
  20. import { deserialize, serialize } from '../src/kernel/serialize';
  21. // stub for node global
  22. declare let global: any;
  23. /**
  24. * This can be used by test modules that wouldn't otherwise import
  25. * this file.
  26. */
  27. export function init() {
  28. if (typeof global !== 'undefined') {
  29. global.TextEncoder = encoding.TextEncoder;
  30. global.TextDecoder = encoding.TextDecoder;
  31. }
  32. }
  33. // Call init.
  34. init();
  35. /**
  36. * Create a set of server settings.
  37. */
  38. export function makeSettings(
  39. settings?: Partial<ServerConnection.ISettings>
  40. ): ServerConnection.ISettings {
  41. return ServerConnection.makeSettings(settings);
  42. }
  43. const EXAMPLE_KERNEL_INFO: KernelMessage.IInfoReplyMsg['content'] = {
  44. status: 'ok',
  45. protocol_version: '1',
  46. implementation: 'a',
  47. implementation_version: '1',
  48. language_info: {
  49. name: 'test',
  50. version: '',
  51. mimetype: '',
  52. file_extension: '',
  53. pygments_lexer: '',
  54. codemirror_mode: '',
  55. nbconverter_exporter: ''
  56. },
  57. banner: '',
  58. help_links: [
  59. {
  60. text: 'A very helpful link',
  61. url: 'https://very.helpful.website'
  62. }
  63. ]
  64. };
  65. export const PYTHON_SPEC: JSONObject = {
  66. name: 'Python',
  67. spec: {
  68. language: 'python',
  69. argv: [],
  70. display_name: 'python',
  71. env: {}
  72. },
  73. resources: { foo: 'bar' }
  74. };
  75. export const DEFAULT_FILE: Contents.IModel = {
  76. name: 'test',
  77. path: 'foo/test',
  78. type: 'file',
  79. created: 'yesterday',
  80. last_modified: 'today',
  81. writable: true,
  82. mimetype: 'text/plain',
  83. content: 'hello, world!',
  84. format: 'text'
  85. };
  86. export const KERNELSPECS: JSONObject = {
  87. default: 'python',
  88. kernelspecs: {
  89. python: PYTHON_SPEC,
  90. shell: {
  91. name: 'shell',
  92. spec: {
  93. language: 'shell',
  94. argv: [],
  95. display_name: 'Shell',
  96. env: {}
  97. },
  98. resources: {}
  99. }
  100. }
  101. };
  102. /**
  103. * Get a single handler for a request.
  104. */
  105. export function getRequestHandler(
  106. status: number,
  107. body: any
  108. ): ServerConnection.ISettings {
  109. const fetch = (info: RequestInfo, init: RequestInit) => {
  110. // Normalize the body.
  111. body = JSON.stringify(body);
  112. // Create the response and return it as a promise.
  113. const response = new Response(body, { status });
  114. return Promise.resolve(response as any);
  115. };
  116. return ServerConnection.makeSettings({ fetch });
  117. }
  118. /**
  119. * An interface for a service that has server settings.
  120. */
  121. export interface IService {
  122. readonly serverSettings: ServerConnection.ISettings;
  123. }
  124. /**
  125. * Handle a single request with a mock response.
  126. */
  127. export function handleRequest(item: IService, status: number, body: any) {
  128. // Store the existing fetch function.
  129. const oldFetch = item.serverSettings.fetch;
  130. // A single use callback.
  131. const temp = (info: RequestInfo, init: RequestInit) => {
  132. // Restore fetch.
  133. (item.serverSettings as any).fetch = oldFetch;
  134. // Normalize the body.
  135. if (typeof body !== 'string') {
  136. body = JSON.stringify(body);
  137. }
  138. // Create the response and return it as a promise.
  139. const response = new Response(body, { status });
  140. return Promise.resolve(response as any);
  141. };
  142. // Override the fetch function.
  143. (item.serverSettings as any).fetch = temp;
  144. }
  145. /**
  146. * Get a random integer between two values
  147. *
  148. * This implementation comes from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
  149. */
  150. function getRandomInt(min: number, max: number) {
  151. min = Math.ceil(min);
  152. max = Math.floor(max);
  153. return Math.floor(Math.random() * (max - min)) + min; // The maximum is exclusive and the minimum is inclusive
  154. }
  155. /**
  156. * Socket class test rig.
  157. */
  158. class SocketTester implements IService {
  159. /**
  160. * Create a new request and socket tester.
  161. */
  162. constructor() {
  163. let port: number;
  164. // Retry 6 random ports before giving up
  165. for (let retry = 0; retry <= 5; retry++) {
  166. try {
  167. port = getRandomInt(9000, 20000);
  168. this._server = new WebSocket.Server({ port });
  169. } catch (err) {
  170. if (retry === 5) {
  171. throw err;
  172. } else {
  173. continue;
  174. }
  175. }
  176. // We have a successful port
  177. break;
  178. }
  179. this.serverSettings = ServerConnection.makeSettings({
  180. wsUrl: `ws://localhost:${port!}/`,
  181. WebSocket: WebSocket as any
  182. });
  183. this._ready = new PromiseDelegate<void>();
  184. this._server!.on('connection', ws => {
  185. this._ws = ws;
  186. this.onSocket(ws);
  187. this._ready!.resolve();
  188. const connect = this._onConnect;
  189. if (connect) {
  190. connect(ws);
  191. }
  192. });
  193. }
  194. readonly serverSettings: ServerConnection.ISettings;
  195. get ready() {
  196. return this._ready!.promise;
  197. }
  198. /**
  199. * Dispose the socket test rig.
  200. */
  201. dispose(): void {
  202. if (this.isDisposed) {
  203. return;
  204. }
  205. if (this._server) {
  206. this._server.close();
  207. }
  208. this._server = null;
  209. }
  210. /**
  211. * Test if the socket test rig is disposed.
  212. */
  213. get isDisposed(): boolean {
  214. return this._server === null;
  215. }
  216. /**
  217. * Send a raw message from the server to a connected client.
  218. */
  219. sendRaw(msg: string | ArrayBuffer) {
  220. this._ws!.send(msg);
  221. }
  222. /**
  223. * Close the socket.
  224. */
  225. async close(): Promise<void> {
  226. this._ready = new PromiseDelegate<void>();
  227. this._ws!.close();
  228. }
  229. /**
  230. * Register the handler for connections.
  231. */
  232. onConnect(cb: (ws: WebSocket) => void): void {
  233. this._onConnect = cb;
  234. }
  235. /**
  236. * A callback executed when a new server websocket is created.
  237. */
  238. protected onSocket(sock: WebSocket): void {
  239. /* no-op */
  240. }
  241. private _ws: WebSocket | null = null;
  242. private _ready: PromiseDelegate<void> | null = null;
  243. private _server: WebSocket.Server | null = null;
  244. private _onConnect: ((ws: WebSocket) => void) | null = null;
  245. protected settings: ServerConnection.ISettings;
  246. }
  247. /**
  248. * Kernel class test rig.
  249. */
  250. export class KernelTester extends SocketTester {
  251. constructor() {
  252. super();
  253. this._kernelManager = new KernelManager({
  254. serverSettings: this.serverSettings
  255. });
  256. }
  257. initialStatus: Kernel.Status = 'starting';
  258. /**
  259. * The parent header sent on messages.
  260. *
  261. * #### Notes:
  262. * Set to `undefined` to send no parent header.
  263. */
  264. parentHeader: KernelMessage.IHeader | undefined;
  265. /**
  266. * Send the status from the server to the client.
  267. */
  268. sendStatus(msgId: string, status: Kernel.Status) {
  269. return this.sendMessage({
  270. msgId,
  271. msgType: 'status',
  272. channel: 'iopub',
  273. content: { execution_state: status }
  274. });
  275. }
  276. /**
  277. * Send an iopub stream message.
  278. */
  279. sendStream(msgId: string, content: KernelMessage.IStreamMsg['content']) {
  280. return this.sendMessage({
  281. msgId,
  282. msgType: 'stream',
  283. channel: 'iopub',
  284. content
  285. });
  286. }
  287. /**
  288. * Send an iopub display message.
  289. */
  290. sendDisplayData(
  291. msgId: string,
  292. content: KernelMessage.IDisplayDataMsg['content']
  293. ) {
  294. return this.sendMessage({
  295. msgId,
  296. msgType: 'display_data',
  297. channel: 'iopub',
  298. content
  299. });
  300. }
  301. /**
  302. * Send an iopub display message.
  303. */
  304. sendUpdateDisplayData(
  305. msgId: string,
  306. content: KernelMessage.IUpdateDisplayDataMsg['content']
  307. ) {
  308. return this.sendMessage({
  309. msgId,
  310. msgType: 'update_display_data',
  311. channel: 'iopub',
  312. content
  313. });
  314. }
  315. /**
  316. * Send an iopub comm open message.
  317. */
  318. sendCommOpen(msgId: string, content: KernelMessage.ICommOpenMsg['content']) {
  319. return this.sendMessage({
  320. msgId,
  321. msgType: 'comm_open',
  322. channel: 'iopub',
  323. content
  324. });
  325. }
  326. /**
  327. * Send an iopub comm close message.
  328. */
  329. sendCommClose(
  330. msgId: string,
  331. content: KernelMessage.ICommCloseMsg['content']
  332. ) {
  333. return this.sendMessage({
  334. msgId,
  335. msgType: 'comm_close',
  336. channel: 'iopub',
  337. content
  338. });
  339. }
  340. /**
  341. * Send an iopub comm message.
  342. */
  343. sendCommMsg(msgId: string, content: KernelMessage.ICommMsgMsg['content']) {
  344. return this.sendMessage({
  345. msgId,
  346. msgType: 'comm_msg',
  347. channel: 'iopub',
  348. content
  349. });
  350. }
  351. sendExecuteResult(
  352. msgId: string,
  353. content: KernelMessage.IExecuteResultMsg['content']
  354. ) {
  355. return this.sendMessage({
  356. msgId,
  357. msgType: 'execute_result',
  358. channel: 'iopub',
  359. content
  360. });
  361. }
  362. sendExecuteReply(
  363. msgId: string,
  364. content: KernelMessage.IExecuteReplyMsg['content']
  365. ) {
  366. return this.sendMessage({
  367. msgId,
  368. msgType: 'execute_reply',
  369. channel: 'shell',
  370. content
  371. });
  372. }
  373. sendKernelInfoReply(
  374. msgId: string,
  375. content: KernelMessage.IInfoReplyMsg['content']
  376. ) {
  377. return this.sendMessage({
  378. msgId,
  379. msgType: 'kernel_info_reply',
  380. channel: 'shell',
  381. content
  382. });
  383. }
  384. sendInputRequest(
  385. msgId: string,
  386. content: KernelMessage.IInputRequestMsg['content']
  387. ) {
  388. return this.sendMessage({
  389. msgId,
  390. msgType: 'input_request',
  391. channel: 'stdin',
  392. content
  393. });
  394. }
  395. /**
  396. * Send a kernel message with sensible defaults.
  397. */
  398. sendMessage<T extends KernelMessage.Message>(
  399. options: MakeOptional<KernelMessage.IOptions<T>, 'session'>
  400. ) {
  401. const msg = KernelMessage.createMessage<any>({
  402. session: this.serverSessionId,
  403. ...options
  404. });
  405. msg.parent_header = this.parentHeader;
  406. this.send(msg);
  407. return msg.header.msg_id;
  408. }
  409. /**
  410. * Send a kernel message from the server to the client.
  411. */
  412. send(msg: KernelMessage.Message): void {
  413. this.sendRaw(serialize(msg));
  414. }
  415. /**
  416. * Start a client-side kernel talking to our websocket server.
  417. */
  418. async start(): Promise<Kernel.IKernelConnection> {
  419. // Set up the kernel request response.
  420. handleRequest(this, 201, { name: 'test', id: UUID.uuid4() });
  421. // Construct a new kernel.
  422. this._kernel = await this._kernelManager.startNew();
  423. // Wait for the other side to signal it is connected
  424. await this.ready;
  425. // Wait for the initial kernel info reply if we have a normal status
  426. if (this.initialStatus === 'starting') {
  427. await this._kernel.info;
  428. }
  429. return this._kernel;
  430. }
  431. /**
  432. * Shut down the current kernel
  433. */
  434. async shutdown(): Promise<void> {
  435. if (this._kernel && !this._kernel.isDisposed) {
  436. // Set up the kernel request response.
  437. handleRequest(this, 204, {});
  438. await this._kernel.shutdown();
  439. }
  440. }
  441. /**
  442. * Register the message callback with the websocket server.
  443. */
  444. onMessage(cb: (msg: KernelMessage.IMessage) => void): void {
  445. this._onMessage = cb;
  446. }
  447. /**
  448. * Dispose the tester.
  449. */
  450. dispose() {
  451. if (this._kernel) {
  452. this._kernel.dispose();
  453. this._kernel = null;
  454. }
  455. super.dispose();
  456. }
  457. /**
  458. * Set up a new server websocket to pretend like it is a server kernel.
  459. */
  460. protected onSocket(sock: WebSocket): void {
  461. super.onSocket(sock);
  462. this.sendStatus(UUID.uuid4(), this.initialStatus);
  463. sock.on('message', (msg: any) => {
  464. if (msg instanceof Buffer) {
  465. msg = new Uint8Array(msg).buffer;
  466. }
  467. const data = deserialize(msg);
  468. if (data.header.msg_type === 'kernel_info_request') {
  469. // First send status busy message.
  470. this.parentHeader = data.header;
  471. this.sendStatus(UUID.uuid4(), 'busy');
  472. // Then send the kernel_info_reply message.
  473. this.sendKernelInfoReply(UUID.uuid4(), EXAMPLE_KERNEL_INFO);
  474. // Then send status idle message.
  475. this.sendStatus(UUID.uuid4(), 'idle');
  476. this.parentHeader = undefined;
  477. } else {
  478. const onMessage = this._onMessage;
  479. if (onMessage) {
  480. onMessage(data);
  481. }
  482. }
  483. });
  484. }
  485. readonly serverSessionId = UUID.uuid4();
  486. private _kernelManager: Kernel.IManager;
  487. private _kernel: Kernel.IKernelConnection | null = null;
  488. private _onMessage: ((msg: KernelMessage.IMessage) => void) | null = null;
  489. }
  490. /**
  491. * Create a unique session id.
  492. */
  493. export function createSessionModel(id?: string): Session.IModel {
  494. return {
  495. id: id || UUID.uuid4(),
  496. path: UUID.uuid4(),
  497. name: '',
  498. type: '',
  499. kernel: { id: UUID.uuid4(), name: UUID.uuid4() }
  500. };
  501. }
  502. /**
  503. * Session test rig.
  504. */
  505. export class SessionTester extends SocketTester {
  506. constructor() {
  507. super();
  508. const kernelManager = new KernelManager({
  509. serverSettings: this.serverSettings
  510. });
  511. this._sessionManager = new SessionManager({ kernelManager });
  512. }
  513. initialStatus: Kernel.Status = 'starting';
  514. /**
  515. * Start a mock session.
  516. */
  517. async startSession(): Promise<Session.ISessionConnection> {
  518. handleRequest(this, 201, createSessionModel());
  519. this._session = await this._sessionManager!.startNew({
  520. path: UUID.uuid4(),
  521. name: UUID.uuid4(),
  522. type: 'test'
  523. });
  524. await this.ready;
  525. return this._session;
  526. }
  527. /**
  528. * Shut down the current session
  529. */
  530. async shutdown(): Promise<void> {
  531. if (this._session) {
  532. // Set up the session request response.
  533. handleRequest(this, 204, {});
  534. await this._session.shutdown();
  535. }
  536. }
  537. dispose(): void {
  538. super.dispose();
  539. if (this._session) {
  540. this._session.dispose();
  541. this._session = null!;
  542. }
  543. }
  544. /**
  545. * Send the status from the server to the client.
  546. */
  547. sendStatus(status: Kernel.Status, parentHeader?: KernelMessage.IHeader) {
  548. const msg = KernelMessage.createMessage({
  549. msgType: 'status',
  550. channel: 'iopub',
  551. session: this.serverSessionId,
  552. content: {
  553. execution_state: status
  554. }
  555. });
  556. if (parentHeader) {
  557. msg.parent_header = parentHeader;
  558. }
  559. this.send(msg);
  560. }
  561. /**
  562. * Send a kernel message from the server to the client.
  563. */
  564. send(msg: KernelMessage.IMessage): void {
  565. this.sendRaw(serialize(msg));
  566. }
  567. /**
  568. * Register the message callback with the websocket server.
  569. */
  570. onMessage(cb: (msg: KernelMessage.IMessage) => void): void {
  571. this._onMessage = cb;
  572. }
  573. /**
  574. * Set up a new server websocket to pretend like it is a server kernel.
  575. */
  576. protected onSocket(sock: WebSocket): void {
  577. super.onSocket(sock);
  578. sock.on('message', (msg: any) => {
  579. if (msg instanceof Buffer) {
  580. msg = new Uint8Array(msg).buffer;
  581. }
  582. const data = deserialize(msg);
  583. if (KernelMessage.isInfoRequestMsg(data)) {
  584. // First send status busy message.
  585. this.sendStatus('busy', data.header);
  586. // Then send the kernel_info_reply message.
  587. const reply = KernelMessage.createMessage({
  588. msgType: 'kernel_info_reply',
  589. channel: 'shell',
  590. session: this.serverSessionId,
  591. content: EXAMPLE_KERNEL_INFO
  592. });
  593. reply.parent_header = data.header;
  594. this.send(reply);
  595. // Then send status idle message.
  596. this.sendStatus('idle', data.header);
  597. } else {
  598. const onMessage = this._onMessage;
  599. if (onMessage) {
  600. onMessage(data);
  601. }
  602. }
  603. });
  604. }
  605. readonly serverSessionId = UUID.uuid4();
  606. private _session: Session.ISessionConnection;
  607. private _onMessage: ((msg: KernelMessage.IMessage) => void) | null = null;
  608. private _sessionManager: Session.IManager | null = null;
  609. }
  610. /**
  611. * Terminal session test rig.
  612. */
  613. export class TerminalTester extends SocketTester {
  614. /**
  615. * Register the message callback with the websocket server.
  616. */
  617. onMessage(cb: (msg: Terminal.IMessage) => void) {
  618. this._onMessage = cb;
  619. }
  620. protected onSocket(sock: WebSocket): void {
  621. super.onSocket(sock);
  622. sock.on('message', (msg: any) => {
  623. const onMessage = this._onMessage;
  624. if (onMessage) {
  625. const data = JSON.parse(msg) as JSONPrimitive[];
  626. const termMsg: Terminal.IMessage = {
  627. type: data[0] as Terminal.MessageType,
  628. content: data.slice(1)
  629. };
  630. onMessage(termMsg);
  631. }
  632. });
  633. }
  634. private _onMessage: ((msg: Terminal.IMessage) => void) | null = null;
  635. }
  636. /**
  637. * Make a new type with the given keys declared as optional.
  638. *
  639. * #### Notes
  640. * An example:
  641. *
  642. * interface A {a: number, b: string}
  643. * type B = MakeOptional<A, 'a'>
  644. * const x: B = {b: 'test'}
  645. */
  646. type MakeOptional<T, K> = Pick<T, Exclude<keyof T, K>> &
  647. { [P in Extract<keyof T, K>]?: T[P] };