test_execute.py 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070
  1. # coding=utf-8
  2. """
  3. Module with tests for the execute preprocessor.
  4. """
  5. # Copyright (c) IPython Development Team.
  6. # Distributed under the terms of the Modified BSD License.
  7. from base64 import b64encode, b64decode
  8. import copy
  9. import glob
  10. import io
  11. import os
  12. import re
  13. import threading
  14. import multiprocessing as mp
  15. import nbformat
  16. import sys
  17. import pytest
  18. import functools
  19. from .base import PreprocessorTestsBase
  20. from ..execute import ExecutePreprocessor, CellExecutionError, executenb, DeadKernelError
  21. from ...exporters.exporter import ResourcesDict
  22. import IPython
  23. from traitlets import TraitError
  24. from nbformat import NotebookNode
  25. from jupyter_client.kernelspec import KernelSpecManager
  26. from nbconvert.filters import strip_ansi
  27. from testpath import modified_env
  28. from ipython_genutils.py3compat import string_types
  29. from pebble import ProcessPool
  30. try:
  31. from queue import Empty # Py 3
  32. except ImportError:
  33. from Queue import Empty # Py 2
  34. try:
  35. TimeoutError # Py 3
  36. except NameError:
  37. TimeoutError = RuntimeError # Py 2
  38. try:
  39. from unittest.mock import MagicMock, patch # Py 3
  40. except ImportError:
  41. from mock import MagicMock, patch # Py 2
  42. PY3 = False
  43. if sys.version_info[0] >= 3:
  44. PY3 = True
  45. addr_pat = re.compile(r'0x[0-9a-f]{7,9}')
  46. ipython_input_pat = re.compile(r'<ipython-input-\d+-[0-9a-f]+>')
  47. current_dir = os.path.dirname(__file__)
  48. IPY_MAJOR = IPython.version_info[0]
  49. def normalize_base64(b64_text):
  50. # if it's base64, pass it through b64 decode/encode to avoid
  51. # equivalent values from being considered unequal
  52. try:
  53. return b64encode(b64decode(b64_text.encode('ascii'))).decode('ascii')
  54. except (ValueError, TypeError):
  55. return b64_text
  56. def build_preprocessor(opts):
  57. """Make an instance of a preprocessor"""
  58. preprocessor = ExecutePreprocessor()
  59. preprocessor.enabled = True
  60. for opt in opts:
  61. setattr(preprocessor, opt, opts[opt])
  62. # Perform some state setup that should probably be in the init
  63. preprocessor._display_id_map = {}
  64. preprocessor.widget_state = {}
  65. preprocessor.widget_buffers = {}
  66. return preprocessor
  67. def run_notebook(filename, opts, resources):
  68. """Loads and runs a notebook, returning both the version prior to
  69. running it and the version after running it.
  70. """
  71. with io.open(filename) as f:
  72. input_nb = nbformat.read(f, 4)
  73. preprocessor = build_preprocessor(opts)
  74. cleaned_input_nb = copy.deepcopy(input_nb)
  75. for cell in cleaned_input_nb.cells:
  76. if 'execution_count' in cell:
  77. del cell['execution_count']
  78. cell['outputs'] = []
  79. # Override terminal size to standardise traceback format
  80. with modified_env({'COLUMNS': '80', 'LINES': '24'}):
  81. output_nb, _ = preprocessor(cleaned_input_nb, resources)
  82. return input_nb, output_nb
  83. def prepare_cell_mocks(*messages):
  84. """
  85. This function prepares a preprocessor object which has a fake kernel client
  86. to mock the messages sent over zeromq. The mock kernel client will return
  87. the messages passed into this wrapper back from `preproc.kc.iopub_channel.get_msg`
  88. callbacks. It also appends a kernel idle message to the end of messages.
  89. This allows for testing in with following call expectations:
  90. @prepare_cell_mocks({
  91. 'msg_type': 'stream',
  92. 'header': {'msg_type': 'stream'},
  93. 'content': {'name': 'stdout', 'text': 'foo'},
  94. })
  95. def test_message_foo(self, preprocessor, cell_mock, message_mock):
  96. preprocessor.kc.iopub_channel.get_msg()
  97. # =>
  98. # {
  99. # 'msg_type': 'stream',
  100. # 'parent_header': {'msg_id': 'fake_id'},
  101. # 'header': {'msg_type': 'stream'},
  102. # 'content': {'name': 'stdout', 'text': 'foo'},
  103. # }
  104. preprocessor.kc.iopub_channel.get_msg()
  105. # =>
  106. # {
  107. # 'msg_type': 'status',
  108. # 'parent_header': {'msg_id': 'fake_id'},
  109. # 'content': {'execution_state': 'idle'},
  110. # }
  111. preprocessor.kc.iopub_channel.get_msg() # => None
  112. message_mock.call_count # => 3
  113. """
  114. parent_id = 'fake_id'
  115. messages = list(messages)
  116. # Always terminate messages with an idle to exit the loop
  117. messages.append({'msg_type': 'status', 'content': {'execution_state': 'idle'}})
  118. def shell_channel_message_mock():
  119. # Return the message generator for
  120. # self.kc.shell_channel.get_msg => {'parent_header': {'msg_id': parent_id}}
  121. return MagicMock(return_value={'parent_header': {'msg_id': parent_id}})
  122. def iopub_messages_mock():
  123. # Return the message generator for
  124. # self.kc.iopub_channel.get_msg => messages[i]
  125. return MagicMock(
  126. side_effect=[
  127. # Default the parent_header so mocks don't need to include this
  128. PreprocessorTestsBase.merge_dicts(
  129. {'parent_header': {'msg_id': parent_id}}, msg)
  130. for msg in messages
  131. ]
  132. )
  133. def prepared_wrapper(func):
  134. @functools.wraps(func)
  135. def test_mock_wrapper(self):
  136. """
  137. This inner function wrapper populates the preprocessor object with
  138. the fake kernel client. This client has it's iopub and shell
  139. channels mocked so as to fake the setup handshake and return
  140. the messages passed into prepare_cell_mocks as the run_cell loop
  141. processes them.
  142. """
  143. cell_mock = NotebookNode(source='"foo" = "bar"', outputs=[])
  144. preprocessor = build_preprocessor({})
  145. preprocessor.nb = {'cells': [cell_mock]}
  146. # self.kc.iopub_channel.get_msg => message_mock.side_effect[i]
  147. message_mock = iopub_messages_mock()
  148. preprocessor.kc = MagicMock(
  149. iopub_channel=MagicMock(get_msg=message_mock),
  150. shell_channel=MagicMock(get_msg=shell_channel_message_mock()),
  151. execute=MagicMock(return_value=parent_id)
  152. )
  153. preprocessor.parent_id = parent_id
  154. return func(self, preprocessor, cell_mock, message_mock)
  155. return test_mock_wrapper
  156. return prepared_wrapper
  157. def normalize_output(output):
  158. """
  159. Normalizes outputs for comparison.
  160. """
  161. output = dict(output)
  162. if 'metadata' in output:
  163. del output['metadata']
  164. if 'text' in output:
  165. output['text'] = re.sub(addr_pat, '<HEXADDR>', output['text'])
  166. if 'text/plain' in output.get('data', {}):
  167. output['data']['text/plain'] = \
  168. re.sub(addr_pat, '<HEXADDR>', output['data']['text/plain'])
  169. if 'application/vnd.jupyter.widget-view+json' in output.get('data', {}):
  170. output['data']['application/vnd.jupyter.widget-view+json'] \
  171. ['model_id'] = '<MODEL_ID>'
  172. for key, value in output.get('data', {}).items():
  173. if isinstance(value, string_types):
  174. if sys.version_info.major == 2:
  175. value = value.replace('u\'', '\'')
  176. output['data'][key] = normalize_base64(value)
  177. if 'traceback' in output:
  178. tb = [
  179. re.sub(ipython_input_pat, '<IPY-INPUT>', strip_ansi(line))
  180. for line in output['traceback']
  181. ]
  182. output['traceback'] = tb
  183. return output
  184. def assert_notebooks_equal(expected, actual):
  185. expected_cells = expected['cells']
  186. actual_cells = actual['cells']
  187. assert len(expected_cells) == len(actual_cells)
  188. for expected_cell, actual_cell in zip(expected_cells, actual_cells):
  189. expected_outputs = expected_cell.get('outputs', [])
  190. actual_outputs = actual_cell.get('outputs', [])
  191. normalized_expected_outputs = list(map(normalize_output, expected_outputs))
  192. normalized_actual_outputs = list(map(normalize_output, actual_outputs))
  193. assert normalized_expected_outputs == normalized_actual_outputs
  194. expected_execution_count = expected_cell.get('execution_count', None)
  195. actual_execution_count = actual_cell.get('execution_count', None)
  196. assert expected_execution_count == actual_execution_count
  197. def notebook_resources():
  198. """Prepare a notebook resources dictionary for executing test notebooks in the `files` folder."""
  199. res = ResourcesDict()
  200. res['metadata'] = ResourcesDict()
  201. res['metadata']['path'] = os.path.join(current_dir, 'files')
  202. return res
  203. @pytest.mark.parametrize(
  204. ["input_name", "opts"],
  205. [
  206. ("Clear Output.ipynb", dict(kernel_name="python")),
  207. ("Empty Cell.ipynb", dict(kernel_name="python")),
  208. ("Factorials.ipynb", dict(kernel_name="python")),
  209. ("HelloWorld.ipynb", dict(kernel_name="python")),
  210. ("Inline Image.ipynb", dict(kernel_name="python")),
  211. ("Interrupt-IPY6.ipynb", dict(kernel_name="python", timeout=1, interrupt_on_timeout=True, allow_errors=True)) if IPY_MAJOR < 7 else
  212. ("Interrupt.ipynb", dict(kernel_name="python", timeout=1, interrupt_on_timeout=True, allow_errors=True)),
  213. ("JupyterWidgets.ipynb", dict(kernel_name="python")),
  214. ("Skip Exceptions with Cell Tags-IPY6.ipynb", dict(kernel_name="python")) if IPY_MAJOR < 7 else
  215. ("Skip Exceptions with Cell Tags.ipynb", dict(kernel_name="python")),
  216. ("Skip Exceptions-IPY6.ipynb", dict(kernel_name="python", allow_errors=True)) if IPY_MAJOR < 7 else
  217. ("Skip Exceptions.ipynb", dict(kernel_name="python", allow_errors=True)),
  218. ("SVG.ipynb", dict(kernel_name="python")),
  219. ("Unicode.ipynb", dict(kernel_name="python")),
  220. ("UnicodePy3.ipynb", dict(kernel_name="python")),
  221. ("update-display-id.ipynb", dict(kernel_name="python")),
  222. ("Check History in Memory.ipynb", dict(kernel_name="python")),
  223. ]
  224. )
  225. def test_run_all_notebooks(input_name, opts):
  226. """Runs a series of test notebooks and compares them to their actual output"""
  227. input_file = os.path.join(current_dir, 'files', input_name)
  228. input_nb, output_nb = run_notebook(input_file, opts, notebook_resources())
  229. assert_notebooks_equal(input_nb, output_nb)
  230. @pytest.mark.skipif(not PY3,
  231. reason = "Not tested for Python 2")
  232. def test_parallel_notebooks(capfd, tmpdir):
  233. """Two notebooks should be able to be run simultaneously without problems.
  234. The two notebooks spawned here use the filesystem to check that the other notebook
  235. wrote to the filesystem."""
  236. opts = dict(kernel_name="python")
  237. input_name = "Parallel Execute {label}.ipynb"
  238. input_file = os.path.join(current_dir, "files", input_name)
  239. res = notebook_resources()
  240. with modified_env({"NBEXECUTE_TEST_PARALLEL_TMPDIR": str(tmpdir)}):
  241. threads = [
  242. threading.Thread(
  243. target=run_notebook,
  244. args=(
  245. input_file.format(label=label),
  246. opts,
  247. res,
  248. ),
  249. )
  250. for label in ("A", "B")
  251. ]
  252. [t.start() for t in threads]
  253. [t.join(timeout=2) for t in threads]
  254. captured = capfd.readouterr()
  255. assert captured.err == ""
  256. @pytest.mark.skipif(not PY3,
  257. reason = "Not tested for Python 2")
  258. def test_many_parallel_notebooks(capfd):
  259. """Ensure that when many IPython kernels are run in parallel, nothing awful happens.
  260. Specifically, many IPython kernels when run simultaneously would enocunter errors
  261. due to using the same SQLite history database.
  262. """
  263. opts = dict(kernel_name="python", timeout=5)
  264. input_name = "HelloWorld.ipynb"
  265. input_file = os.path.join(current_dir, "files", input_name)
  266. res = PreprocessorTestsBase().build_resources()
  267. res["metadata"]["path"] = os.path.join(current_dir, "files")
  268. # run once, to trigger creating the original context
  269. run_notebook(input_file, opts, res)
  270. with ProcessPool(max_workers=4) as pool:
  271. futures = [
  272. # Travis needs a lot more time even though 10s is enough on most dev machines
  273. pool.schedule(run_notebook, args=(input_file, opts, res), timeout=30)
  274. for i in range(0, 8)
  275. ]
  276. for index, future in enumerate(futures):
  277. future.result()
  278. captured = capfd.readouterr()
  279. assert captured.err == ""
  280. class TestExecute(PreprocessorTestsBase):
  281. """Contains test functions for execute.py"""
  282. maxDiff = None
  283. def test_constructor(self):
  284. """Can a ExecutePreprocessor be constructed?"""
  285. build_preprocessor({})
  286. def test_populate_language_info(self):
  287. preprocessor = build_preprocessor(opts=dict(kernel_name="python"))
  288. nb = nbformat.v4.new_notebook() # Certainly has no language_info.
  289. nb, _ = preprocessor.preprocess(nb, resources={})
  290. assert 'language_info' in nb.metadata
  291. def test_empty_path(self):
  292. """Can the kernel be started when the path is empty?"""
  293. filename = os.path.join(current_dir, 'files', 'HelloWorld.ipynb')
  294. res = self.build_resources()
  295. res['metadata']['path'] = ''
  296. input_nb, output_nb = run_notebook(filename, {}, res)
  297. assert_notebooks_equal(input_nb, output_nb)
  298. @pytest.mark.xfail("python3" not in KernelSpecManager().find_kernel_specs(),
  299. reason="requires a python3 kernelspec")
  300. def test_empty_kernel_name(self):
  301. """Can kernel in nb metadata be found when an empty string is passed?
  302. Note: this pattern should be discouraged in practice.
  303. Passing in no kernel_name to ExecutePreprocessor is recommended instead.
  304. """
  305. filename = os.path.join(current_dir, 'files', 'UnicodePy3.ipynb')
  306. res = self.build_resources()
  307. input_nb, output_nb = run_notebook(filename, {"kernel_name": ""}, res)
  308. assert_notebooks_equal(input_nb, output_nb)
  309. with pytest.raises(TraitError):
  310. input_nb, output_nb = run_notebook(filename, {"kernel_name": None}, res)
  311. def test_disable_stdin(self):
  312. """Test disabling standard input"""
  313. filename = os.path.join(current_dir, 'files', 'Disable Stdin.ipynb')
  314. res = self.build_resources()
  315. res['metadata']['path'] = os.path.dirname(filename)
  316. input_nb, output_nb = run_notebook(filename, dict(allow_errors=True), res)
  317. # We need to special-case this particular notebook, because the
  318. # traceback contains machine-specific stuff like where IPython
  319. # is installed. It is sufficient here to just check that an error
  320. # was thrown, and that it was a StdinNotImplementedError
  321. self.assertEqual(len(output_nb['cells']), 1)
  322. self.assertEqual(len(output_nb['cells'][0]['outputs']), 1)
  323. output = output_nb['cells'][0]['outputs'][0]
  324. self.assertEqual(output['output_type'], 'error')
  325. self.assertEqual(output['ename'], 'StdinNotImplementedError')
  326. self.assertEqual(output['evalue'], 'raw_input was called, but this frontend does not support input requests.')
  327. def test_timeout(self):
  328. """Check that an error is raised when a computation times out"""
  329. filename = os.path.join(current_dir, 'files', 'Interrupt.ipynb')
  330. res = self.build_resources()
  331. res['metadata']['path'] = os.path.dirname(filename)
  332. with pytest.raises(TimeoutError):
  333. run_notebook(filename, dict(timeout=1), res)
  334. def test_timeout_func(self):
  335. """Check that an error is raised when a computation times out"""
  336. filename = os.path.join(current_dir, 'files', 'Interrupt.ipynb')
  337. res = self.build_resources()
  338. res['metadata']['path'] = os.path.dirname(filename)
  339. def timeout_func(source):
  340. return 10
  341. with pytest.raises(TimeoutError):
  342. run_notebook(filename, dict(timeout_func=timeout_func), res)
  343. def test_kernel_death(self):
  344. """Check that an error is raised when the kernel is_alive is false"""
  345. filename = os.path.join(current_dir, 'files', 'Interrupt.ipynb')
  346. with io.open(filename, 'r') as f:
  347. input_nb = nbformat.read(f, 4)
  348. res = self.build_resources()
  349. res['metadata']['path'] = os.path.dirname(filename)
  350. preprocessor = build_preprocessor({"timeout": 5})
  351. try:
  352. input_nb, output_nb = preprocessor(input_nb, {})
  353. except TimeoutError:
  354. pass
  355. km, kc = preprocessor.start_new_kernel()
  356. with patch.object(km, "is_alive") as alive_mock:
  357. alive_mock.return_value = False
  358. with pytest.raises(DeadKernelError):
  359. input_nb, output_nb = preprocessor.preprocess(input_nb, {}, km=km)
  360. def test_allow_errors(self):
  361. """
  362. Check that conversion halts if ``allow_errors`` is False.
  363. """
  364. filename = os.path.join(current_dir, 'files', 'Skip Exceptions.ipynb')
  365. res = self.build_resources()
  366. res['metadata']['path'] = os.path.dirname(filename)
  367. with pytest.raises(CellExecutionError) as exc:
  368. run_notebook(filename, dict(allow_errors=False), res)
  369. self.assertIsInstance(str(exc.value), str)
  370. if sys.version_info >= (3, 0):
  371. assert u"# üñîçø∂é" in str(exc.value)
  372. else:
  373. assert u"# üñîçø∂é".encode('utf8', 'replace') in str(exc.value)
  374. def test_force_raise_errors(self):
  375. """
  376. Check that conversion halts if the ``force_raise_errors`` traitlet on
  377. ExecutePreprocessor is set to True.
  378. """
  379. filename = os.path.join(current_dir, 'files',
  380. 'Skip Exceptions with Cell Tags.ipynb')
  381. res = self.build_resources()
  382. res['metadata']['path'] = os.path.dirname(filename)
  383. with pytest.raises(CellExecutionError) as exc:
  384. run_notebook(filename, dict(force_raise_errors=True), res)
  385. self.assertIsInstance(str(exc.value), str)
  386. if sys.version_info >= (3, 0):
  387. assert u"# üñîçø∂é" in str(exc.value)
  388. else:
  389. assert u"# üñîçø∂é".encode('utf8', 'replace') in str(exc.value)
  390. def test_custom_kernel_manager(self):
  391. from .fake_kernelmanager import FakeCustomKernelManager
  392. filename = os.path.join(current_dir, 'files', 'HelloWorld.ipynb')
  393. with io.open(filename) as f:
  394. input_nb = nbformat.read(f, 4)
  395. preprocessor = build_preprocessor({
  396. 'kernel_manager_class': FakeCustomKernelManager
  397. })
  398. cleaned_input_nb = copy.deepcopy(input_nb)
  399. for cell in cleaned_input_nb.cells:
  400. if 'execution_count' in cell:
  401. del cell['execution_count']
  402. cell['outputs'] = []
  403. # Override terminal size to standardise traceback format
  404. with modified_env({'COLUMNS': '80', 'LINES': '24'}):
  405. output_nb, _ = preprocessor(cleaned_input_nb,
  406. self.build_resources())
  407. expected = FakeCustomKernelManager.expected_methods.items()
  408. for method, call_count in expected:
  409. self.assertNotEqual(call_count, 0, '{} was called'.format(method))
  410. def test_process_message_wrapper(self):
  411. outputs = []
  412. class WrappedPreProc(ExecutePreprocessor):
  413. def process_message(self, msg, cell, cell_index):
  414. result = super(WrappedPreProc, self).process_message(msg, cell, cell_index)
  415. if result:
  416. outputs.append(result)
  417. return result
  418. current_dir = os.path.dirname(__file__)
  419. filename = os.path.join(current_dir, 'files', 'HelloWorld.ipynb')
  420. with io.open(filename) as f:
  421. input_nb = nbformat.read(f, 4)
  422. original = copy.deepcopy(input_nb)
  423. wpp = WrappedPreProc()
  424. executed = wpp.preprocess(input_nb, {})[0]
  425. assert outputs == [
  426. {'name': 'stdout', 'output_type': 'stream', 'text': 'Hello World\n'}
  427. ]
  428. assert_notebooks_equal(original, executed)
  429. def test_execute_function(self):
  430. # Test the executenb() convenience API
  431. filename = os.path.join(current_dir, 'files', 'HelloWorld.ipynb')
  432. with io.open(filename) as f:
  433. input_nb = nbformat.read(f, 4)
  434. original = copy.deepcopy(input_nb)
  435. executed = executenb(original, os.path.dirname(filename))
  436. assert_notebooks_equal(original, executed)
  437. def test_widgets(self):
  438. """Runs a test notebook with widgets and checks the widget state is saved."""
  439. input_file = os.path.join(current_dir, 'files', 'JupyterWidgets.ipynb')
  440. opts = dict(kernel_name="python")
  441. res = self.build_resources()
  442. res['metadata']['path'] = os.path.dirname(input_file)
  443. input_nb, output_nb = run_notebook(input_file, opts, res)
  444. output_data = [
  445. output.get('data', {})
  446. for cell in output_nb['cells']
  447. for output in cell['outputs']
  448. ]
  449. model_ids = [
  450. data['application/vnd.jupyter.widget-view+json']['model_id']
  451. for data in output_data
  452. if 'application/vnd.jupyter.widget-view+json' in data
  453. ]
  454. wdata = output_nb['metadata']['widgets'] \
  455. ['application/vnd.jupyter.widget-state+json']
  456. for k in model_ids:
  457. d = wdata['state'][k]
  458. assert 'model_name' in d
  459. assert 'model_module' in d
  460. assert 'state' in d
  461. assert 'version_major' in wdata
  462. assert 'version_minor' in wdata
  463. class TestRunCell(PreprocessorTestsBase):
  464. """Contains test functions for ExecutePreprocessor.run_cell"""
  465. @prepare_cell_mocks()
  466. def test_idle_message(self, preprocessor, cell_mock, message_mock):
  467. preprocessor.run_cell(cell_mock)
  468. # Just the exit message should be fetched
  469. assert message_mock.call_count == 1
  470. # Ensure no outputs were generated
  471. assert cell_mock.outputs == []
  472. @prepare_cell_mocks({
  473. 'msg_type': 'stream',
  474. 'header': {'msg_type': 'execute_reply'},
  475. 'parent_header': {'msg_id': 'wrong_parent'},
  476. 'content': {'name': 'stdout', 'text': 'foo'}
  477. })
  478. def test_message_for_wrong_parent(self, preprocessor, cell_mock, message_mock):
  479. preprocessor.run_cell(cell_mock)
  480. # An ignored stream followed by an idle
  481. assert message_mock.call_count == 2
  482. # Ensure no output was written
  483. assert cell_mock.outputs == []
  484. @prepare_cell_mocks({
  485. 'msg_type': 'status',
  486. 'header': {'msg_type': 'status'},
  487. 'content': {'execution_state': 'busy'}
  488. })
  489. def test_busy_message(self, preprocessor, cell_mock, message_mock):
  490. preprocessor.run_cell(cell_mock)
  491. # One busy message, followed by an idle
  492. assert message_mock.call_count == 2
  493. # Ensure no outputs were generated
  494. assert cell_mock.outputs == []
  495. @prepare_cell_mocks({
  496. 'msg_type': 'stream',
  497. 'header': {'msg_type': 'stream'},
  498. 'content': {'name': 'stdout', 'text': 'foo'},
  499. }, {
  500. 'msg_type': 'stream',
  501. 'header': {'msg_type': 'stream'},
  502. 'content': {'name': 'stderr', 'text': 'bar'}
  503. })
  504. def test_deadline_exec_reply(self, preprocessor, cell_mock, message_mock):
  505. # exec_reply is never received, so we expect to hit the timeout.
  506. preprocessor.kc.shell_channel.get_msg = MagicMock(side_effect=Empty())
  507. preprocessor.timeout = 1
  508. with pytest.raises(TimeoutError):
  509. preprocessor.run_cell(cell_mock)
  510. assert message_mock.call_count == 3
  511. # Ensure the output was captured
  512. self.assertListEqual(cell_mock.outputs, [
  513. {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'},
  514. {'output_type': 'stream', 'name': 'stderr', 'text': 'bar'}
  515. ])
  516. @prepare_cell_mocks()
  517. def test_deadline_iopub(self, preprocessor, cell_mock, message_mock):
  518. # The shell_channel will complete, so we expect only to hit the iopub timeout.
  519. message_mock.side_effect = Empty()
  520. preprocessor.raise_on_iopub_timeout = True
  521. with pytest.raises(TimeoutError):
  522. preprocessor.run_cell(cell_mock)
  523. @prepare_cell_mocks({
  524. 'msg_type': 'stream',
  525. 'header': {'msg_type': 'stream'},
  526. 'content': {'name': 'stdout', 'text': 'foo'},
  527. }, {
  528. 'msg_type': 'stream',
  529. 'header': {'msg_type': 'stream'},
  530. 'content': {'name': 'stderr', 'text': 'bar'}
  531. })
  532. def test_eventual_deadline_iopub(self, preprocessor, cell_mock, message_mock):
  533. # Process a few messages before raising a timeout from iopub
  534. message_mock.side_effect = list(message_mock.side_effect)[:-1] + [Empty()]
  535. preprocessor.kc.shell_channel.get_msg = MagicMock(
  536. return_value={'parent_header': {'msg_id': preprocessor.parent_id}})
  537. preprocessor.raise_on_iopub_timeout = True
  538. with pytest.raises(TimeoutError):
  539. preprocessor.run_cell(cell_mock)
  540. assert message_mock.call_count == 3
  541. # Ensure the output was captured
  542. self.assertListEqual(cell_mock.outputs, [
  543. {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'},
  544. {'output_type': 'stream', 'name': 'stderr', 'text': 'bar'}
  545. ])
  546. @prepare_cell_mocks({
  547. 'msg_type': 'execute_input',
  548. 'header': {'msg_type': 'execute_input'},
  549. 'content': {}
  550. })
  551. def test_execute_input_message(self, preprocessor, cell_mock, message_mock):
  552. preprocessor.run_cell(cell_mock)
  553. # One ignored execute_input, followed by an idle
  554. assert message_mock.call_count == 2
  555. # Ensure no outputs were generated
  556. assert cell_mock.outputs == []
  557. @prepare_cell_mocks({
  558. 'msg_type': 'stream',
  559. 'header': {'msg_type': 'stream'},
  560. 'content': {'name': 'stdout', 'text': 'foo'},
  561. }, {
  562. 'msg_type': 'stream',
  563. 'header': {'msg_type': 'stream'},
  564. 'content': {'name': 'stderr', 'text': 'bar'}
  565. })
  566. def test_stream_messages(self, preprocessor, cell_mock, message_mock):
  567. preprocessor.run_cell(cell_mock)
  568. # An stdout then stderr stream followed by an idle
  569. assert message_mock.call_count == 3
  570. # Ensure the output was captured
  571. self.assertListEqual(cell_mock.outputs, [
  572. {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'},
  573. {'output_type': 'stream', 'name': 'stderr', 'text': 'bar'}
  574. ])
  575. @prepare_cell_mocks({
  576. 'msg_type': 'stream',
  577. 'header': {'msg_type': 'execute_reply'},
  578. 'content': {'name': 'stdout', 'text': 'foo'}
  579. }, {
  580. 'msg_type': 'clear_output',
  581. 'header': {'msg_type': 'clear_output'},
  582. 'content': {}
  583. })
  584. def test_clear_output_message(self, preprocessor, cell_mock, message_mock):
  585. preprocessor.run_cell(cell_mock)
  586. # A stream, followed by a clear, and then an idle
  587. assert message_mock.call_count == 3
  588. # Ensure the output was cleared
  589. assert cell_mock.outputs == []
  590. @prepare_cell_mocks({
  591. 'msg_type': 'stream',
  592. 'header': {'msg_type': 'stream'},
  593. 'content': {'name': 'stdout', 'text': 'foo'}
  594. }, {
  595. 'msg_type': 'clear_output',
  596. 'header': {'msg_type': 'clear_output'},
  597. 'content': {'wait': True}
  598. })
  599. def test_clear_output_wait_message(self, preprocessor, cell_mock, message_mock):
  600. preprocessor.run_cell(cell_mock)
  601. # A stream, followed by a clear, and then an idle
  602. assert message_mock.call_count == 3
  603. # Should be true without another message to trigger the clear
  604. self.assertTrue(preprocessor.clear_before_next_output)
  605. # Ensure the output wasn't cleared yet
  606. assert cell_mock.outputs == [
  607. {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'}
  608. ]
  609. @prepare_cell_mocks({
  610. 'msg_type': 'stream',
  611. 'header': {'msg_type': 'stream'},
  612. 'content': {'name': 'stdout', 'text': 'foo'}
  613. }, {
  614. 'msg_type': 'clear_output',
  615. 'header': {'msg_type': 'clear_output'},
  616. 'content': {'wait': True}
  617. }, {
  618. 'msg_type': 'stream',
  619. 'header': {'msg_type': 'stream'},
  620. 'content': {'name': 'stderr', 'text': 'bar'}
  621. })
  622. def test_clear_output_wait_then_message_message(self, preprocessor, cell_mock, message_mock):
  623. preprocessor.run_cell(cell_mock)
  624. # An stdout stream, followed by a wait clear, an stderr stream, and then an idle
  625. assert message_mock.call_count == 4
  626. # Should be false after the stderr message
  627. assert not preprocessor.clear_before_next_output
  628. # Ensure the output wasn't cleared yet
  629. assert cell_mock.outputs == [
  630. {'output_type': 'stream', 'name': 'stderr', 'text': 'bar'}
  631. ]
  632. @prepare_cell_mocks({
  633. 'msg_type': 'stream',
  634. 'header': {'msg_type': 'stream'},
  635. 'content': {'name': 'stdout', 'text': 'foo'}
  636. }, {
  637. 'msg_type': 'clear_output',
  638. 'header': {'msg_type': 'clear_output'},
  639. 'content': {'wait': True}
  640. }, {
  641. 'msg_type': 'update_display_data',
  642. 'header': {'msg_type': 'update_display_data'},
  643. 'content': {'metadata': {'metafoo': 'metabar'}, 'data': {'foo': 'bar'}}
  644. })
  645. def test_clear_output_wait_then_update_display_message(self, preprocessor, cell_mock, message_mock):
  646. preprocessor.run_cell(cell_mock)
  647. # An stdout stream, followed by a wait clear, an stderr stream, and then an idle
  648. assert message_mock.call_count == 4
  649. # Should be false after the stderr message
  650. assert preprocessor.clear_before_next_output
  651. # Ensure the output wasn't cleared yet because update_display doesn't add outputs
  652. assert cell_mock.outputs == [
  653. {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'}
  654. ]
  655. @prepare_cell_mocks({
  656. 'msg_type': 'execute_reply',
  657. 'header': {'msg_type': 'execute_reply'},
  658. 'content': {'execution_count': 42}
  659. })
  660. def test_execution_count_message(self, preprocessor, cell_mock, message_mock):
  661. preprocessor.run_cell(cell_mock)
  662. # An execution count followed by an idle
  663. assert message_mock.call_count == 2
  664. assert cell_mock.execution_count == 42
  665. # Ensure no outputs were generated
  666. assert cell_mock.outputs == []
  667. @prepare_cell_mocks({
  668. 'msg_type': 'stream',
  669. 'header': {'msg_type': 'stream'},
  670. 'content': {'execution_count': 42, 'name': 'stdout', 'text': 'foo'}
  671. })
  672. def test_execution_count_with_stream_message(self, preprocessor, cell_mock, message_mock):
  673. preprocessor.run_cell(cell_mock)
  674. # An execution count followed by an idle
  675. assert message_mock.call_count == 2
  676. assert cell_mock.execution_count == 42
  677. # Should also consume the message stream
  678. assert cell_mock.outputs == [
  679. {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'}
  680. ]
  681. @prepare_cell_mocks({
  682. 'msg_type': 'comm',
  683. 'header': {'msg_type': 'comm'},
  684. 'content': {
  685. 'comm_id': 'foobar',
  686. 'data': {'state': {'foo': 'bar'}}
  687. }
  688. })
  689. def test_widget_comm_message(self, preprocessor, cell_mock, message_mock):
  690. preprocessor.run_cell(cell_mock)
  691. # A comm message without buffer info followed by an idle
  692. assert message_mock.call_count == 2
  693. self.assertEqual(preprocessor.widget_state, {'foobar': {'foo': 'bar'}})
  694. # Buffers should still be empty
  695. assert not preprocessor.widget_buffers
  696. # Ensure no outputs were generated
  697. assert cell_mock.outputs == []
  698. @prepare_cell_mocks({
  699. 'msg_type': 'comm',
  700. 'header': {'msg_type': 'comm'},
  701. 'buffers': [b'123'],
  702. 'content': {
  703. 'comm_id': 'foobar',
  704. 'data': {
  705. 'state': {'foo': 'bar'},
  706. 'buffer_paths': ['path']
  707. }
  708. }
  709. })
  710. def test_widget_comm_buffer_message(self, preprocessor, cell_mock, message_mock):
  711. preprocessor.run_cell(cell_mock)
  712. # A comm message with buffer info followed by an idle
  713. assert message_mock.call_count == 2
  714. assert preprocessor.widget_state == {'foobar': {'foo': 'bar'}}
  715. assert preprocessor.widget_buffers == {
  716. 'foobar': [{'data': 'MTIz', 'encoding': 'base64', 'path': 'path'}]
  717. }
  718. # Ensure no outputs were generated
  719. assert cell_mock.outputs == []
  720. @prepare_cell_mocks({
  721. 'msg_type': 'comm',
  722. 'header': {'msg_type': 'comm'},
  723. 'content': {
  724. 'comm_id': 'foobar',
  725. # No 'state'
  726. 'data': {'foo': 'bar'}
  727. }
  728. })
  729. def test_unknown_comm_message(self, preprocessor, cell_mock, message_mock):
  730. preprocessor.run_cell(cell_mock)
  731. # An unknown comm message followed by an idle
  732. assert message_mock.call_count == 2
  733. # Widget states should be empty as the message has the wrong shape
  734. assert not preprocessor.widget_state
  735. assert not preprocessor.widget_buffers
  736. # Ensure no outputs were generated
  737. assert cell_mock.outputs == []
  738. @prepare_cell_mocks({
  739. 'msg_type': 'execute_result',
  740. 'header': {'msg_type': 'execute_result'},
  741. 'content': {
  742. 'metadata': {'metafoo': 'metabar'},
  743. 'data': {'foo': 'bar'},
  744. 'execution_count': 42
  745. }
  746. })
  747. def test_execute_result_message(self, preprocessor, cell_mock, message_mock):
  748. preprocessor.run_cell(cell_mock)
  749. # An execute followed by an idle
  750. assert message_mock.call_count == 2
  751. assert cell_mock.execution_count == 42
  752. # Should generate an associated message
  753. assert cell_mock.outputs == [{
  754. 'output_type': 'execute_result',
  755. 'metadata': {'metafoo': 'metabar'},
  756. 'data': {'foo': 'bar'},
  757. 'execution_count': 42
  758. }]
  759. # No display id was provided
  760. assert not preprocessor._display_id_map
  761. @prepare_cell_mocks({
  762. 'msg_type': 'execute_result',
  763. 'header': {'msg_type': 'execute_result'},
  764. 'content': {
  765. 'transient': {'display_id': 'foobar'},
  766. 'metadata': {'metafoo': 'metabar'},
  767. 'data': {'foo': 'bar'},
  768. 'execution_count': 42
  769. }
  770. })
  771. def test_execute_result_with_display_message(self, preprocessor, cell_mock, message_mock):
  772. preprocessor.run_cell(cell_mock)
  773. # An execute followed by an idle
  774. assert message_mock.call_count == 2
  775. assert cell_mock.execution_count == 42
  776. # Should generate an associated message
  777. assert cell_mock.outputs == [{
  778. 'output_type': 'execute_result',
  779. 'metadata': {'metafoo': 'metabar'},
  780. 'data': {'foo': 'bar'},
  781. 'execution_count': 42
  782. }]
  783. assert 'foobar' in preprocessor._display_id_map
  784. @prepare_cell_mocks({
  785. 'msg_type': 'display_data',
  786. 'header': {'msg_type': 'display_data'},
  787. 'content': {'metadata': {'metafoo': 'metabar'}, 'data': {'foo': 'bar'}}
  788. })
  789. def test_display_data_without_id_message(self, preprocessor, cell_mock, message_mock):
  790. preprocessor.run_cell(cell_mock)
  791. # A display followed by an idle
  792. assert message_mock.call_count == 2
  793. # Should generate an associated message
  794. assert cell_mock.outputs == [{
  795. 'output_type': 'display_data',
  796. 'metadata': {'metafoo': 'metabar'},
  797. 'data': {'foo': 'bar'}
  798. }]
  799. # No display id was provided
  800. assert not preprocessor._display_id_map
  801. @prepare_cell_mocks({
  802. 'msg_type': 'display_data',
  803. 'header': {'msg_type': 'display_data'},
  804. 'content': {
  805. 'transient': {'display_id': 'foobar'},
  806. 'metadata': {'metafoo': 'metabar'},
  807. 'data': {'foo': 'bar'}
  808. }
  809. })
  810. def test_display_data_message(self, preprocessor, cell_mock, message_mock):
  811. preprocessor.run_cell(cell_mock)
  812. # A display followed by an idle
  813. assert message_mock.call_count == 2
  814. # Should generate an associated message
  815. assert cell_mock.outputs == [{
  816. 'output_type': 'display_data',
  817. 'metadata': {'metafoo': 'metabar'},
  818. 'data': {'foo': 'bar'}
  819. }]
  820. assert 'foobar' in preprocessor._display_id_map
  821. @prepare_cell_mocks({
  822. 'msg_type': 'display_data',
  823. 'header': {'msg_type': 'display_data'},
  824. 'content': {
  825. 'transient': {'display_id': 'foobar'},
  826. 'metadata': {'metafoo': 'metabar'},
  827. 'data': {'foo': 'bar'}
  828. }
  829. }, {
  830. 'msg_type': 'display_data',
  831. 'header': {'msg_type': 'display_data'},
  832. 'content': {
  833. 'transient': {'display_id': 'foobar_other'},
  834. 'metadata': {'metafoo_other': 'metabar_other'},
  835. 'data': {'foo': 'bar_other'}
  836. }
  837. }, {
  838. 'msg_type': 'display_data',
  839. 'header': {'msg_type': 'display_data'},
  840. 'content': {
  841. 'transient': {'display_id': 'foobar'},
  842. 'metadata': {'metafoo2': 'metabar2'},
  843. 'data': {'foo': 'bar2', 'baz': 'foobarbaz'}
  844. }
  845. })
  846. def test_display_data_same_id_message(self, preprocessor, cell_mock, message_mock):
  847. preprocessor.run_cell(cell_mock)
  848. # A display followed by an idle
  849. assert message_mock.call_count == 4
  850. # Original output should be manipulated and a copy of the second now
  851. assert cell_mock.outputs == [{
  852. 'output_type': 'display_data',
  853. 'metadata': {'metafoo2': 'metabar2'},
  854. 'data': {'foo': 'bar2', 'baz': 'foobarbaz'}
  855. }, {
  856. 'output_type': 'display_data',
  857. 'metadata': {'metafoo_other': 'metabar_other'},
  858. 'data': {'foo': 'bar_other'}
  859. }, {
  860. 'output_type': 'display_data',
  861. 'metadata': {'metafoo2': 'metabar2'},
  862. 'data': {'foo': 'bar2', 'baz': 'foobarbaz'}
  863. }]
  864. assert 'foobar' in preprocessor._display_id_map
  865. @prepare_cell_mocks({
  866. 'msg_type': 'update_display_data',
  867. 'header': {'msg_type': 'update_display_data'},
  868. 'content': {'metadata': {'metafoo': 'metabar'}, 'data': {'foo': 'bar'}}
  869. })
  870. def test_update_display_data_without_id_message(self, preprocessor, cell_mock, message_mock):
  871. preprocessor.run_cell(cell_mock)
  872. # An update followed by an idle
  873. assert message_mock.call_count == 2
  874. # Display updates don't create any outputs
  875. assert cell_mock.outputs == []
  876. # No display id was provided
  877. assert not preprocessor._display_id_map
  878. @prepare_cell_mocks({
  879. 'msg_type': 'display_data',
  880. 'header': {'msg_type': 'display_data'},
  881. 'content': {
  882. 'transient': {'display_id': 'foobar'},
  883. 'metadata': {'metafoo2': 'metabar2'},
  884. 'data': {'foo': 'bar2', 'baz': 'foobarbaz'}
  885. }
  886. }, {
  887. 'msg_type': 'update_display_data',
  888. 'header': {'msg_type': 'update_display_data'},
  889. 'content': {
  890. 'transient': {'display_id': 'foobar2'},
  891. 'metadata': {'metafoo2': 'metabar2'},
  892. 'data': {'foo': 'bar2', 'baz': 'foobarbaz'}
  893. }
  894. })
  895. def test_update_display_data_mismatch_id_message(self, preprocessor, cell_mock, message_mock):
  896. preprocessor.run_cell(cell_mock)
  897. # An update followed by an idle
  898. assert message_mock.call_count == 3
  899. # Display updates don't create any outputs
  900. assert cell_mock.outputs == [{
  901. 'output_type': 'display_data',
  902. 'metadata': {'metafoo2': 'metabar2'},
  903. 'data': {'foo': 'bar2', 'baz': 'foobarbaz'}
  904. }]
  905. assert 'foobar' in preprocessor._display_id_map
  906. @prepare_cell_mocks({
  907. 'msg_type': 'display_data',
  908. 'header': {'msg_type': 'display_data'},
  909. 'content': {
  910. 'transient': {'display_id': 'foobar'},
  911. 'metadata': {'metafoo': 'metabar'},
  912. 'data': {'foo': 'bar'}
  913. }
  914. }, {
  915. 'msg_type': 'update_display_data',
  916. 'header': {'msg_type': 'update_display_data'},
  917. 'content': {
  918. 'transient': {'display_id': 'foobar'},
  919. 'metadata': {'metafoo2': 'metabar2'},
  920. 'data': {'foo': 'bar2', 'baz': 'foobarbaz'}
  921. }
  922. })
  923. def test_update_display_data_message(self, preprocessor, cell_mock, message_mock):
  924. preprocessor.run_cell(cell_mock)
  925. # A display followed by an update then an idle
  926. assert message_mock.call_count == 3
  927. # Original output should be manipulated
  928. assert cell_mock.outputs == [{
  929. 'output_type': 'display_data',
  930. 'metadata': {'metafoo2': 'metabar2'},
  931. 'data': {'foo': 'bar2', 'baz': 'foobarbaz'}
  932. }]
  933. assert 'foobar' in preprocessor._display_id_map
  934. @prepare_cell_mocks({
  935. 'msg_type': 'error',
  936. 'header': {'msg_type': 'error'},
  937. 'content': {'ename': 'foo', 'evalue': 'bar', 'traceback': ['Boom']}
  938. })
  939. def test_error_message(self, preprocessor, cell_mock, message_mock):
  940. preprocessor.run_cell(cell_mock)
  941. # An error followed by an idle
  942. assert message_mock.call_count == 2
  943. # Should also consume the message stream
  944. assert cell_mock.outputs == [{
  945. 'output_type': 'error',
  946. 'ename': 'foo',
  947. 'evalue': 'bar',
  948. 'traceback': ['Boom']
  949. }]