|
@@ -4,17 +4,15 @@
|
|
|
This module is meant to run JupyterLab in a headless browser, making sure
|
|
|
the application launches and starts up without errors.
|
|
|
"""
|
|
|
-import asyncio
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
-import inspect
|
|
|
import logging
|
|
|
from os import path as osp
|
|
|
import os
|
|
|
import shutil
|
|
|
import sys
|
|
|
+import subprocess
|
|
|
|
|
|
from tornado.ioloop import IOLoop
|
|
|
-from tornado.iostream import StreamClosedError
|
|
|
from jupyter_server.serverapp import flags, aliases, ServerApp
|
|
|
from jupyter_server.utils import urljoin, pathname2url
|
|
|
from traitlets import Bool, Dict
|
|
@@ -47,11 +45,11 @@ class LogErrorHandler(logging.Handler):
|
|
|
self.errored = False
|
|
|
|
|
|
def filter(self, record):
|
|
|
- # Handle known StreamClosedError from Tornado
|
|
|
- # These occur when we forcibly close Websockets or
|
|
|
- # browser connections during the test.
|
|
|
- # https://github.com/tornadoweb/tornado/issues/2834
|
|
|
- if hasattr(record, 'exc_info') and isinstance(record.exc_info[1], StreamClosedError):
|
|
|
+ # known startup error message
|
|
|
+ if 'paste' in record.msg:
|
|
|
+ return
|
|
|
+ # handle known shutdown message
|
|
|
+ if 'Stream is closed' in record.msg:
|
|
|
return
|
|
|
return super().filter(record)
|
|
|
|
|
@@ -61,30 +59,41 @@ class LogErrorHandler(logging.Handler):
|
|
|
|
|
|
|
|
|
def run_test(app, func):
|
|
|
- """Synchronous entry point to run a test function.
|
|
|
-
|
|
|
- func is a function that accepts an app url as a parameter and returns a result.
|
|
|
-
|
|
|
- func can be synchronous or asynchronous. If it is synchronous, it will be run
|
|
|
- in a thread, so asynchronous is preferred.
|
|
|
- """
|
|
|
- IOLoop.current().spawn_callback(run_test_async, app, func)
|
|
|
-
|
|
|
-
|
|
|
-async def run_test_async(app, func):
|
|
|
"""Run a test against the application.
|
|
|
-
|
|
|
func is a function that accepts an app url as a parameter and returns a result.
|
|
|
-
|
|
|
- func can be synchronous or asynchronous. If it is synchronous, it will be run
|
|
|
- in a thread, so asynchronous is preferred.
|
|
|
"""
|
|
|
handler = LogErrorHandler()
|
|
|
- app.log.addHandler(handler)
|
|
|
|
|
|
env_patch = TestEnv()
|
|
|
env_patch.start()
|
|
|
|
|
|
+ def finished(future):
|
|
|
+ try:
|
|
|
+ result = future.result()
|
|
|
+ except Exception as e:
|
|
|
+ app.log.error(str(e))
|
|
|
+ app.log.info('Stopping server...')
|
|
|
+ app.stop()
|
|
|
+ if handler.errored:
|
|
|
+ app.log.critical('Exiting with 1 due to errors')
|
|
|
+ result = 1
|
|
|
+ elif result != 0:
|
|
|
+ app.log.critical('Exiting with %s due to errors' % result)
|
|
|
+ else:
|
|
|
+ app.log.info('Exiting normally')
|
|
|
+ result = 0
|
|
|
+
|
|
|
+ try:
|
|
|
+ app.http_server.stop()
|
|
|
+ app.io_loop.stop()
|
|
|
+ env_patch.stop()
|
|
|
+ os._exit(result)
|
|
|
+ except Exception as e:
|
|
|
+ self.log.error(str(e))
|
|
|
+ if 'Stream is closed' in str(e):
|
|
|
+ os._exit(result)
|
|
|
+ os._exit(1)
|
|
|
+
|
|
|
# The entry URL for browser tests is different in notebook >= 6.0,
|
|
|
# since that uses a local HTML file to point the user at the app.
|
|
|
if hasattr(app, 'browser_open_file'):
|
|
@@ -92,61 +101,22 @@ async def run_test_async(app, func):
|
|
|
else:
|
|
|
url = app.display_url
|
|
|
|
|
|
- # Allow a synchronous function to be passed in.
|
|
|
- if inspect.isawaitable(func):
|
|
|
- test = func(url)
|
|
|
- else:
|
|
|
- loop = asyncio.get_event_loop()
|
|
|
- executor = ThreadPoolExecutor()
|
|
|
- task = loop.run_in_executor(executor, func, url)
|
|
|
- test = asyncio.wait([task])
|
|
|
-
|
|
|
- try:
|
|
|
- await test
|
|
|
- except Exception as e:
|
|
|
- app.log.critical("Caught exception during the test:")
|
|
|
- app.log.error(str(e))
|
|
|
-
|
|
|
- app.log.info("Test Complete")
|
|
|
-
|
|
|
- result = 0
|
|
|
- if handler.errored:
|
|
|
- result = 1
|
|
|
- app.log.critical('Exiting with 1 due to errors')
|
|
|
- else:
|
|
|
- app.log.info('Exiting normally')
|
|
|
-
|
|
|
- app.log.info('Stopping server...')
|
|
|
- try:
|
|
|
- app.http_server.stop()
|
|
|
- app.io_loop.stop()
|
|
|
- env_patch.stop()
|
|
|
- except Exception as e:
|
|
|
- self.log.error(str(e))
|
|
|
- result = 1
|
|
|
- finally:
|
|
|
- sys.exit(result)
|
|
|
-
|
|
|
-
|
|
|
-async def run_async_process(cmd, **kwargs):
|
|
|
- """Run an asynchronous command"""
|
|
|
- proc = await asyncio.create_subprocess_exec(
|
|
|
- *cmd,
|
|
|
- **kwargs)
|
|
|
-
|
|
|
- return await proc.communicate()
|
|
|
+ app.log.addHandler(handler)
|
|
|
+ pool = ThreadPoolExecutor()
|
|
|
+ future = pool.submit(func, url)
|
|
|
+ IOLoop.current().add_future(future, finished)
|
|
|
|
|
|
|
|
|
-async def run_browser(url):
|
|
|
+def run_browser(url):
|
|
|
"""Run the browser test and return an exit code.
|
|
|
"""
|
|
|
target = osp.join(get_app_dir(), 'browser_test')
|
|
|
if not osp.exists(osp.join(target, 'node_modules')):
|
|
|
os.makedirs(target)
|
|
|
- await run_async_process(["jlpm", "init", "-y"], cwd=target)
|
|
|
- await run_async_process(["jlpm", "add", "puppeteer@^2"], cwd=target)
|
|
|
+ subprocess.call(["jlpm", "init", "-y"], cwd=target)
|
|
|
+ subprocess.call(["jlpm", "add", "puppeteer"], cwd=target)
|
|
|
shutil.copy(osp.join(here, 'chrome-test.js'), osp.join(target, 'chrome-test.js'))
|
|
|
- await run_async_process(["node", "chrome-test.js", url], cwd=target)
|
|
|
+ return subprocess.check_call(["node", "chrome-test.js", url], cwd=target)
|
|
|
|
|
|
|
|
|
class BrowserApp(LabApp):
|