sessioncontext.spec.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. Dialog,
  5. ISessionContext,
  6. SessionContext,
  7. sessionContextDialogs
  8. } from '@jupyterlab/apputils';
  9. import {
  10. KernelManager,
  11. KernelSpecManager,
  12. SessionAPI,
  13. SessionManager
  14. } from '@jupyterlab/services';
  15. import {
  16. acceptDialog,
  17. dismissDialog,
  18. flakyIt as it,
  19. JupyterServer,
  20. testEmission
  21. } from '@jupyterlab/testutils';
  22. import { PromiseDelegate, UUID } from '@lumino/coreutils';
  23. const server = new JupyterServer();
  24. beforeAll(async () => {
  25. await server.start();
  26. });
  27. afterAll(async () => {
  28. await server.shutdown();
  29. });
  30. describe('@jupyterlab/apputils', () => {
  31. describe('SessionContext', () => {
  32. let kernelManager: KernelManager;
  33. let sessionManager: SessionManager;
  34. let specsManager: KernelSpecManager;
  35. let path = '';
  36. let sessionContext: SessionContext;
  37. beforeAll(async () => {
  38. jest.setTimeout(20000);
  39. kernelManager = new KernelManager();
  40. sessionManager = new SessionManager({ kernelManager });
  41. specsManager = new KernelSpecManager();
  42. await Promise.all([
  43. sessionManager.ready,
  44. kernelManager.ready,
  45. specsManager.ready
  46. ]);
  47. });
  48. beforeEach(async () => {
  49. Dialog.flush();
  50. path = UUID.uuid4();
  51. sessionContext = new SessionContext({
  52. path,
  53. sessionManager,
  54. specsManager,
  55. kernelPreference: { name: specsManager.specs?.default }
  56. });
  57. });
  58. afterEach(async () => {
  59. Dialog.flush();
  60. try {
  61. if (sessionContext.session) {
  62. await sessionContext.shutdown();
  63. }
  64. } catch (error) {
  65. console.warn('Session shutdown failed.', error);
  66. }
  67. sessionContext.dispose();
  68. });
  69. describe('#constructor()', () => {
  70. it('should create a session context', () => {
  71. expect(sessionContext).toBeInstanceOf(SessionContext);
  72. });
  73. });
  74. describe('#disposed', () => {
  75. it('should be emitted when the session context is disposed', async () => {
  76. sessionContext.kernelPreference = { canStart: false };
  77. await sessionContext.initialize();
  78. let called = false;
  79. sessionContext.disposed.connect((sender, args) => {
  80. expect(sender).toBe(sessionContext);
  81. expect(args).toBeUndefined();
  82. called = true;
  83. });
  84. sessionContext.dispose();
  85. expect(called).toBe(true);
  86. });
  87. });
  88. describe('#kernelChanged', () => {
  89. it('should be emitted when the kernel changes', async () => {
  90. let called = false;
  91. sessionContext.kernelChanged.connect(
  92. (sender, { oldValue, newValue }) => {
  93. if (oldValue !== null) {
  94. return;
  95. }
  96. expect(sender).toBe(sessionContext);
  97. expect(oldValue).toBeNull();
  98. expect(newValue).toBe(sessionContext.session?.kernel || null);
  99. called = true;
  100. }
  101. );
  102. await sessionContext.initialize();
  103. expect(called).toBe(true);
  104. });
  105. });
  106. describe('#sessionChanged', () => {
  107. it('should be emitted when the session changes', async () => {
  108. let called = false;
  109. sessionContext.sessionChanged.connect(
  110. (sender, { oldValue, newValue }) => {
  111. if (oldValue !== null) {
  112. return;
  113. }
  114. expect(sender).toBe(sessionContext);
  115. expect(oldValue).toBeNull();
  116. expect(newValue).toBe(sessionContext.session);
  117. called = true;
  118. }
  119. );
  120. await sessionContext.initialize();
  121. expect(called).toBe(true);
  122. });
  123. });
  124. describe('#statusChanged', () => {
  125. it('should be emitted when the status changes', async () => {
  126. let called = false;
  127. sessionContext.statusChanged.connect((sender, args) => {
  128. expect(sender).toBe(sessionContext);
  129. expect(typeof args).toBe('string');
  130. called = true;
  131. });
  132. await sessionContext.initialize();
  133. await sessionContext.session!.kernel!.info;
  134. expect(called).toBe(true);
  135. });
  136. });
  137. describe('#iopubMessage', () => {
  138. it('should be emitted for iopub kernel messages', async () => {
  139. let called = false;
  140. sessionContext.iopubMessage.connect((sender, args) => {
  141. expect(sender).toBe(sessionContext);
  142. called = true;
  143. });
  144. await sessionContext.initialize();
  145. await sessionContext.session!.kernel!.info;
  146. expect(called).toBe(true);
  147. });
  148. });
  149. describe('#propertyChanged', () => {
  150. it('should be emitted when a session path changes', async () => {
  151. let called = false;
  152. await sessionContext.initialize();
  153. sessionContext.propertyChanged.connect((sender, args) => {
  154. expect(sender).toBe(sessionContext);
  155. expect(args).toBe('path');
  156. called = true;
  157. });
  158. await sessionContext.session!.setPath('foo');
  159. expect(called).toBe(true);
  160. });
  161. it('should be emitted when a session name changes', async () => {
  162. let called = false;
  163. await sessionContext.initialize();
  164. sessionContext.propertyChanged.connect((sender, args) => {
  165. expect(sender).toBe(sessionContext);
  166. expect(args).toBe('name');
  167. called = true;
  168. });
  169. await sessionContext.session!.setName('foo');
  170. expect(called).toBe(true);
  171. });
  172. it('should be emitted when a session type changes', async () => {
  173. let called = false;
  174. await sessionContext.initialize();
  175. sessionContext.propertyChanged.connect((sender, args) => {
  176. expect(sender).toBe(sessionContext);
  177. expect(args).toBe('type');
  178. called = true;
  179. });
  180. await sessionContext.session!.setType('foo');
  181. expect(called).toBe(true);
  182. });
  183. });
  184. describe('#kernel', () => {
  185. it('should be the current kernel of the the session', async () => {
  186. expect(sessionContext.session?.kernel).toBeFalsy();
  187. await sessionContext.initialize();
  188. expect(sessionContext.session?.kernel).toBeTruthy();
  189. });
  190. });
  191. describe('#kernelPreference', () => {
  192. it('should be the kernel preference of the session', () => {
  193. const preference: ISessionContext.IKernelPreference = {
  194. name: 'foo',
  195. language: 'bar',
  196. id: '1234',
  197. shouldStart: true,
  198. canStart: true
  199. };
  200. sessionContext.kernelPreference = preference;
  201. expect(sessionContext.kernelPreference).toBe(preference);
  202. });
  203. });
  204. describe('#manager', () => {
  205. it('should be the session manager used by the session', () => {
  206. expect(sessionContext.sessionManager).toBe(sessionManager);
  207. });
  208. });
  209. describe('#initialize()', () => {
  210. it('should start the default kernel', async () => {
  211. await sessionContext.initialize();
  212. expect(sessionContext.session?.kernel?.name).toBe(
  213. specsManager.specs!.default
  214. );
  215. });
  216. it('should connect to an existing session on the path', async () => {
  217. const other = await sessionManager.startNew({
  218. name: '',
  219. path,
  220. type: 'test'
  221. });
  222. await sessionContext.initialize();
  223. expect(other.kernel?.id).toBeDefined();
  224. expect(other.kernel?.id).toBe(sessionContext.session?.kernel?.id);
  225. await other.shutdown();
  226. other.dispose();
  227. });
  228. it('should connect to an existing kernel', async () => {
  229. // Shut down and dispose the session so it can be re-instantiated.
  230. await sessionContext.shutdown();
  231. const other = await sessionManager.startNew({
  232. name: '',
  233. path: UUID.uuid4(),
  234. type: 'test'
  235. });
  236. const kernelPreference = { id: other.kernel!.id };
  237. sessionContext = new SessionContext({
  238. sessionManager,
  239. specsManager,
  240. kernelPreference
  241. });
  242. await sessionContext.initialize();
  243. expect(other.kernel?.id).toBeDefined();
  244. expect(other.kernel?.id).toBe(sessionContext.session?.kernel?.id);
  245. // We don't call other.shutdown() here because that
  246. // is handled by the afterEach() handler above.
  247. other.dispose();
  248. });
  249. it('should yield true if there is no distinct kernel to start', async () => {
  250. // Remove the kernel preference before initializing.
  251. sessionContext.kernelPreference = {};
  252. const result = await sessionContext.initialize();
  253. expect(result).toBe(true);
  254. });
  255. it('should be a no-op if the shouldStart kernelPreference is false', async () => {
  256. sessionContext.kernelPreference = { shouldStart: false };
  257. const result = await sessionContext.initialize();
  258. expect(result).toBe(false);
  259. expect(sessionContext.session?.kernel).toBeFalsy();
  260. });
  261. it('should be a no-op if the canStart kernelPreference is false', async () => {
  262. sessionContext.kernelPreference = { canStart: false };
  263. const result = await sessionContext.initialize();
  264. expect(result).toBe(false);
  265. expect(sessionContext.session?.kernel).toBeFalsy();
  266. });
  267. it('should handle an error during startup', async () => {
  268. // Give it a mock manager that errors on connectTo
  269. const mockManager = new SessionManager({ kernelManager });
  270. sessionContext = new SessionContext({
  271. path,
  272. sessionManager: mockManager,
  273. specsManager,
  274. kernelPreference: { name: specsManager.specs?.default }
  275. });
  276. (mockManager as any).running = () => {
  277. return [{ path }];
  278. };
  279. (mockManager as any).connectTo = () => {
  280. throw new Error('mock error');
  281. };
  282. let caught = false;
  283. const promise = sessionContext.initialize().catch(() => {
  284. caught = true;
  285. });
  286. await Promise.all([promise, acceptDialog()]);
  287. expect(caught).toBe(true);
  288. });
  289. });
  290. describe('#kernelDisplayName', () => {
  291. it('should be the display name of the current kernel', async () => {
  292. await sessionContext.initialize();
  293. const spec = await sessionContext.session!.kernel!.spec;
  294. expect(sessionContext.kernelDisplayName).toBe(spec!.display_name);
  295. });
  296. it('should display "No Kernel" when there is no kernel', async () => {
  297. sessionContext.kernelPreference = {
  298. canStart: false,
  299. shouldStart: false
  300. };
  301. expect(sessionContext.kernelDisplayName).toBe('No Kernel');
  302. });
  303. it('should display the pending kernel name when it looks like we are starting a kernel', async () => {
  304. sessionContext.kernelPreference = {
  305. autoStartDefault: true,
  306. canStart: true,
  307. shouldStart: true
  308. };
  309. expect(sessionContext.kernelDisplayName).toBe('Echo Kernel');
  310. });
  311. });
  312. describe('#kernelDisplayStatus', () => {
  313. it('should be the status of the current kernel if connected', async () => {
  314. await sessionContext.initialize();
  315. await sessionContext.session!.kernel!.info;
  316. expect(sessionContext.kernelDisplayStatus).toBe(
  317. sessionContext.session?.kernel?.status
  318. );
  319. });
  320. it('should be the connection status of the current kernel if not connected', async () => {
  321. await sessionContext.initialize();
  322. const reconnect = sessionContext.session!.kernel!.reconnect();
  323. expect(sessionContext.kernelDisplayStatus).toBe(
  324. sessionContext.session?.kernel?.connectionStatus
  325. );
  326. await reconnect;
  327. });
  328. it('should be "initializing" if it looks like we are trying to start a kernel', async () => {
  329. sessionContext.kernelPreference = {};
  330. expect(sessionContext.kernelDisplayStatus).toBe('initializing');
  331. });
  332. it('should be "idle" if there is no current kernel', async () => {
  333. await sessionContext.initialize();
  334. await sessionContext.shutdown();
  335. expect(sessionContext.kernelDisplayStatus).toBe('idle');
  336. });
  337. });
  338. describe('#isDisposed', () => {
  339. it('should test whether a client session has been disposed', () => {
  340. expect(sessionContext.isDisposed).toBe(false);
  341. sessionContext.dispose();
  342. expect(sessionContext.isDisposed).toBe(true);
  343. });
  344. });
  345. describe('#dispose()', () => {
  346. it('should dispose the resources held by the client session', () => {
  347. sessionContext.dispose();
  348. expect(sessionContext.isDisposed).toBe(true);
  349. sessionContext.dispose();
  350. expect(sessionContext.isDisposed).toBe(true);
  351. });
  352. it('should not shut down the session by default', async () => {
  353. await sessionContext.initialize();
  354. const id = sessionContext.session!.id;
  355. sessionContext.dispose();
  356. const sessions = await SessionAPI.listRunning();
  357. expect(sessions.find(s => s.id === id)).toBeTruthy();
  358. await SessionAPI.shutdownSession(id);
  359. });
  360. it('should shut down the session when shutdownOnDispose is true', async () => {
  361. sessionContext.kernelPreference = {
  362. ...sessionContext.kernelPreference,
  363. shutdownOnDispose: true
  364. };
  365. const delegate = new PromiseDelegate();
  366. await sessionContext.initialize();
  367. const id = sessionContext.session!.id;
  368. // Wait for the session to shut down.
  369. sessionContext.sessionManager.runningChanged.connect((_, sessions) => {
  370. if (!sessions.find(s => s.id === id)) {
  371. delegate.resolve(void 0);
  372. return;
  373. }
  374. });
  375. sessionContext.dispose();
  376. return delegate.promise;
  377. });
  378. });
  379. describe('#changeKernel()', () => {
  380. it('should change the current kernel', async () => {
  381. await sessionContext.initialize();
  382. const name = sessionContext.session?.kernel?.name;
  383. const id = sessionContext.session?.kernel?.id;
  384. const kernel = (await sessionContext.changeKernel({ name }))!;
  385. expect(kernel.id).not.toBe(id);
  386. expect(kernel.name).toBe(name);
  387. });
  388. it('should still work if called before fully initialized', async () => {
  389. const initPromise = sessionContext.initialize(); // Start but don't finish init.
  390. const name = 'echo';
  391. const kernelPromise = sessionContext.changeKernel({ name });
  392. let lastKernel = null;
  393. sessionContext.kernelChanged.connect(() => {
  394. lastKernel = sessionContext.session?.kernel;
  395. });
  396. const results = await Promise.all([kernelPromise, initPromise]);
  397. const kernel = results[0];
  398. const shouldSelect = results[1];
  399. expect(shouldSelect).toBe(false);
  400. expect(lastKernel).toBe(kernel);
  401. });
  402. it('should handle multiple requests', async () => {
  403. await sessionContext.initialize();
  404. const name = 'echo';
  405. const kernelPromise0 = sessionContext.changeKernel({ name });
  406. // The last launched kernel should win.
  407. const kernelPromise1 = sessionContext.changeKernel({ name });
  408. let lastKernel = null;
  409. sessionContext.kernelChanged.connect(() => {
  410. lastKernel = sessionContext.session?.kernel;
  411. });
  412. const results = await Promise.all([kernelPromise0, kernelPromise1]);
  413. // We can't know which of the two was launched first, so the result
  414. // could be either, just make sure it isn't the original kernel.
  415. expect([results[0], results[1]]).toContain(lastKernel);
  416. });
  417. });
  418. describe('#shutdown', () => {
  419. it('should kill the kernel and shut down the session', async () => {
  420. await sessionContext.initialize();
  421. expect(sessionContext.session?.kernel).toBeTruthy();
  422. await sessionContext.shutdown();
  423. expect(sessionContext.session?.kernel).toBeFalsy();
  424. });
  425. it('should handle a shutdown during startup', async () => {
  426. const initPromise = sessionContext.initialize(); // Start but don't finish init.
  427. const shutdownPromise = sessionContext.shutdown();
  428. const results = await Promise.all([initPromise, shutdownPromise]);
  429. expect(results[0]).toBe(false);
  430. expect(sessionContext.session).toBe(null);
  431. });
  432. });
  433. describe('.getDefaultKernel()', () => {
  434. it('should return null if no options are given', () => {
  435. expect(
  436. SessionContext.getDefaultKernel({
  437. specs: specsManager.specs,
  438. preference: {}
  439. })
  440. ).toBeNull();
  441. });
  442. it('should return a matching name', () => {
  443. const spec = specsManager.specs!.kernelspecs[
  444. specsManager.specs!.default
  445. ]!;
  446. expect(
  447. SessionContext.getDefaultKernel({
  448. specs: specsManager.specs,
  449. preference: { name: spec.name }
  450. })
  451. ).toBe(spec.name);
  452. });
  453. it('should return null if no match is found', () => {
  454. expect(
  455. SessionContext.getDefaultKernel({
  456. specs: specsManager.specs,
  457. preference: { name: 'foo' }
  458. })
  459. ).toBeNull();
  460. });
  461. it('should return a matching language', () => {
  462. const spec = specsManager.specs!.kernelspecs[
  463. specsManager.specs!.default
  464. ]!;
  465. const kernelspecs: any = {};
  466. kernelspecs[spec.name] = spec;
  467. expect(
  468. SessionContext.getDefaultKernel({
  469. specs: {
  470. default: spec.name,
  471. kernelspecs
  472. },
  473. preference: { language: spec.language }
  474. })
  475. ).toBe(spec.name);
  476. });
  477. it('should return null if a language matches twice', () => {
  478. const spec = specsManager.specs!.kernelspecs[
  479. specsManager.specs!.default
  480. ]!;
  481. const kernelspecs: any = {};
  482. kernelspecs['foo'] = spec;
  483. kernelspecs['bar'] = spec;
  484. expect(
  485. SessionContext.getDefaultKernel({
  486. specs: {
  487. default: spec.name,
  488. kernelspecs
  489. },
  490. preference: { language: spec.language }
  491. })
  492. ).toBeNull();
  493. });
  494. });
  495. describe('.sessionContextDialogs', () => {
  496. describe('#selectKernel()', () => {
  497. it('should select a kernel for the session', async () => {
  498. await sessionContext.initialize();
  499. const { id, name } = sessionContext?.session!.kernel!;
  500. const accept = acceptDialog();
  501. await sessionContextDialogs.selectKernel(sessionContext);
  502. await accept;
  503. const session = sessionContext?.session;
  504. expect(session!.kernel!.id).not.toBe(id);
  505. expect(session!.kernel!.name).toBe(name);
  506. });
  507. it('should keep the existing kernel if dismissed', async () => {
  508. await sessionContext.initialize();
  509. const { id, name } = sessionContext!.session!.kernel!;
  510. const dismiss = dismissDialog();
  511. await sessionContextDialogs.selectKernel(sessionContext);
  512. await dismiss;
  513. const session = sessionContext.session;
  514. expect(session!.kernel!.id).toBe(id);
  515. expect(session!.kernel!.name).toBe(name);
  516. });
  517. });
  518. describe('#restart()', () => {
  519. it('should restart if the user accepts the dialog', async () => {
  520. const emission = testEmission(sessionContext.statusChanged, {
  521. find: (_, args) => args === 'restarting'
  522. });
  523. await sessionContext.initialize();
  524. await sessionContext!.session?.kernel?.info;
  525. const restart = sessionContextDialogs.restart(sessionContext);
  526. await acceptDialog();
  527. expect(await restart).toBe(true);
  528. await emission;
  529. });
  530. it('should not restart if the user rejects the dialog', async () => {
  531. let called = false;
  532. await sessionContext.initialize();
  533. sessionContext.statusChanged.connect((sender, args) => {
  534. if (args === 'restarting') {
  535. called = true;
  536. }
  537. });
  538. const restart = sessionContextDialogs.restart(sessionContext);
  539. await dismissDialog();
  540. expect(await restart).toBe(false);
  541. expect(called).toBe(false);
  542. });
  543. it('should start the same kernel as the previously started kernel', async () => {
  544. await sessionContext.initialize();
  545. await sessionContext.shutdown();
  546. await sessionContextDialogs.restart(sessionContext);
  547. expect(sessionContext?.session?.kernel).toBeTruthy();
  548. });
  549. });
  550. });
  551. });
  552. });