isession.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. // Copyright (c) Jupyter Development Team.
  2. import 'jest';
  3. const it = require('jest-retries');
  4. import { PageConfig } from '@jupyterlab/coreutils';
  5. import { UUID } from '@lumino/coreutils';
  6. import { Signal } from '@lumino/signaling';
  7. import {
  8. KernelMessage,
  9. Session,
  10. SessionManager,
  11. KernelManager,
  12. KernelAPI
  13. } from '../../src';
  14. import {
  15. expectFailure,
  16. testEmission,
  17. JupyterServer
  18. } from '@jupyterlab/testutils';
  19. import { handleRequest, SessionTester, init } from '../utils';
  20. init();
  21. let kernelManager: KernelManager;
  22. let sessionManager: SessionManager;
  23. /**
  24. * Start a new session with a unique name.
  25. */
  26. async function startNew(): Promise<Session.ISessionConnection> {
  27. const session = await sessionManager.startNew({
  28. path: UUID.uuid4(),
  29. name: UUID.uuid4(),
  30. type: 'test'
  31. });
  32. return session;
  33. }
  34. const server = new JupyterServer();
  35. beforeAll(async () => {
  36. await server.start();
  37. kernelManager = new KernelManager();
  38. sessionManager = new SessionManager({ kernelManager });
  39. });
  40. afterAll(async () => {
  41. await server.shutdown();
  42. });
  43. describe('session', () => {
  44. let session: Session.ISessionConnection;
  45. let defaultSession: Session.ISessionConnection;
  46. beforeAll(async () => {
  47. jest.setTimeout(20000);
  48. defaultSession = await startNew();
  49. });
  50. afterEach(async () => {
  51. if (session && !session.isDisposed) {
  52. await session.shutdown();
  53. }
  54. });
  55. afterAll(async () => {
  56. await defaultSession.shutdown();
  57. });
  58. describe('Session.DefaultSession', () => {
  59. describe('#disposed', () => {
  60. it('should emit when the session is disposed', async () => {
  61. let called = false;
  62. session = await startNew();
  63. session.disposed.connect(() => {
  64. called = true;
  65. });
  66. await session.shutdown();
  67. session.dispose();
  68. expect(called).toBe(true);
  69. });
  70. });
  71. describe('#kernelChanged', () => {
  72. it('should emit when the kernel changes', async () => {
  73. let called: Session.ISessionConnection.IKernelChangedArgs | null = null;
  74. const object = {};
  75. await defaultSession.kernel?.requestKernelInfo();
  76. defaultSession.kernelChanged.connect((s, args) => {
  77. called = args;
  78. Signal.disconnectReceiver(object);
  79. }, object);
  80. const original = defaultSession.kernel!;
  81. // Create a new kernel with the same kernel name (same type)
  82. await defaultSession.changeKernel({ name: original.name });
  83. expect(original).not.toBe(defaultSession.kernel);
  84. expect(called).toEqual({
  85. name: 'kernel',
  86. oldValue: original,
  87. newValue: defaultSession.kernel
  88. });
  89. original.dispose();
  90. });
  91. });
  92. describe('#statusChanged', () => {
  93. it('should emit when the kernel status changes', async () => {
  94. let called = false;
  95. defaultSession.statusChanged.connect((s, status) => {
  96. if (status === 'busy') {
  97. called = true;
  98. }
  99. });
  100. await defaultSession.kernel!.requestKernelInfo();
  101. expect(called).toBe(true);
  102. });
  103. });
  104. describe('#iopubMessage', () => {
  105. it('should be emitted for an iopub message', async () => {
  106. let called = false;
  107. defaultSession.iopubMessage.connect((s, msg) => {
  108. if (msg.header.msg_type === 'status') {
  109. called = true;
  110. }
  111. });
  112. await defaultSession.kernel!.requestExecute({ code: 'a=1' }, true).done;
  113. expect(called).toBe(true);
  114. });
  115. });
  116. describe('#unhandledMessage', () => {
  117. it('should be emitted for an unhandled message', async () => {
  118. const tester = new SessionTester();
  119. const session = await tester.startSession();
  120. const msgId = UUID.uuid4();
  121. const emission = testEmission(session.unhandledMessage, {
  122. find: (k, msg) => msg.header.msg_id === msgId
  123. });
  124. const msg = KernelMessage.createMessage({
  125. msgType: 'kernel_info_request',
  126. channel: 'shell',
  127. session: tester.serverSessionId,
  128. msgId,
  129. content: {}
  130. });
  131. msg.parent_header = { session: session.kernel!.clientId };
  132. tester.send(msg);
  133. await emission;
  134. await tester.shutdown();
  135. tester.dispose();
  136. });
  137. });
  138. describe('#propertyChanged', () => {
  139. it('should be emitted when the session path changes', async () => {
  140. const newPath = UUID.uuid4();
  141. let called = false;
  142. const object = {};
  143. defaultSession.propertyChanged.connect((s, type) => {
  144. expect(defaultSession.path).toBe(newPath);
  145. expect(type).toBe('path');
  146. called = true;
  147. Signal.disconnectReceiver(object);
  148. }, object);
  149. await defaultSession.setPath(newPath);
  150. expect(called).toBe(true);
  151. });
  152. });
  153. describe('#id', () => {
  154. it('should be a string', () => {
  155. expect(typeof defaultSession.id).toBe('string');
  156. });
  157. });
  158. describe('#path', () => {
  159. it('should be a string', () => {
  160. expect(typeof defaultSession.path).toBe('string');
  161. });
  162. });
  163. describe('#name', () => {
  164. it('should be a string', () => {
  165. expect(typeof defaultSession.name).toBe('string');
  166. });
  167. });
  168. describe('#type', () => {
  169. it('should be a string', () => {
  170. expect(typeof defaultSession.name).toBe('string');
  171. });
  172. });
  173. describe('#model', () => {
  174. it('should be an IModel', () => {
  175. const model = defaultSession.model;
  176. expect(typeof model.id).toBe('string');
  177. expect(typeof model.path).toBe('string');
  178. expect(typeof model.kernel!.name).toBe('string');
  179. expect(typeof model.kernel!.id).toBe('string');
  180. });
  181. });
  182. describe('#kernel', () => {
  183. it('should be an IKernel object', () => {
  184. expect(typeof defaultSession.kernel!.id).toBe('string');
  185. });
  186. });
  187. describe('#serverSettings', () => {
  188. it('should be the serverSettings', () => {
  189. expect(defaultSession.serverSettings.baseUrl).toBe(
  190. PageConfig.getBaseUrl()
  191. );
  192. });
  193. });
  194. describe('#isDisposed', () => {
  195. it('should be true after we dispose of the session', async () => {
  196. const session = await startNew();
  197. expect(session.isDisposed).toBe(false);
  198. session.dispose();
  199. expect(session.isDisposed).toBe(true);
  200. });
  201. it('should be safe to call multiple times', async () => {
  202. const session = await startNew();
  203. expect(session.isDisposed).toBe(false);
  204. expect(session.isDisposed).toBe(false);
  205. session.dispose();
  206. expect(session.isDisposed).toBe(true);
  207. expect(session.isDisposed).toBe(true);
  208. });
  209. });
  210. describe('#dispose()', () => {
  211. it('should dispose of the resources held by the session', async () => {
  212. const session = await startNew();
  213. session.dispose();
  214. expect(session.isDisposed).toBe(true);
  215. });
  216. it('should be safe to call twice', async () => {
  217. const session = await startNew();
  218. session.dispose();
  219. expect(session.isDisposed).toBe(true);
  220. session.dispose();
  221. expect(session.isDisposed).toBe(true);
  222. });
  223. it('should be safe to call if the kernel is disposed', async () => {
  224. const session = await startNew();
  225. session.kernel!.dispose();
  226. session.dispose();
  227. expect(session.isDisposed).toBe(true);
  228. });
  229. });
  230. describe('#setPath()', () => {
  231. it('should set the path of the session', async () => {
  232. const newPath = UUID.uuid4();
  233. await defaultSession.setPath(newPath);
  234. expect(defaultSession.path).toBe(newPath);
  235. });
  236. it('should fail for improper response status', async () => {
  237. handleRequest(defaultSession, 201, {});
  238. await expectFailure(defaultSession.setPath(UUID.uuid4()));
  239. });
  240. it('should fail for error response status', async () => {
  241. handleRequest(defaultSession, 500, {});
  242. await expectFailure(defaultSession.setPath(UUID.uuid4()), '');
  243. });
  244. it('should fail for improper model', async () => {
  245. handleRequest(defaultSession, 200, {});
  246. await expectFailure(defaultSession.setPath(UUID.uuid4()));
  247. });
  248. it('should fail if the session is disposed', async () => {
  249. const session = sessionManager.connectTo({
  250. model: defaultSession.model
  251. });
  252. session.dispose();
  253. const promise = session.setPath(UUID.uuid4());
  254. await expectFailure(promise, 'Session is disposed');
  255. });
  256. });
  257. describe('#setType()', () => {
  258. it('should set the type of the session', async () => {
  259. const session = await startNew();
  260. const type = UUID.uuid4();
  261. await session.setType(type);
  262. expect(session.type).toBe(type);
  263. await session.shutdown();
  264. });
  265. it('should fail for improper response status', async () => {
  266. handleRequest(defaultSession, 201, {});
  267. await expectFailure(defaultSession.setType(UUID.uuid4()));
  268. });
  269. it('should fail for error response status', async () => {
  270. handleRequest(defaultSession, 500, {});
  271. await expectFailure(defaultSession.setType(UUID.uuid4()), '');
  272. });
  273. it('should fail for improper model', async () => {
  274. handleRequest(defaultSession, 200, {});
  275. await expectFailure(defaultSession.setType(UUID.uuid4()));
  276. });
  277. it('should fail if the session is disposed', async () => {
  278. const session = sessionManager.connectTo({
  279. model: defaultSession.model
  280. });
  281. session.dispose();
  282. const promise = session.setPath(UUID.uuid4());
  283. await expectFailure(promise, 'Session is disposed');
  284. });
  285. });
  286. describe('#setName()', () => {
  287. it('should set the name of the session', async () => {
  288. const name = UUID.uuid4();
  289. await defaultSession.setName(name);
  290. expect(defaultSession.name).toBe(name);
  291. });
  292. it('should fail for improper response status', async () => {
  293. handleRequest(defaultSession, 201, {});
  294. await expectFailure(defaultSession.setName(UUID.uuid4()));
  295. });
  296. it('should fail for error response status', async () => {
  297. handleRequest(defaultSession, 500, {});
  298. await expectFailure(defaultSession.setName(UUID.uuid4()), '');
  299. });
  300. it('should fail for improper model', async () => {
  301. handleRequest(defaultSession, 200, {});
  302. await expectFailure(defaultSession.setName(UUID.uuid4()));
  303. });
  304. it('should fail if the session is disposed', async () => {
  305. const session = sessionManager.connectTo({
  306. model: defaultSession.model
  307. });
  308. session.dispose();
  309. const promise = session.setPath(UUID.uuid4());
  310. await expectFailure(promise, 'Session is disposed');
  311. });
  312. });
  313. describe('#changeKernel()', () => {
  314. it('should create a new kernel with the new name', async () => {
  315. session = await startNew();
  316. const previous = session.kernel!;
  317. await previous.info;
  318. await session.changeKernel({ name: previous.name });
  319. expect(session.kernel!.name).toBe(previous.name);
  320. expect(session.kernel!.id).not.toBe(previous.id);
  321. expect(session.kernel).not.toBe(previous);
  322. previous.dispose();
  323. });
  324. it('should accept the id of the new kernel', async () => {
  325. session = await startNew();
  326. const previous = session.kernel!;
  327. await previous.info;
  328. const kernel = await KernelAPI.startNew();
  329. await session.changeKernel({ id: kernel.id });
  330. expect(session.kernel!.id).toBe(kernel.id);
  331. expect(session.kernel).not.toBe(previous);
  332. expect(session.kernel).not.toBe(kernel);
  333. previous.dispose();
  334. await KernelAPI.shutdownKernel(kernel.id);
  335. });
  336. it('should update the session path if it has changed', async () => {
  337. session = await startNew();
  338. const previous = session.kernel!;
  339. await previous.info;
  340. const path = UUID.uuid4() + '.ipynb';
  341. const model = { ...session.model, path };
  342. handleRequest(session, 200, model);
  343. await session.changeKernel({ name: previous.name });
  344. expect(session.kernel!.name).toBe(previous.name);
  345. expect(session.path).toBe(model.path);
  346. previous.dispose();
  347. });
  348. });
  349. describe('#shutdown()', () => {
  350. it('should shut down properly', async () => {
  351. session = await startNew();
  352. await session.shutdown();
  353. });
  354. it('should emit a disposed signal', async () => {
  355. let called = false;
  356. session = await startNew();
  357. session.disposed.connect(() => {
  358. called = true;
  359. });
  360. await session.shutdown();
  361. expect(called).toBe(true);
  362. });
  363. it('should fail for an incorrect response status', async () => {
  364. handleRequest(defaultSession, 200, {});
  365. await expectFailure(defaultSession.shutdown());
  366. });
  367. it('should handle a 404 status', async () => {
  368. session = await startNew();
  369. handleRequest(session, 404, {});
  370. await session.shutdown();
  371. });
  372. it('should handle a specific error status', async () => {
  373. handleRequest(defaultSession, 410, {});
  374. const promise = defaultSession.shutdown();
  375. try {
  376. await promise;
  377. throw Error('should not get here');
  378. } catch (err) {
  379. const text = 'The kernel was deleted but the session was not';
  380. expect(err.message).toEqual(text);
  381. }
  382. });
  383. it('should fail for an error response status', async () => {
  384. handleRequest(defaultSession, 500, {});
  385. await expectFailure(defaultSession.shutdown(), '');
  386. });
  387. it('should fail if the session is disposed', async () => {
  388. const session = sessionManager.connectTo({
  389. model: defaultSession.model
  390. });
  391. session.dispose();
  392. await expectFailure(session.shutdown(), 'Session is disposed');
  393. });
  394. });
  395. });
  396. });