utils.ts 16 KB

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