PageRenderTime 62ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/tests/conftest.py

https://github.com/andymccurdy/redis-py
Python | 462 lines | 421 code | 30 blank | 11 comment | 16 complexity | 6ccc85ddf9ad6f02a34d37808049266a MD5 | raw file
  1. import argparse
  2. import random
  3. import time
  4. from typing import Callable, TypeVar
  5. from unittest.mock import Mock
  6. from urllib.parse import urlparse
  7. import pytest
  8. from packaging.version import Version
  9. import redis
  10. from redis.backoff import NoBackoff
  11. from redis.connection import parse_url
  12. from redis.exceptions import RedisClusterException
  13. from redis.retry import Retry
  14. REDIS_INFO = {}
  15. default_redis_url = "redis://localhost:6379/9"
  16. default_redismod_url = "redis://localhost:36379"
  17. default_redis_unstable_url = "redis://localhost:6378"
  18. # default ssl client ignores verification for the purpose of testing
  19. default_redis_ssl_url = "rediss://localhost:6666"
  20. default_cluster_nodes = 6
  21. _DecoratedTest = TypeVar("_DecoratedTest", bound="Callable")
  22. _TestDecorator = Callable[[_DecoratedTest], _DecoratedTest]
  23. # Taken from python3.9
  24. class BooleanOptionalAction(argparse.Action):
  25. def __init__(
  26. self,
  27. option_strings,
  28. dest,
  29. default=None,
  30. type=None,
  31. choices=None,
  32. required=False,
  33. help=None,
  34. metavar=None,
  35. ):
  36. _option_strings = []
  37. for option_string in option_strings:
  38. _option_strings.append(option_string)
  39. if option_string.startswith("--"):
  40. option_string = "--no-" + option_string[2:]
  41. _option_strings.append(option_string)
  42. if help is not None and default is not None:
  43. help += f" (default: {default})"
  44. super().__init__(
  45. option_strings=_option_strings,
  46. dest=dest,
  47. nargs=0,
  48. default=default,
  49. type=type,
  50. choices=choices,
  51. required=required,
  52. help=help,
  53. metavar=metavar,
  54. )
  55. def __call__(self, parser, namespace, values, option_string=None):
  56. if option_string in self.option_strings:
  57. setattr(namespace, self.dest, not option_string.startswith("--no-"))
  58. def format_usage(self):
  59. return " | ".join(self.option_strings)
  60. def pytest_addoption(parser):
  61. parser.addoption(
  62. "--redis-url",
  63. default=default_redis_url,
  64. action="store",
  65. help="Redis connection string," " defaults to `%(default)s`",
  66. )
  67. parser.addoption(
  68. "--redismod-url",
  69. default=default_redismod_url,
  70. action="store",
  71. help="Connection string to redis server"
  72. " with loaded modules,"
  73. " defaults to `%(default)s`",
  74. )
  75. parser.addoption(
  76. "--redis-ssl-url",
  77. default=default_redis_ssl_url,
  78. action="store",
  79. help="Redis SSL connection string," " defaults to `%(default)s`",
  80. )
  81. parser.addoption(
  82. "--redis-cluster-nodes",
  83. default=default_cluster_nodes,
  84. action="store",
  85. help="The number of cluster nodes that need to be "
  86. "available before the test can start,"
  87. " defaults to `%(default)s`",
  88. )
  89. parser.addoption(
  90. "--redis-unstable-url",
  91. default=default_redis_unstable_url,
  92. action="store",
  93. help="Redis unstable (latest version) connection string "
  94. "defaults to %(default)s`",
  95. )
  96. parser.addoption(
  97. "--uvloop", action=BooleanOptionalAction, help="Run tests with uvloop"
  98. )
  99. def _get_info(redis_url):
  100. client = redis.Redis.from_url(redis_url)
  101. info = client.info()
  102. try:
  103. client.execute_command("DPING")
  104. info["enterprise"] = True
  105. except redis.ResponseError:
  106. info["enterprise"] = False
  107. client.connection_pool.disconnect()
  108. return info
  109. def pytest_sessionstart(session):
  110. redis_url = session.config.getoption("--redis-url")
  111. info = _get_info(redis_url)
  112. version = info["redis_version"]
  113. arch_bits = info["arch_bits"]
  114. cluster_enabled = info["cluster_enabled"]
  115. REDIS_INFO["version"] = version
  116. REDIS_INFO["arch_bits"] = arch_bits
  117. REDIS_INFO["cluster_enabled"] = cluster_enabled
  118. REDIS_INFO["enterprise"] = info["enterprise"]
  119. # module info, if the second redis is running
  120. try:
  121. redismod_url = session.config.getoption("--redismod-url")
  122. info = _get_info(redismod_url)
  123. REDIS_INFO["modules"] = info["modules"]
  124. except redis.exceptions.ConnectionError:
  125. pass
  126. except KeyError:
  127. pass
  128. if cluster_enabled:
  129. cluster_nodes = session.config.getoption("--redis-cluster-nodes")
  130. wait_for_cluster_creation(redis_url, cluster_nodes)
  131. use_uvloop = session.config.getoption("--uvloop")
  132. if use_uvloop:
  133. try:
  134. import uvloop
  135. uvloop.install()
  136. except ImportError as e:
  137. raise RuntimeError(
  138. "Can not import uvloop, make sure it is installed"
  139. ) from e
  140. def wait_for_cluster_creation(redis_url, cluster_nodes, timeout=60):
  141. """
  142. Waits for the cluster creation to complete.
  143. As soon as all :cluster_nodes: nodes become available, the cluster will be
  144. considered ready.
  145. :param redis_url: the cluster's url, e.g. redis://localhost:16379/0
  146. :param cluster_nodes: The number of nodes in the cluster
  147. :param timeout: the amount of time to wait (in seconds)
  148. """
  149. now = time.time()
  150. end_time = now + timeout
  151. client = None
  152. print(f"Waiting for {cluster_nodes} cluster nodes to become available")
  153. while now < end_time:
  154. try:
  155. client = redis.RedisCluster.from_url(redis_url)
  156. if len(client.get_nodes()) == int(cluster_nodes):
  157. print("All nodes are available!")
  158. break
  159. except RedisClusterException:
  160. pass
  161. time.sleep(1)
  162. now = time.time()
  163. if now >= end_time:
  164. available_nodes = 0 if client is None else len(client.get_nodes())
  165. raise RedisClusterException(
  166. f"The cluster did not become available after {timeout} seconds. "
  167. f"Only {available_nodes} nodes out of {cluster_nodes} are available"
  168. )
  169. def skip_if_server_version_lt(min_version: str) -> _TestDecorator:
  170. redis_version = REDIS_INFO.get("version", "0")
  171. check = Version(redis_version) < Version(min_version)
  172. return pytest.mark.skipif(check, reason=f"Redis version required >= {min_version}")
  173. def skip_if_server_version_gte(min_version: str) -> _TestDecorator:
  174. redis_version = REDIS_INFO.get("version", "0")
  175. check = Version(redis_version) >= Version(min_version)
  176. return pytest.mark.skipif(check, reason=f"Redis version required < {min_version}")
  177. def skip_unless_arch_bits(arch_bits: int) -> _TestDecorator:
  178. return pytest.mark.skipif(
  179. REDIS_INFO.get("arch_bits", "") != arch_bits,
  180. reason=f"server is not {arch_bits}-bit",
  181. )
  182. def skip_ifmodversion_lt(min_version: str, module_name: str):
  183. try:
  184. modules = REDIS_INFO["modules"]
  185. except KeyError:
  186. return pytest.mark.skipif(True, reason="Redis server does not have modules")
  187. if modules == []:
  188. return pytest.mark.skipif(True, reason="No redis modules found")
  189. for j in modules:
  190. if module_name == j.get("name"):
  191. version = j.get("ver")
  192. mv = int(min_version.replace(".", ""))
  193. check = version < mv
  194. return pytest.mark.skipif(check, reason="Redis module version")
  195. raise AttributeError(f"No redis module named {module_name}")
  196. def skip_if_redis_enterprise() -> _TestDecorator:
  197. check = REDIS_INFO.get("enterprise", False) is True
  198. return pytest.mark.skipif(check, reason="Redis enterprise")
  199. def skip_ifnot_redis_enterprise() -> _TestDecorator:
  200. check = REDIS_INFO.get("enterprise", False) is False
  201. return pytest.mark.skipif(check, reason="Not running in redis enterprise")
  202. def skip_if_nocryptography() -> _TestDecorator:
  203. try:
  204. import cryptography # noqa
  205. return pytest.mark.skipif(False, reason="Cryptography dependency found")
  206. except ImportError:
  207. return pytest.mark.skipif(True, reason="No cryptography dependency")
  208. def skip_if_cryptography() -> _TestDecorator:
  209. try:
  210. import cryptography # noqa
  211. return pytest.mark.skipif(True, reason="Cryptography dependency found")
  212. except ImportError:
  213. return pytest.mark.skipif(False, reason="No cryptography dependency")
  214. def _get_client(
  215. cls, request, single_connection_client=True, flushdb=True, from_url=None, **kwargs
  216. ):
  217. """
  218. Helper for fixtures or tests that need a Redis client
  219. Uses the "--redis-url" command line argument for connection info. Unlike
  220. ConnectionPool.from_url, keyword arguments to this function override
  221. values specified in the URL.
  222. """
  223. if from_url is None:
  224. redis_url = request.config.getoption("--redis-url")
  225. else:
  226. redis_url = from_url
  227. cluster_mode = REDIS_INFO["cluster_enabled"]
  228. if not cluster_mode:
  229. url_options = parse_url(redis_url)
  230. url_options.update(kwargs)
  231. pool = redis.ConnectionPool(**url_options)
  232. client = cls(connection_pool=pool)
  233. else:
  234. client = redis.RedisCluster.from_url(redis_url, **kwargs)
  235. single_connection_client = False
  236. if single_connection_client:
  237. client = client.client()
  238. if request:
  239. def teardown():
  240. if not cluster_mode:
  241. if flushdb:
  242. try:
  243. client.flushdb()
  244. except redis.ConnectionError:
  245. # handle cases where a test disconnected a client
  246. # just manually retry the flushdb
  247. client.flushdb()
  248. client.close()
  249. client.connection_pool.disconnect()
  250. else:
  251. cluster_teardown(client, flushdb)
  252. request.addfinalizer(teardown)
  253. return client
  254. def cluster_teardown(client, flushdb):
  255. if flushdb:
  256. try:
  257. client.flushdb(target_nodes="primaries")
  258. except redis.ConnectionError:
  259. # handle cases where a test disconnected a client
  260. # just manually retry the flushdb
  261. client.flushdb(target_nodes="primaries")
  262. client.close()
  263. client.disconnect_connection_pools()
  264. # specifically set to the zero database, because creating
  265. # an index on db != 0 raises a ResponseError in redis
  266. @pytest.fixture()
  267. def modclient(request, **kwargs):
  268. rmurl = request.config.getoption("--redismod-url")
  269. with _get_client(
  270. redis.Redis, request, from_url=rmurl, decode_responses=True, **kwargs
  271. ) as client:
  272. yield client
  273. @pytest.fixture()
  274. def r(request):
  275. with _get_client(redis.Redis, request) as client:
  276. yield client
  277. @pytest.fixture()
  278. def r_timeout(request):
  279. with _get_client(redis.Redis, request, socket_timeout=1) as client:
  280. yield client
  281. @pytest.fixture()
  282. def r2(request):
  283. "A second client for tests that need multiple"
  284. with _get_client(redis.Redis, request) as client:
  285. yield client
  286. @pytest.fixture()
  287. def sslclient(request):
  288. with _get_client(redis.Redis, request, ssl=True) as client:
  289. yield client
  290. def _gen_cluster_mock_resp(r, response):
  291. connection = Mock()
  292. connection.retry = Retry(NoBackoff(), 0)
  293. connection.read_response.return_value = response
  294. r.connection = connection
  295. return r
  296. @pytest.fixture()
  297. def mock_cluster_resp_ok(request, **kwargs):
  298. r = _get_client(redis.Redis, request, **kwargs)
  299. return _gen_cluster_mock_resp(r, "OK")
  300. @pytest.fixture()
  301. def mock_cluster_resp_int(request, **kwargs):
  302. r = _get_client(redis.Redis, request, **kwargs)
  303. return _gen_cluster_mock_resp(r, "2")
  304. @pytest.fixture()
  305. def mock_cluster_resp_info(request, **kwargs):
  306. r = _get_client(redis.Redis, request, **kwargs)
  307. response = (
  308. "cluster_state:ok\r\ncluster_slots_assigned:16384\r\n"
  309. "cluster_slots_ok:16384\r\ncluster_slots_pfail:0\r\n"
  310. "cluster_slots_fail:0\r\ncluster_known_nodes:7\r\n"
  311. "cluster_size:3\r\ncluster_current_epoch:7\r\n"
  312. "cluster_my_epoch:2\r\ncluster_stats_messages_sent:170262\r\n"
  313. "cluster_stats_messages_received:105653\r\n"
  314. )
  315. return _gen_cluster_mock_resp(r, response)
  316. @pytest.fixture()
  317. def mock_cluster_resp_nodes(request, **kwargs):
  318. r = _get_client(redis.Redis, request, **kwargs)
  319. response = (
  320. "c8253bae761cb1ecb2b61857d85dfe455a0fec8b 172.17.0.7:7006 "
  321. "slave aa90da731f673a99617dfe930306549a09f83a6b 0 "
  322. "1447836263059 5 connected\n"
  323. "9bd595fe4821a0e8d6b99d70faa660638a7612b3 172.17.0.7:7008 "
  324. "master - 0 1447836264065 0 connected\n"
  325. "aa90da731f673a99617dfe930306549a09f83a6b 172.17.0.7:7003 "
  326. "myself,master - 0 0 2 connected 5461-10922\n"
  327. "1df047e5a594f945d82fc140be97a1452bcbf93e 172.17.0.7:7007 "
  328. "slave 19efe5a631f3296fdf21a5441680f893e8cc96ec 0 "
  329. "1447836262556 3 connected\n"
  330. "4ad9a12e63e8f0207025eeba2354bcf4c85e5b22 172.17.0.7:7005 "
  331. "master - 0 1447836262555 7 connected 0-5460\n"
  332. "19efe5a631f3296fdf21a5441680f893e8cc96ec 172.17.0.7:7004 "
  333. "master - 0 1447836263562 3 connected 10923-16383\n"
  334. "fbb23ed8cfa23f17eaf27ff7d0c410492a1093d6 172.17.0.7:7002 "
  335. "master,fail - 1447829446956 1447829444948 1 disconnected\n"
  336. )
  337. return _gen_cluster_mock_resp(r, response)
  338. @pytest.fixture()
  339. def mock_cluster_resp_slaves(request, **kwargs):
  340. r = _get_client(redis.Redis, request, **kwargs)
  341. response = (
  342. "['1df047e5a594f945d82fc140be97a1452bcbf93e 172.17.0.7:7007 "
  343. "slave 19efe5a631f3296fdf21a5441680f893e8cc96ec 0 "
  344. "1447836789290 3 connected']"
  345. )
  346. return _gen_cluster_mock_resp(r, response)
  347. @pytest.fixture(scope="session")
  348. def master_host(request):
  349. url = request.config.getoption("--redis-url")
  350. parts = urlparse(url)
  351. yield parts.hostname, parts.port
  352. @pytest.fixture()
  353. def unstable_r(request):
  354. url = request.config.getoption("--redis-unstable-url")
  355. with _get_client(
  356. redis.Redis, request, from_url=url, decode_responses=True
  357. ) as client:
  358. yield client
  359. def wait_for_command(client, monitor, command, key=None):
  360. # issue a command with a key name that's local to this process.
  361. # if we find a command with our key before the command we're waiting
  362. # for, something went wrong
  363. if key is None:
  364. # generate key
  365. redis_version = REDIS_INFO["version"]
  366. if Version(redis_version) >= Version("5.0.0"):
  367. id_str = str(client.client_id())
  368. else:
  369. id_str = f"{random.randrange(2 ** 32):08x}"
  370. key = f"__REDIS-PY-{id_str}__"
  371. client.get(key)
  372. while True:
  373. monitor_response = monitor.next_command()
  374. if command in monitor_response["command"]:
  375. return monitor_response
  376. if key in monitor_response["command"]:
  377. return None