From 81535d486e81a6e04c2807b47e650c5b855ab7ab Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 12 Nov 2022 11:28:52 -0600 Subject: [PATCH] More coverage (#1067) --- .github/workflows/integration-tests.yml | 3 +- .github/workflows/python-tests.yml | 21 ++---- codecov.yml | 2 +- jupyter_server/serverapp.py | 7 +- pyproject.toml | 17 ++++- tests/test_utils.py | 65 ++++++++++++++++++- .../test_serverapp_integration.py | 37 +++++++++-- 7 files changed, 123 insertions(+), 29 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 4aa90c158d..57693a166b 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -27,7 +27,8 @@ jobs: pip check - name: Run the tests run: | - pytest -vv --integration_tests=true tests + hatch run cov:integration + integration_check: # This job does nothing and is only used for the branch protection if: always() diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 192a698e97..3cdea3eec0 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -36,11 +36,10 @@ jobs: run: | sudo apt-get update sudo apt-get install texlive-plain-generic inkscape texlive-xetex - sudo apt-get install xvfb x11-utils libxkbcommon-x11-0 - pip install pandoc + sudo apt-get install xvfb x11-utils libxkbcommon-x11-0 pandoc - name: Run the tests if: ${{ !startsWith(matrix.python-version, 'pypy') && !startsWith(matrix.os, 'windows') }} - run: hatch run cov:test -W default || hatch run cov:test -W default --lf + run: hatch run cov:test -W default || hatch run test:test -W default --lf - name: Run the tests on pypy and windows if: ${{ startsWith(matrix.python-version, 'pypy') || startsWith(matrix.os, 'windows') }} run: hatch run test:test -W default || hatch run test:test -W default --lf @@ -71,20 +70,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - - uses: pre-commit/action@v3.0.0 - with: - extra_args: --all-files --hook-stage=manual - - name: Help message if pre-commit fail - if: ${{ failure() }} - run: | - echo "You can install pre-commit hooks to automatically run formatting" - echo "on each commit with:" - echo " pre-commit install" - echo "or you can run by hand on staged files with" - echo " pre-commit run" - echo "or after-the-fact on already committed files with" - echo " pre-commit run --all-files --hook-stage=manual" + - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - uses: jupyterlab/maintainer-tools/.github/actions/pre-commit@v1 test_docs: name: Test Docs diff --git a/codecov.yml b/codecov.yml index eb9b9dff30..b75c3e2dbc 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,7 +3,7 @@ coverage: project: default: target: auto - threshold: 10 + threshold: 1 patch: default: target: 0% diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index 865f797b17..b39bb097d6 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -510,7 +510,6 @@ def shutdown_server(server_info, timeout=5, log=None): url = server_info["url"] pid = server_info["pid"] - try: shutdown_url = urljoin(url, "api/shutdown") if log: @@ -2891,7 +2890,11 @@ def list_running_servers(runtime_dir=None, log=None): for file_name in os.listdir(runtime_dir): if re.match("jpserver-(.+).json", file_name): with open(os.path.join(runtime_dir, file_name), encoding="utf-8") as f: - info = json.load(f) + # Handle race condition where file is being written. + try: + info = json.load(f) + except json.JSONDecodeError: + continue # Simple check whether that process is really still running # Also remove leftover files from IPython 2.x without a pid field diff --git a/pyproject.toml b/pyproject.toml index 9a5ad677e0..38a4f3fc99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,8 @@ dependencies = ["coverage", "pytest-cov"] [tool.hatch.envs.cov.env-vars] ARGS = "-vv --cov jupyter_server --cov-branch --cov-report term-missing:skip-covered" [tool.hatch.envs.cov.scripts] -test = "python -m pytest $ARGS --cov-fail-under 70 {args}" +test = "python -m pytest $ARGS --cov-fail-under 75 {args}" +integration = "python -m pytest $ARGS --integration_tests=true {args}" [tool.hatch.version] path = "jupyter_server/_version.py" @@ -145,6 +146,20 @@ filterwarnings = [ "module:Jupyter is migrating its paths to use standard platformdirs:DeprecationWarning", ] +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "class .*\bProtocol\\):", +"@(abc\\.)?abstractmethod", +] + [tool.jupyter-releaser.hooks] before-build-python = ["npm install", "npm run build"] diff --git a/tests/test_utils.py b/tests/test_utils.py index 5c5d16e73d..d3e168ff60 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,10 +1,27 @@ +import os +import socket +import subprocess +import sys +import warnings from pathlib import Path from unittest.mock import patch import pytest from traitlets.tests.utils import check_help_all_output -from jupyter_server.utils import is_namespace_package, url_escape, url_unescape +from jupyter_server.utils import ( + check_pid, + check_version, + is_namespace_package, + path2url, + run_sync_in_loop, + samefile_simple, + to_api_path, + unix_socket_in_use, + url2path, + url_escape, + url_unescape, +) def test_help_output(): @@ -59,3 +76,49 @@ def test_is_namespace_package_no_spec(): assert is_namespace_package("dummy") is None mocked_spec.assert_called_once_with("dummy") + + +@pytest.mark.skipif(os.name == "nt", reason="Paths are annoying on Windows") +def test_path_utils(tmp_path): + path = str(tmp_path) + assert os.path.basename(path2url(path)) == os.path.basename(path) + + url = path2url(path) + assert path.endswith(url2path(url)) + + assert samefile_simple(path, path) + + assert to_api_path(path, os.path.dirname(path)) == os.path.basename(path) + + +def test_check_version(): + assert check_version("1.0.2", "1.0.1") + assert not check_version("1.0.0", "1.0.1") + assert check_version(1.0, "1.0.1") + + +def test_check_pid(): + proc = subprocess.Popen([sys.executable]) + proc.kill() + proc.wait() + check_pid(proc.pid) + + +async def test_run_sync_in_loop(): + async def foo(): + pass + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + await run_sync_in_loop(foo()) + + +@pytest.mark.skipif(os.name != "posix", reason="Requires unix sockets") +def test_unix_socket_in_use(tmp_path): + root_tmp_dir = Path("/tmp").resolve() + server_address = os.path.join(root_tmp_dir, os.path.basename(tmp_path)) + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.bind(server_address) + sock.listen(0) + assert unix_socket_in_use(server_address) + sock.close() diff --git a/tests/unix_sockets/test_serverapp_integration.py b/tests/unix_sockets/test_serverapp_integration.py index 9661539d7e..d11a99d60c 100644 --- a/tests/unix_sockets/test_serverapp_integration.py +++ b/tests/unix_sockets/test_serverapp_integration.py @@ -1,19 +1,19 @@ +import os import stat +import subprocess import sys +import time import pytest +from jupyter_server.serverapp import list_running_servers, shutdown_server +from jupyter_server.utils import urlencode_unix_socket, urlencode_unix_socket_path + # Skip this module if on Windows. Unix sockets are not available on Windows. pytestmark = pytest.mark.skipif( sys.platform.startswith("win"), reason="Unix sockets are not available on Windows." ) -import os -import subprocess -import time - -from jupyter_server.utils import urlencode_unix_socket, urlencode_unix_socket_path - def _cleanup_process(proc): proc.wait() @@ -171,3 +171,28 @@ def test_launch_socket_collision(jp_unix_socket_file): _ensure_stopped() _cleanup_process(p1) + + +@pytest.mark.integration_test +def test_shutdown_server(jp_environ): + # Start a server in another process + # Stop that server + import subprocess + + from jupyter_client.connect import LocalPortCache + + port = LocalPortCache().find_available_port("localhost") + p = subprocess.Popen(["jupyter-server", f"--port={port}"]) + servers = [] + while 1: + servers = list(list_running_servers()) + if len(servers): + break + time.sleep(0.1) + while 1: + try: + shutdown_server(servers[0]) + break + except ConnectionRefusedError: + time.sleep(0.1) + p.wait()