Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add test infrastructure #20

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .github/actions/setup-test-env/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: setup-test-env
description: "Setup test environment"

inputs:
python-version:
default: "3.10"
description: "Python version to test."

outputs:
python-version:
description: "Installed Python version."
value: ${{ inputs.python-version }}
bokeh-version:
description: "Installed bokeh version."
value: ${{ steps.install.outputs.bokeh-version }}

runs:
using: composite

steps:
- name: Setup miniforge
uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest
activate-environment: bokeh-fastapi-test
python-version: ${{ inputs.python-version }}

- shell: bash -el {0}
run: |
# Display conda info
conda info

# - name: Restore conda environment
# id: cache
# uses: actions/cache@v4
# with:
# path: ${{ env.CONDA }}/envs
# key:
# env-${{ runner.os }}-${{ runner.arch }}-${{ inputs.python-version
# }}|${{ hashFiles('environment-dev.yml', 'pyproject.toml') }}
# restore-keys: |
# env-${{ runner.os }}-${{ runner.arch }}-${{ inputs.python-version }}

- id: install
shell: bash -el {0}
run: |
# Install bokeh-fastapi
conda install pip
pip install .
BOKEH_VERSION=$(conda list --json | jq --raw-output '.[] | select(.name=="bokeh").version')
echo "bokeh-version=${BOKEH_VERSION}" | tee --append "${GITHUB_OUTPUT}"

- name: Checkout repository
uses: actions/checkout@v4
with:
repository: bokeh/bokeh
path: ./tests/bokeh
ref: ${{ steps.install.outputs.bokeh-version }}

- # if: steps.cache.outputs.cache-hit != 'true'
shell: bash
run: |
# Update conda environment if necessary
conda env update --name bokeh-fastapi-test --file ./tests/bokeh/conda/environment-test-${{ inputs.python-version }}.yml

- shell: bash -el {0}
run: |
# Display test environment
conda list

- shell: bash
working-directory: ./tests
run: |
# Apply test patches
./setup.sh
60 changes: 60 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: test

on:
pull_request:

jobs:
detect-usage:
runs-on: ubuntu-latest
defaults:
run:
shell: bash -el {0}

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup environment
id: setup
uses: ./.github/actions/setup-test-env

- name: Restore test cache
id: cache
uses: actions/cache@v4
with:
path: ./.bokeh_fastapi_cache
key: test-${{ runner.os }}-${{ runner.arch }}|${{ steps.setup.outputs.python-version }}-${{ steps.setup.outputs.bokeh-version }}
restore-keys: test-${{ runner.os }}-${{ runner.arch }}

- name: Create test cache
if: steps.cache.outputs.cache-hit != 'true'
working-directory: ./tests/bokeh
run: pytest --no-header --no-summary --quiet --tb=no tests/unit || true
run:
needs:
- detect-usage

runs-on: ubuntu-latest
defaults:
run:
shell: bash -el {0}

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup environment
id: setup
uses: ./.github/actions/setup-test-env

- name: Restore test cache
id: cache
uses: actions/cache@v4
with:
path: ./.bokeh_fastapi_cache
key: test-${{ runner.os }}-${{ runner.arch }}|${{ steps.setup.outputs.python-version }}-${{ steps.setup.outputs.bokeh-version }}
restore-keys: test-${{ runner.os }}-${{ runner.arch }}

- name: Run tests
working-directory: ./tests/bokeh
run: pytest --tb=short
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
src/bokeh_fastapi/_version.py

tests/bokeh
.bokeh_fastapi_cache

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ requires = [
build-backend = "setuptools.build_meta"

[project]
name = "bokeh_fastapi"
name = "bokeh-fastapi"
description = "Compatibility layer between Bokeh and FastAPI"
readme = "README.md"
authors = [
Expand Down
111 changes: 111 additions & 0 deletions tests/conftest_patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import contextlib
import json
import sys
import unittest.mock
from pathlib import Path

import bokeh
import pytest
from bokeh.server.tornado import BokehTornado
from bokeh_fastapi import BokehFastAPI


def cache_path() -> Path:
folder = Path(__file__).parents[3] / ".bokeh_fastapi_cache"
folder.mkdir(parents=True, exist_ok=True)
name = f"python-{sys.version_info[0]}.{sys.version_info[1]}-bokeh-{bokeh.__version__}.json"
return folder / name


TESTS = []
PATCHES = {}


def update_required_patches(modules):
global PATCHES
for module in modules:
if module in PATCHES:
continue

for name, obj in module.__dict__.items():
if isinstance(obj, type) and issubclass(obj, BokehTornado):
PATCHES[module.__name__] = name
break


class BokehFastAPICompat(BokehFastAPI):
pass
# def __init__(self, *args, **kwargs):
# kwargs["websocket_origins"] = kwargs.pop("extra_websocket_origins")
# kwargs.pop("absolute_url", None)
# kwargs.pop("index", None)
# kwargs.pop("websocket_max_message_size_bytes", None)
# kwargs.pop("extra_patterns", None)
# super().__init__(*args, **kwargs)
#
# def initialize(self, *args, **kwargs):
# pass
#
# def start(self, *args, **kwargs):
# pass
#
# def __call__(self, *args, **kwargs):
# pass


@pytest.hookimpl(wrapper=True)
def pytest_collection_modifyitems(config, items):
path = cache_path()
if path.exists():
with open(cache_path()) as file:
cache = json.load(file)

tests = set(cache["tests"])
select = []
deselect = []
for item in items:
(select if item.nodeid in tests else deselect).append(item)
items[:] = select
config.hook.pytest_deselected(items=deselect)

for module_name, obj_name in cache["patches"].items():
unittest.mock.patch(
f"{module_name}.{obj_name}", new=BokehFastAPICompat
).start()
else:
update_required_patches({item.module for item in items})

return (yield)


def pytest_terminal_summary():
path = cache_path()
if not path.exists():
with open(path, "w") as file:
json.dump({"patches": PATCHES, "tests": TESTS}, file, indent=2)


@pytest.fixture(autouse=True)
def detect_bokeh_tornado_usage(request):
update_required_patches(
[
module
for name, module in sys.modules.items()
if (name == "bokeh" or name.startswith("bokeh."))
and name != "bokeh.server.tornado"
]
)

with contextlib.ExitStack() as stack:
spies = [
stack.enter_context(
unittest.mock.patch(f"{module_name}.{obj_name}", wraps=BokehTornado)
)
for module_name, obj_name in PATCHES.items()
]

yield

global TESTS
if any(spy.called for spy in spies):
TESTS.append(request.node.nodeid)
9 changes: 9 additions & 0 deletions tests/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
TESTS_DIR='./bokeh/tests'
CONFTEST="${TESTS_DIR}/conftest.py"
BACKUP="${TESTS_DIR}/conftest.py.bak"

if [[ ! -f "${BACKUP}" ]]; then
cp "${CONFTEST}" "${BACKUP}"
fi

cat "${BACKUP}" './conftest_patch.py' > "${CONFTEST}"
Loading