sessioncontext.spec.ts 20 KB

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