sessioncontext.spec.ts 20 KB

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