|
| 1 | +# Copyright (c) Jupyter Development Team. |
| 2 | +# Distributed under the terms of the Modified BSD License. |
| 3 | + |
| 4 | +import os |
| 5 | +import json |
| 6 | +import pytest |
| 7 | + |
| 8 | +from binascii import hexlify |
| 9 | + |
| 10 | +import urllib.parse |
| 11 | +import tornado |
| 12 | +from tornado.escape import url_escape |
| 13 | + |
| 14 | +from traitlets.config import Config |
| 15 | + |
| 16 | +from jupyter_server.extension import serverextension |
| 17 | +from jupyter_server.serverapp import ServerApp |
| 18 | +from jupyter_server.utils import url_path_join |
| 19 | +from jupyter_server.services.contents.filemanager import FileContentsManager |
| 20 | + |
| 21 | +import nbformat |
| 22 | + |
| 23 | +# This shouldn't be needed anymore, since pytest_tornasync is found in entrypoints |
| 24 | +pytest_plugins = "pytest_tornasync" |
| 25 | + |
| 26 | +# NOTE: This is a temporary fix for Windows 3.8 |
| 27 | +# We have to override the io_loop fixture with an |
| 28 | +# asyncio patch. This will probably be removed in |
| 29 | +# the future. |
| 30 | + |
| 31 | +@pytest.fixture |
| 32 | +def jp_asyncio_patch(): |
| 33 | + ServerApp()._init_asyncio_patch() |
| 34 | + |
| 35 | +@pytest.fixture |
| 36 | +def io_loop(jp_asyncio_patch): |
| 37 | + loop = tornado.ioloop.IOLoop() |
| 38 | + loop.make_current() |
| 39 | + yield loop |
| 40 | + loop.clear_current() |
| 41 | + loop.close(all_fds=True) |
| 42 | + |
| 43 | +jp_server_config = pytest.fixture(lambda: {}) |
| 44 | + |
| 45 | +some_resource = u"The very model of a modern major general" |
| 46 | +sample_kernel_json = { |
| 47 | + 'argv':['cat', '{connection_file}'], |
| 48 | + 'display_name': 'Test kernel', |
| 49 | +} |
| 50 | +jp_argv = pytest.fixture(lambda: []) |
| 51 | + |
| 52 | + |
| 53 | + |
| 54 | +@pytest.fixture |
| 55 | +def jp_extension_environ(jp_env_config_path, monkeypatch): |
| 56 | + """Monkeypatch a Jupyter Extension's config path into each test's environment variable""" |
| 57 | + monkeypatch.setattr(serverextension, "ENV_CONFIG_PATH", [str(jp_env_config_path)]) |
| 58 | + |
| 59 | + |
| 60 | +@pytest.fixture |
| 61 | +def jp_http_port(http_server_port): |
| 62 | + return http_server_port[-1] |
| 63 | + |
| 64 | + |
| 65 | +@pytest.fixture(scope='function') |
| 66 | +def jp_configurable_serverapp( |
| 67 | + jp_environ, |
| 68 | + jp_server_config, |
| 69 | + jp_argv, |
| 70 | + jp_http_port, |
| 71 | + tmp_path, |
| 72 | + jp_root_dir, |
| 73 | + io_loop, |
| 74 | +): |
| 75 | + ServerApp.clear_instance() |
| 76 | + |
| 77 | + def _configurable_serverapp( |
| 78 | + config=jp_server_config, |
| 79 | + argv=jp_argv, |
| 80 | + environ=jp_environ, |
| 81 | + http_port=jp_http_port, |
| 82 | + tmp_path=tmp_path, |
| 83 | + root_dir=jp_root_dir, |
| 84 | + **kwargs |
| 85 | + ): |
| 86 | + c = Config(config) |
| 87 | + c.NotebookNotary.db_file = ":memory:" |
| 88 | + token = hexlify(os.urandom(4)).decode("ascii") |
| 89 | + url_prefix = "/" |
| 90 | + app = ServerApp.instance( |
| 91 | + # Set the log level to debug for testing purposes |
| 92 | + log_level='DEBUG', |
| 93 | + port=http_port, |
| 94 | + port_retries=0, |
| 95 | + open_browser=False, |
| 96 | + root_dir=str(root_dir), |
| 97 | + base_url=url_prefix, |
| 98 | + config=c, |
| 99 | + allow_root=True, |
| 100 | + token=token, |
| 101 | + **kwargs |
| 102 | + ) |
| 103 | + |
| 104 | + app.init_signal = lambda: None |
| 105 | + app.log.propagate = True |
| 106 | + app.log.handlers = [] |
| 107 | + # Initialize app without httpserver |
| 108 | + app.initialize(argv=argv, new_httpserver=False) |
| 109 | + app.log.propagate = True |
| 110 | + app.log.handlers = [] |
| 111 | + # Start app without ioloop |
| 112 | + app.start_app() |
| 113 | + return app |
| 114 | + |
| 115 | + return _configurable_serverapp |
| 116 | + |
| 117 | + |
| 118 | +@pytest.fixture(scope="function") |
| 119 | +def jp_serverapp(jp_server_config, jp_argv, jp_configurable_serverapp): |
| 120 | + app = jp_configurable_serverapp(config=jp_server_config, argv=jp_argv) |
| 121 | + yield app |
| 122 | + app.remove_server_info_file() |
| 123 | + app.remove_browser_open_file() |
| 124 | + app.cleanup_kernels() |
| 125 | + |
| 126 | + |
| 127 | +@pytest.fixture |
| 128 | +def app(jp_serverapp): |
| 129 | + """app fixture is needed by pytest_tornasync plugin""" |
| 130 | + return jp_serverapp.web_app |
| 131 | + |
| 132 | + |
| 133 | +@pytest.fixture |
| 134 | +def jp_auth_header(jp_serverapp): |
| 135 | + return {"Authorization": "token {token}".format(token=jp_serverapp.token)} |
| 136 | + |
| 137 | + |
| 138 | +@pytest.fixture |
| 139 | +def jp_base_url(): |
| 140 | + return "/" |
| 141 | + |
| 142 | + |
| 143 | +@pytest.fixture |
| 144 | +def jp_fetch(http_server_client, jp_auth_header, jp_base_url): |
| 145 | + """fetch fixture that handles auth, base_url, and path""" |
| 146 | + def client_fetch(*parts, headers={}, params={}, **kwargs): |
| 147 | + # Handle URL strings |
| 148 | + path_url = url_escape(url_path_join(jp_base_url, *parts), plus=False) |
| 149 | + params_url = urllib.parse.urlencode(params) |
| 150 | + url = path_url + "?" + params_url |
| 151 | + # Add auth keys to header |
| 152 | + headers.update(jp_auth_header) |
| 153 | + # Make request. |
| 154 | + return http_server_client.fetch( |
| 155 | + url, headers=headers, request_timeout=20, **kwargs |
| 156 | + ) |
| 157 | + return client_fetch |
| 158 | + |
| 159 | + |
| 160 | +@pytest.fixture |
| 161 | +def jp_ws_fetch(jp_auth_header, jp_http_port): |
| 162 | + """websocket fetch fixture that handles auth, base_url, and path""" |
| 163 | + def client_fetch(*parts, headers={}, params={}, **kwargs): |
| 164 | + # Handle URL strings |
| 165 | + path = url_escape(url_path_join(*parts), plus=False) |
| 166 | + urlparts = urllib.parse.urlparse('ws://localhost:{}'.format(jp_http_port)) |
| 167 | + urlparts = urlparts._replace( |
| 168 | + path=path, |
| 169 | + query=urllib.parse.urlencode(params) |
| 170 | + ) |
| 171 | + url = urlparts.geturl() |
| 172 | + # Add auth keys to header |
| 173 | + headers.update(jp_auth_header) |
| 174 | + # Make request. |
| 175 | + req = tornado.httpclient.HTTPRequest( |
| 176 | + url, |
| 177 | + headers=jp_auth_header, |
| 178 | + connect_timeout=120 |
| 179 | + ) |
| 180 | + return tornado.websocket.websocket_connect(req) |
| 181 | + return client_fetch |
| 182 | + |
| 183 | + |
| 184 | +@pytest.fixture |
| 185 | +def jp_kernelspecs(jp_data_dir): |
| 186 | + spec_names = ['sample', 'sample 2'] |
| 187 | + for name in spec_names: |
| 188 | + sample_kernel_dir = jp_data_dir.joinpath('kernels', name) |
| 189 | + sample_kernel_dir.mkdir(parents=True) |
| 190 | + # Create kernel json file |
| 191 | + sample_kernel_file = sample_kernel_dir.joinpath('kernel.json') |
| 192 | + sample_kernel_file.write_text(json.dumps(sample_kernel_json)) |
| 193 | + # Create resources text |
| 194 | + sample_kernel_resources = sample_kernel_dir.joinpath('resource.txt') |
| 195 | + sample_kernel_resources.write_text(some_resource) |
| 196 | + |
| 197 | + |
| 198 | +@pytest.fixture(params=[True, False]) |
| 199 | +def jp_contents_manager(request, tmp_path): |
| 200 | + return FileContentsManager(root_dir=str(tmp_path), use_atomic_writing=request.param) |
| 201 | + |
| 202 | + |
| 203 | +@pytest.fixture |
| 204 | +def jp_create_notebook(jp_root_dir): |
| 205 | + """Create a notebook in the test's home directory.""" |
| 206 | + def inner(nbpath): |
| 207 | + nbpath = jp_root_dir.joinpath(nbpath) |
| 208 | + # Check that the notebook has the correct file extension. |
| 209 | + if nbpath.suffix != '.ipynb': |
| 210 | + raise Exception("File extension for notebook must be .ipynb") |
| 211 | + # If the notebook path has a parent directory, make sure it's created. |
| 212 | + parent = nbpath.parent |
| 213 | + parent.mkdir(parents=True, exist_ok=True) |
| 214 | + # Create a notebook string and write to file. |
| 215 | + nb = nbformat.v4.new_notebook() |
| 216 | + nbtext = nbformat.writes(nb, version=4) |
| 217 | + nbpath.write_text(nbtext) |
| 218 | + return inner |
0 commit comments