diff --git a/.dockerignore b/.dockerignore index bbe15fd0..222f5482 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,5 +4,3 @@ dist venv*/ .coverage tests/ -testkit/ -testkitbackend/ diff --git a/testkit/Dockerfile b/testkit/Dockerfile index 44aba2ed..9727c1be 100644 --- a/testkit/Dockerfile +++ b/testkit/Dockerfile @@ -1,48 +1,70 @@ -FROM ubuntu:20.04 +ARG TIME_WARP="" +FROM ubuntu:20.04 AS base ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y locales && \ - apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ - localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 \ - && rm -rf /var/lib/apt/lists/* + localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* ENV LANG=en_US.UTF-8 # Using apt-get update alone in a RUN statement causes caching issues and subsequent apt-get install instructions fail. -RUN apt-get --quiet update && apt-get --quiet install -y \ - software-properties-common \ - bash \ - python3 \ - python3-pip \ - git \ - curl \ - tar \ - wget \ - && rm -rf /var/lib/apt/lists/* +RUN apt-get update && \ + apt-get install -y \ + software-properties-common \ + bash \ + python3 \ + python3-pip \ + git \ + curl \ + tar \ + wget && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* # Install Build Tools RUN apt-get update && \ apt-get install -y --no-install-recommends \ - make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \ - libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev \ - libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev \ + make \ + build-essential \ + libssl-dev \ + zlib1g-dev \ + libbz2-dev \ + libreadline-dev\ + libsqlite3-dev \ + wget \ + curl \ + llvm \ + libncurses5-dev \ + xz-utils \ + tk-dev\ + libxml2-dev \ + libxmlsec1-dev \ + libffi-dev \ + liblzma-dev\ ca-certificates && \ - apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -# Install our own CAs on the image. -# Assumes Linux Debian based image. -COPY CAs/* /usr/local/share/ca-certificates/ -# Store custom CAs somewhere where the backend can find them later. -COPY CustomCAs/* /usr/local/share/custom-ca-certificates/ -RUN update-ca-certificates + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* # Install pyenv RUN git clone https://github.com/pyenv/pyenv.git .pyenv ENV PYENV_ROOT=/.pyenv ENV PATH="$PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH" +ENV PIP_NO_CACHE_DIR=1 -# Setup python version -ENV PYTHON_VERSIONS="3.13 3.12 3.11 3.10" + +FROM base AS base-py-arg +# Install all supported Python versions +ARG PYTHON_VERSIONS="3.13 3.12 3.11 3.10" + + +FROM base AS base-py-arg-single-python +# Only install Python 3.10 in time warp mode +ARG PYTHON_VERSIONS="3.10" + + +FROM base-py-arg${TIME_WARP:+"-single-python"} AS base-py-install RUN for version in $PYTHON_VERSIONS; do \ pyenv install $version; \ @@ -56,3 +78,35 @@ RUN pyenv global $(pyenv versions --bare --skip-aliases | sort --version-sort -- RUN for version in $PYTHON_VERSIONS; do \ python$version -m pip install -U pip; \ done + +ARG TIME_WARP +ENV DRIVER_TIME_WARP=$TIME_WARP + + +FROM base-py-install AS backend-timewarp +WORKDIR /home/root/testkit + +COPY testkit/backend.py testkit/build.py testkit/_common.py testkit/ +COPY pyproject.toml . + +RUN for version in $PYTHON_VERSIONS; do \ + TEST_BACKEND_VERSION="python$version" python testkit/build.py && \ + python$version -m pip install --force-reinstall neo4j==${TIME_WARP}; \ + done +COPY testkitbackend ./testkitbackend +ENTRYPOINT ["python", "testkit/backend.py"] + + +FROM base-py-install AS backend + +# Install our own CAs on the image. +# Assumes Linux Debian based image. +COPY CAs/* /usr/local/share/ca-certificates/ +# Store custom CAs somewhere where the backend can find them later. +COPY CustomCAs/* /usr/local/share/custom-ca-certificates/ +RUN update-ca-certificates + + +FROM backend${TIME_WARP:+"-timewarp"} AS final +WORKDIR /home/root/testkit +EXPOSE 9876/tcp diff --git a/testkit/_common.py b/testkit/_common.py index 7e3e6a34..14e4dfaa 100644 --- a/testkit/_common.py +++ b/testkit/_common.py @@ -21,6 +21,7 @@ TEST_BACKEND_VERSION = os.getenv("TEST_BACKEND_VERSION", "python") +DRIVER_TIME_WARP = os.getenv("DRIVER_TIME_WARP") def run(args, env=None): diff --git a/testkit/backend.py b/testkit/backend.py index 1bd38768..89dcc069 100644 --- a/testkit/backend.py +++ b/testkit/backend.py @@ -23,4 +23,5 @@ cmd = ["-m", "testkitbackend"] if "TEST_BACKEND_SERVER" in os.environ: cmd.append(os.environ["TEST_BACKEND_SERVER"]) - run_python(cmd) + is_time_warp = bool(os.environ.get("DRIVER_TIME_WARP")) + run_python(cmd, warning_as_error=not is_time_warp) diff --git a/testkit/build.py b/testkit/build.py index 8c89be20..b42ab19f 100644 --- a/testkit/build.py +++ b/testkit/build.py @@ -16,10 +16,22 @@ """Building driver and test backend inside driver container.""" -from _common import run_python +import sys + +from _common import ( + DRIVER_TIME_WARP, + run_python, +) if __name__ == "__main__": + if DRIVER_TIME_WARP: + run_python( + ["-m", "pip", "install", "-U", "--group", "testkit"], + warning_as_error=False, + ) + sys.exit(0) + run_python( ["-m", "pip", "install", "-U", "pip"], warning_as_error=False, diff --git a/testkitbackend/_async/requests.py b/testkitbackend/_async/requests.py index 6acd9dec..086aca2b 100644 --- a/testkitbackend/_async/requests.py +++ b/testkitbackend/_async/requests.py @@ -45,6 +45,10 @@ ) from .._warning_check import warnings_check from ..exceptions import MarkdAsDriverError +from ..test_config import ( + FEATURES, + SKIPPED_TESTS, +) if t.TYPE_CHECKING: @@ -108,9 +112,6 @@ def load_config(): return skips, features -SKIPPED_TESTS, FEATURES = load_config() - - def _get_skip_reason(test_name): for skip_pattern, reason in SKIPPED_TESTS.items(): if skip_pattern[0] == skip_pattern[-1] == "'": diff --git a/testkitbackend/_sync/requests.py b/testkitbackend/_sync/requests.py index a83eff01..a319bc0c 100644 --- a/testkitbackend/_sync/requests.py +++ b/testkitbackend/_sync/requests.py @@ -45,6 +45,10 @@ ) from .._warning_check import warnings_check from ..exceptions import MarkdAsDriverError +from ..test_config import ( + FEATURES, + SKIPPED_TESTS, +) if t.TYPE_CHECKING: @@ -108,9 +112,6 @@ def load_config(): return skips, features -SKIPPED_TESTS, FEATURES = load_config() - - def _get_skip_reason(test_name): for skip_pattern, reason in SKIPPED_TESTS.items(): if skip_pattern[0] == skip_pattern[-1] == "'": diff --git a/testkitbackend/exceptions.py b/testkitbackend/exceptions.py index bacd901d..d49b15a9 100644 --- a/testkitbackend/exceptions.py +++ b/testkitbackend/exceptions.py @@ -14,9 +14,27 @@ # limitations under the License. +from .time_warp_compat import VERSION + + class MarkdAsDriverError(Exception): """Wrap any error as DriverException.""" def __init__(self, wrapped_exc): super().__init__() self.wrapped_exc = wrapped_exc + + +class TimeWarpError(Exception): + """ + Request cannot be fulfilled with in the current time warp mode. + + The backend understood the request, but is running in time warp mode + against an older driver that does not support the requested feature. + """ + + def __init__(self, feature_name: str): + super().__init__( + f"{feature_name.capitalize()} is not supported in time warp mode " + f"(driver version {'.'.join(map(str, VERSION))})." + ) diff --git a/testkitbackend/test_config.json b/testkitbackend/test_config.json index 59eefc4e..730232ae 100644 --- a/testkitbackend/test_config.json +++ b/testkitbackend/test_config.json @@ -56,7 +56,7 @@ "Feature:Bolt:5.2": true, "Feature:Bolt:5.3": true, "Feature:Bolt:5.4": true, - "Feature:Bolt:5.5": true, + "Feature:Bolt:5.5": "Version was never released in a server", "Feature:Bolt:5.6": true, "Feature:Bolt:5.7": true, "Feature:Bolt:5.8": true, diff --git a/testkitbackend/test_config.py b/testkitbackend/test_config.py new file mode 100644 index 00000000..bc0b935f --- /dev/null +++ b/testkitbackend/test_config.py @@ -0,0 +1,46 @@ +# Copyright (c) "Neo4j" +# Neo4j Sweden AB [https://neo4j.com] +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import json +import ssl +from os import path + +from .time_warp_compat import ( + BLOCKED_TESTKIT_FEATURES, + EXTRA_TESTKIT_FEATURES, +) + + +__all__ = ["FEATURES", "SKIPPED_TESTS"] + + +def _load_config(): + config_path = path.join(path.dirname(__file__), "test_config.json") + with open(config_path, encoding="utf-8") as fd: + config = json.load(fd) + skips = config["skips"] + features = [ + k + for k, v in config["features"].items() + if v is True and k not in BLOCKED_TESTKIT_FEATURES + ] + features.extend(EXTRA_TESTKIT_FEATURES) + if ssl.HAS_TLSv1_3: + features += ["Feature:TLS:1.3"] + return skips, features + + +SKIPPED_TESTS, FEATURES = _load_config() diff --git a/testkitbackend/time_warp_compat.py b/testkitbackend/time_warp_compat.py new file mode 100644 index 00000000..725001c7 --- /dev/null +++ b/testkitbackend/time_warp_compat.py @@ -0,0 +1,59 @@ +# Copyright (c) "Neo4j" +# Neo4j Sweden AB [https://neo4j.com] +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from __future__ import annotations + +import os +import typing as t + + +if t.TYPE_CHECKING: + import typing_extensions as te + + +__all__ = [ + "BLOCKED_TESTKIT_FEATURES", + "EXTRA_TESTKIT_FEATURES", + "VERSION", +] + + +def _get_time_warp_version() -> tuple[float, ...]: + time_warp_env = os.environ.get("DRIVER_TIME_WARP") + if not time_warp_env: + return (float("inf"),) + return tuple(int(e) for e in time_warp_env.split(".")) + + +VERSION: te.Final[tuple[float, ...]] = _get_time_warp_version() + + +def _get_blocked_testkit_features() -> frozenset[str]: + blocked: list[str] = [] + return frozenset(blocked) + + +def _get_extra_testkit_features() -> frozenset[str]: + extra: list[str] = [] + return frozenset(extra) + + +BLOCKED_TESTKIT_FEATURES: te.Final[frozenset[str]] = ( + _get_blocked_testkit_features() +) +EXTRA_TESTKIT_FEATURES: te.Final[frozenset[str]] = ( + _get_extra_testkit_features() +)