Skip to content

Commit 3d7826f

Browse files
authored
Modernize integration and performance test suites (#727)
* Move from BoltKit to TestKit * cleaner use of pytest fixtures * clean-up tox config
1 parent 07041fb commit 3d7826f

34 files changed

+424
-639
lines changed

testkit/Dockerfile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,16 @@ ENV PYENV_ROOT /.pyenv
4242
ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH
4343

4444
# Set minimum supported Python version
45-
RUN pyenv install 3.7.12
45+
RUN pyenv install 3.7:latest
46+
RUN pyenv install 3.8:latest
47+
RUN pyenv install 3.9:latest
48+
RUN pyenv install 3.10:latest
4649
RUN pyenv rehash
47-
RUN pyenv global 3.7.12
50+
RUN pyenv global $(pyenv versions --bare --skip-aliases)
4851

4952
# Install Latest pip for each environment
5053
# https://pip.pypa.io/en/stable/news/
5154
RUN python -m pip install --upgrade pip
5255

5356
# Install Python Testing Tools
54-
RUN python -m pip install coverage tox
57+
RUN python -m pip install coverage tox tox-factor

testkit/integration.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,13 @@
1818
# limitations under the License.
1919

2020

21+
import subprocess
22+
23+
24+
def run(args):
25+
subprocess.run(
26+
args, universal_newlines=True, stderr=subprocess.STDOUT, check=True)
27+
28+
2129
if __name__ == "__main__":
22-
pass
30+
run(["python", "-m", "tox", "-f", "integration"])

testkit/unittests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ def run(args):
2727

2828

2929
if __name__ == "__main__":
30-
run(["python", "-m", "tox", "-c", "tox-unit.ini"])
30+
run(["python", "-m", "tox", "-f", "unit"])

tests/conftest.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# Copyright (c) "Neo4j"
2+
# Neo4j Sweden AB [https://neo4j.com]
3+
#
4+
# This file is part of Neo4j.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# https://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
19+
import asyncio
20+
from functools import wraps
21+
from os import environ
22+
import warnings
23+
24+
import pytest
25+
import pytest_asyncio
26+
27+
from neo4j import (
28+
AsyncGraphDatabase,
29+
ExperimentalWarning,
30+
GraphDatabase,
31+
)
32+
from neo4j._exceptions import BoltHandshakeError
33+
from neo4j._sync.io import Bolt
34+
from neo4j.exceptions import ServiceUnavailable
35+
36+
from . import env
37+
38+
39+
# from neo4j.debug import watch
40+
#
41+
# watch("neo4j")
42+
43+
44+
@pytest.fixture(scope="session")
45+
def uri():
46+
return env.NEO4J_SERVER_URI
47+
48+
49+
@pytest.fixture(scope="session")
50+
def bolt_uri(uri):
51+
if env.NEO4J_SCHEME != "bolt":
52+
pytest.skip("Test requires bolt scheme")
53+
return uri
54+
55+
56+
@pytest.fixture(scope="session")
57+
def _forced_bolt_uri():
58+
return f"bolt://{env.NEO4J_HOST}:{env.NEO4J_PORT}"
59+
60+
61+
@pytest.fixture(scope="session")
62+
def neo4j_uri():
63+
if env.NEO4J_SCHEME != "neo4j":
64+
pytest.skip("Test requires neo4j scheme")
65+
return uri
66+
67+
68+
@pytest.fixture(scope="session")
69+
def _forced_neo4j_uri():
70+
return f"neo4j://{env.NEO4J_HOST}:{env.NEO4J_PORT}"
71+
72+
73+
@pytest.fixture(scope="session")
74+
def auth():
75+
return env.NEO4J_USER, env.NEO4J_PASS
76+
77+
78+
@pytest.fixture
79+
def driver(uri, auth):
80+
with GraphDatabase.driver(uri, auth=auth) as driver:
81+
yield driver
82+
83+
84+
@pytest.fixture
85+
def bolt_driver(bolt_uri, auth):
86+
with GraphDatabase.driver(bolt_uri, auth=auth) as driver:
87+
yield driver
88+
89+
90+
@pytest.fixture
91+
def neo4j_driver(neo4j_uri, auth):
92+
with GraphDatabase.driver(neo4j_uri, auth=auth) as driver:
93+
yield driver
94+
95+
96+
@wraps(AsyncGraphDatabase.driver)
97+
def get_async_driver_no_warning(*args, **kwargs):
98+
# with warnings.catch_warnings():
99+
# warnings.filterwarnings("ignore", "neo4j async", ExperimentalWarning)
100+
with pytest.warns(ExperimentalWarning, match="neo4j async"):
101+
return AsyncGraphDatabase.driver(*args, **kwargs)
102+
103+
104+
@pytest_asyncio.fixture
105+
async def async_driver(uri, auth):
106+
async with get_async_driver_no_warning(uri, auth=auth) as driver:
107+
yield driver
108+
109+
110+
@pytest_asyncio.fixture
111+
async def async_bolt_driver(bolt_uri, auth):
112+
async with get_async_driver_no_warning(bolt_uri, auth=auth) as driver:
113+
yield driver
114+
115+
116+
@pytest_asyncio.fixture
117+
async def async_neo4j_driver(neo4j_uri, auth):
118+
async with get_async_driver_no_warning(neo4j_uri, auth=auth) as driver:
119+
yield driver
120+
121+
122+
@pytest.fixture
123+
def _forced_bolt_driver(_forced_bolt_uri):
124+
with GraphDatabase.driver(_forced_bolt_uri, auth=auth) as driver:
125+
yield driver
126+
127+
128+
@pytest.fixture
129+
def _forced_neo4j_driver(_forced_neo4j_uri):
130+
with GraphDatabase.driver(_forced_neo4j_uri, auth=auth) as driver:
131+
yield driver
132+
133+
134+
@pytest.fixture(scope="session")
135+
def server_info(_forced_bolt_driver):
136+
return _forced_bolt_driver.get_server_info()
137+
138+
139+
@pytest.fixture(scope="session")
140+
def bolt_protocol_version(server_info):
141+
return server_info.protocol_version
142+
143+
144+
def mark_requires_min_bolt_version(version="3.5"):
145+
return pytest.mark.skipif(
146+
env.NEO4J_VERSION < version,
147+
reason=f"requires server version '{version}' or higher, "
148+
f"found '{env.NEO4J_VERSION}'"
149+
)
150+
151+
152+
def mark_requires_edition(edition):
153+
return pytest.mark.skipif(
154+
env.NEO4J_EDITION != edition,
155+
reason=f"requires server edition '{edition}', "
156+
f"found '{env.NEO4J_EDITION}'"
157+
)
158+
159+
160+
@pytest.fixture
161+
def session(driver):
162+
with driver.session() as session:
163+
yield session
164+
165+
166+
@pytest.fixture
167+
def bolt_session(bolt_driver):
168+
with bolt_driver.session() as session:
169+
yield session
170+
171+
172+
@pytest.fixture
173+
def neo4j_session(neo4j_driver):
174+
with neo4j_driver.session() as session:
175+
yield session
176+
177+
178+
# async support for pytest-benchmark
179+
# https://github.com/ionelmc/pytest-benchmark/issues/66
180+
@pytest_asyncio.fixture
181+
async def aio_benchmark(benchmark, event_loop):
182+
def _wrapper(func, *args, **kwargs):
183+
if asyncio.iscoroutinefunction(func):
184+
@benchmark
185+
def _():
186+
return event_loop.run_until_complete(func(*args, **kwargs))
187+
else:
188+
benchmark(func, *args, **kwargs)
189+
190+
return _wrapper

tests/env.py

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,86 @@
1616
# limitations under the License.
1717

1818

19-
from os import getenv
19+
import abc
20+
from os import environ
21+
import sys
2022

2123

22-
# Full path of a server package to be used for integration testing
23-
NEO4J_SERVER_PACKAGE = getenv("NEO4J_SERVER_PACKAGE")
24+
class _LazyEval(abc.ABC):
25+
@abc.abstractmethod
26+
def eval(self):
27+
pass
2428

25-
# An existing remote server at this URI
26-
NEO4J_SERVER_URI = getenv("NEO4J_URI")
2729

28-
# Name of a user for the currently running server
29-
NEO4J_USER = getenv("NEO4J_USER")
30+
class _LazyEvalEnv(_LazyEval):
31+
def __init__(self, env_key, type_=str, default=...):
32+
self.env_key = env_key
33+
self.type_ = type_
34+
self.default = default
3035

31-
# Password for the currently running server
32-
NEO4J_PASSWORD = getenv("NEO4J_PASSWORD")
36+
def eval(self):
37+
if self.default is not ...:
38+
value = environ.get(self.env_key, default=self.default)
39+
else:
40+
try:
41+
value = environ[self.env_key]
42+
except KeyError as e:
43+
raise Exception(
44+
f"Missing environemnt variable {self.env_key}"
45+
) from e
46+
if self.type_ is bool:
47+
return value.lower() in ("yes", "y", "1", "on", "true")
48+
if self.type_ is not None:
49+
return self.type_(value)
3350

34-
NEOCTRL_ARGS = getenv("NEOCTRL_ARGS", "3.4.1")
51+
52+
class _LazyEvalFunc(_LazyEval):
53+
def __init__(self, func):
54+
self.func = func
55+
56+
def eval(self):
57+
return self.func()
58+
59+
60+
class _Module:
61+
def __init__(self, module):
62+
self._moudle = module
63+
64+
def __getattr__(self, item):
65+
val = getattr(self._moudle, item)
66+
if isinstance(val, _LazyEval):
67+
val = val.eval()
68+
setattr(self._moudle, item, val)
69+
return val
70+
71+
72+
_module = _Module(sys.modules[__name__])
73+
74+
sys.modules[__name__] = _module
75+
76+
77+
NEO4J_HOST = _LazyEvalEnv("TEST_NEO4J_HOST")
78+
NEO4J_PORT = _LazyEvalEnv("TEST_NEO4J_PORT", int)
79+
NEO4J_USER = _LazyEvalEnv("TEST_NEO4J_USER")
80+
NEO4J_PASS = _LazyEvalEnv("TEST_NEO4J_PASS")
81+
NEO4J_SCHEME = _LazyEvalEnv("TEST_NEO4J_SCHEME")
82+
NEO4J_EDITION = _LazyEvalEnv("TEST_NEO4J_EDITION")
83+
NEO4J_VERSION = _LazyEvalEnv("TEST_NEO4J_VERSION")
84+
NEO4J_IS_CLUSTER = _LazyEvalEnv("TEST_NEO4J_IS_CLUSTER", bool)
85+
NEO4J_SERVER_URI = _LazyEvalFunc(
86+
lambda: f"{_module.NEO4J_SCHEME}://{_module.NEO4J_HOST}:"
87+
f"{_module.NEO4J_PORT}"
88+
)
89+
90+
91+
__all__ = (
92+
"NEO4J_HOST",
93+
"NEO4J_PORT",
94+
"NEO4J_USER",
95+
"NEO4J_PASS",
96+
"NEO4J_SCHEME",
97+
"NEO4J_EDITION",
98+
"NEO4J_VERSION",
99+
"NEO4J_IS_CLUSTER",
100+
"NEO4J_SERVER_URI",
101+
)

tests/integration/async_/test_custom_ssl_context.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626

2727
@mark_async_test
28-
async def test_custom_ssl_context_wraps_connection(target, auth, mocker):
28+
async def test_custom_ssl_context_wraps_connection(uri, auth, mocker):
2929
# Test that the driver calls either `.wrap_socket` or `.wrap_bio` on the
3030
# provided custom SSL context.
3131

@@ -39,8 +39,8 @@ def wrap_fail(*_, **__):
3939
fake_ssl_context.wrap_socket.side_effect = wrap_fail
4040
fake_ssl_context.wrap_bio.side_effect = wrap_fail
4141

42-
driver = AsyncGraphDatabase.neo4j_driver(
43-
target, auth=auth, ssl_context=fake_ssl_context
42+
driver = AsyncGraphDatabase.driver(
43+
uri, auth=auth, ssl_context=fake_ssl_context
4444
)
4545
async with driver:
4646
async with driver.session() as session:

0 commit comments

Comments
 (0)