diff --git a/kazoo/client.py b/kazoo/client.py
index 4877f3580..dc775ba87 100644
--- a/kazoo/client.py
+++ b/kazoo/client.py
@@ -99,10 +99,7 @@
_RETRY_COMPAT_DEFAULTS = dict(
- max_retries=None,
- retry_delay=0.1,
- retry_backoff=2,
- retry_max_delay=3600,
+ max_retries=None, retry_delay=0.1, retry_backoff=2, retry_max_delay=3600
)
_RETRY_COMPAT_MAPPING = dict(
@@ -177,7 +174,6 @@ def __init__(
check_hostname: bool = False,
) -> None: ...
- # FIXME This should be deprecated then killed
@overload
@deprecated(
"Passing retry configuration parameters directly to the client"
@@ -332,11 +328,7 @@ def __init__(
self.logger = logger or log
# Record the handler strategy used
- # FIXME we need to work on making SequentialGeventHandler being an
- # IHandler to make this cleaner but for now this will do.
- self.handler = cast(
- "IHandler", handler if handler else SequentialThreadingHandler()
- )
+ self.handler = handler if handler else SequentialThreadingHandler()
if inspect.isclass(self.handler):
raise ConfigurationError(
"Handler must be an instance of a class, "
@@ -346,16 +338,8 @@ def __init__(
self.auth_data = set(auth_data if auth_data else [])
self.default_acl = default_acl
self.randomize_hosts = randomize_hosts
- # FIXME Note: hosts and chroot are set by set_hosts, which also checks
- # for chroot changes at runtime, so we initialize them to None here to
- # avoid confusion with the empty string that set_hosts would set them
- # to. This is massively hacky as set_hosts is only called from here
- # anyway, but I want to make this change minimally invasive.
- # we should really do self.hosts, self.chroot = self.set_hosts(hosts)
- # and have set_hosts return the hosts and chroot
- self.hosts: list[tuple[str, int]] = None # type: ignore[assignment]
- self.chroot: str = None # type: ignore[assignment]
- self.set_hosts(hosts)
+
+ self.hosts, self.chroot = self._collect_hosts(hosts)
self.use_ssl = use_ssl
self.verify_certs = verify_certs
@@ -395,74 +379,54 @@ def __init__(
self._stopped.set()
self._writer_stopped.set()
- # FIXME This is kind of gross but we need to set these to something so
- # that the type checker will understand that they are set by the time
- # they are used and that they have the right type.
- # We would do better to use a few variables/functions instead of
- # overloading self.retry but this is a bit less invasive to the code
- # and the type checker can understand it with a few hacks
- self.retry: KazooRetry = None # type: ignore[assignment]
- self._conn_retry: KazooRetry = None # type: ignore[assignment]
-
- if type(connection_retry) is dict:
- self._conn_retry = KazooRetry(**connection_retry)
- elif type(connection_retry) is KazooRetry:
- self._conn_retry = connection_retry
-
- if type(command_retry) is dict:
- self.retry = KazooRetry(**command_retry)
- elif type(command_retry) is KazooRetry:
- self.retry = command_retry
-
- if type(self._conn_retry) is KazooRetry:
+ old_retry_keys = dict(_RETRY_COMPAT_DEFAULTS)
+ for key in old_retry_keys:
+ try:
+ old_retry_keys[key] = cast(
+ "dict[str, float | None]", kwargs
+ ).pop(key)
+ warnings.warn(
+ "Passing retry configuration param %s to the "
+ "client directly is deprecated, please pass a "
+ "configured retry object (using param %s)"
+ % (key, _RETRY_COMPAT_MAPPING[key]),
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ except KeyError:
+ pass
+
+ retry_keys: dict[str, Any] = {}
+ for oldname, value in old_retry_keys.items():
+ retry_keys[_RETRY_COMPAT_MAPPING[oldname]] = value
+ retry_keys["sleep_func"] = self.handler.sleep_func
+
+ def make_retry(
+ retry: KazooRetry | KazooRetryParams | None,
+ ) -> KazooRetry:
+ if isinstance(retry, dict):
+ return KazooRetry(**retry)
+ if isinstance(retry, KazooRetry):
+ return retry
+ return KazooRetry(**retry_keys)
+
+ self._conn_retry = make_retry(connection_retry)
+ self._retry = make_retry(command_retry)
+
+ if self._conn_retry is not None:
if self.handler.sleep_func != self._conn_retry.sleep_func:
raise ConfigurationError(
"Retry handler and event handler "
" must use the same sleep func"
)
- if type(self.retry) is KazooRetry:
- if self.handler.sleep_func != self.retry.sleep_func:
+ if self._retry is not None:
+ if self.handler.sleep_func != self._retry.sleep_func:
raise ConfigurationError(
"Command retry handler and event handler "
"must use the same sleep func"
)
- if self.retry is None or self._conn_retry is None:
- # Note: because of the hacks at line 280, mypy thinks this is
- # unreachable
- old_retry_keys = dict( # type: ignore[unreachable]
- _RETRY_COMPAT_DEFAULTS
- )
- for key in old_retry_keys:
- try:
- old_retry_keys[key] = kwargs.pop(key)
- warnings.warn(
- "Passing retry configuration param %s to the "
- "client directly is deprecated, please pass a "
- "configured retry object (using param %s)"
- % (key, _RETRY_COMPAT_MAPPING[key]),
- DeprecationWarning,
- stacklevel=2,
- )
- except KeyError:
- pass
-
- retry_keys = {}
- for oldname, value in old_retry_keys.items():
- retry_keys[_RETRY_COMPAT_MAPPING[oldname]] = value
-
- if self._conn_retry is None:
- self._conn_retry = KazooRetry(
- sleep_func=self.handler.sleep_func,
- **retry_keys,
- )
- if self.retry is None:
- self.retry = KazooRetry(
- sleep_func=self.handler.sleep_func,
- **retry_keys,
- )
-
# Managing legacy SASL options
for scheme, auth in self.auth_data:
if scheme != "sasl":
@@ -506,7 +470,6 @@ def __init__(
# Every retry call should have its own copy of the retry helper
# to avoid shared retry counts
- self._retry = self.retry
def _retry(
func: Callable[GenericArgs, KazooRetry.RETRY_RETURN],
@@ -515,14 +478,7 @@ def _retry(
) -> KazooRetry.RETRY_RETURN:
return self._retry.copy()(func, *args, **kwargs)
- # FIXME
- # (expression has type "Callable[[VarArg(Any), KwArg(Any)], Any]",
- # variable has type "KazooRetry") so basically self.retry needs to be
- # set to that and then the type checker will understand that
- # self.retry.copy() is a valid call. This is just a mess and needs the
- # code rearranging to be more mypy friendly but this is the least
- # invasive way to do it for now
- self.retry = _retry # type: ignore[assignment]
+ self.retry = _retry
self.Barrier = partial(Barrier, self)
self.Counter = partial(Counter, self)
@@ -607,10 +563,14 @@ def connected(self) -> bool:
established."""
return self._live.is_set()
+ def _collect_hosts(
+ self, hosts: str | list[str]
+ ) -> tuple[list[tuple[str, int]], str]:
+ new_hosts, chroot = collect_hosts(hosts)
+ return new_hosts, normpath(chroot)
+
def set_hosts(
- self,
- hosts: str | list[str],
- randomize_hosts: bool | None = None,
+ self, hosts: str | list[str], randomize_hosts: bool | None = None
) -> None:
"""sets the list of hosts used by this client.
@@ -635,25 +595,18 @@ def set_hosts(
zookeeper server cluster has undefined behavior.
"""
-
# Change the client setting for randomization if specified
+ # Randomizing the list will be done at connect time
if randomize_hosts is not None:
self.randomize_hosts = randomize_hosts
- # Randomizing the list will be done at connect time
- self.hosts, chroot = collect_hosts(hosts)
+ self.hosts, chroot = self._collect_hosts(hosts)
- if chroot:
- new_chroot = normpath(chroot)
- else:
- new_chroot = ""
-
- if self.chroot is not None and new_chroot != self.chroot:
+ if chroot != self.chroot:
raise ConfigurationError(
"Changing chroot at runtime is not currently supported"
)
-
- self.chroot = new_chroot
+ self.chroot = chroot
def add_listener(self, listener: ListenerFunc) -> None:
"""Add a function to be called for connection state changes.
@@ -981,29 +934,12 @@ def _try_fetch() -> tuple[int, ...] | None:
except ValueError:
return None
- def _is_valid(version: tuple[int, ...] | None) -> bool:
- # All zookeeper versions should have at least major.minor
- # version numbers; if we get one that doesn't it is likely not
- # correct and was truncated...
- if version and len(version) > 1:
- return True
- return False
-
- # FIXME A better way of doing this would be to put the initial
- # _try_fetch in the loop and inline _is_valid but I want to minimise
- # code changes
-
# Try 1 + retries amount of times to get a version that we know
# will likely be acceptable...
- version = _try_fetch()
- if _is_valid(version):
- # mypy doesn't recognise that _is_valid guarantees this
- return version # type: ignore[return-value]
- for _i in range(0, retries):
+ for _ in range(0, retries + 1):
version = _try_fetch()
- if _is_valid(version):
- # mypy doesn't recognise that _is_valid guarantees this
- return version # type: ignore[return-value]
+ if version is not None and len(version) > 1:
+ return version
raise KazooException(
"Unable to fetch useable server"
" version after trying %s times" % (1 + max(0, retries))
@@ -1578,12 +1514,8 @@ def get_children_async(
raise TypeError("Invalid type for 'include_data' (bool expected)")
async_result = self.handler.async_result()
- # FIXME? Do this as req = getc2 if include_data else getc
- req: GetChildren | GetChildren2
- if include_data:
- req = GetChildren2(_prefix_root(self.chroot, path), watch)
- else:
- req = GetChildren(_prefix_root(self.chroot, path), watch)
+ func = GetChildren2 if include_data else GetChildren
+ req = func(_prefix_root(self.chroot, path), watch)
self._call(req, async_result)
return async_result
@@ -2099,9 +2031,7 @@ def _check_tx_state(self) -> None:
raise ValueError("Transaction already committed")
def _add(
- self,
- request: Any,
- post_processor: Callable[[Any], Any] | None = None,
+ self, request: Any, post_processor: Callable[[Any], Any] | None = None
) -> None:
self._check_tx_state()
self.client.logger.log(BLATHER, "Added %r to %r", request, self)
diff --git a/kazoo/handlers/eventlet.py b/kazoo/handlers/eventlet.py
index c6f01d11f..40e3923f0 100644
--- a/kazoo/handlers/eventlet.py
+++ b/kazoo/handlers/eventlet.py
@@ -17,16 +17,10 @@
from kazoo.handlers import utils
from kazoo.handlers.utils import selector_select
+from kazoo.interfaces import IHandler, SpawnedFunc
if TYPE_CHECKING:
- from kazoo.interfaces import (
- Event,
- FdLike,
- IHandler,
- Lockable,
- ReentrantLock,
- Socket,
- )
+ from kazoo.interfaces import Event, FdLike, Lockable, ReentrantLock, Socket
from kazoo.protocol.states import Callback
@@ -65,8 +59,7 @@ def __init__(self, handler: IHandler):
)
-# FIXME This should inherit from IHandler
-class SequentialEventletHandler:
+class SequentialEventletHandler(IHandler):
"""Eventlet handler for sequentially executing callbacks.
This handler executes callbacks in a sequential manner. A queue is
@@ -96,29 +89,23 @@ class SequentialEventletHandler:
"""
name = "sequential_eventlet_handler"
- queue_impl = green_queue.LightQueue
+ queue_impl = green_queue.LightQueue # type: ignore[assignment]
queue_empty = green_queue.Empty
+ sleep_func = staticmethod(green_time.sleep) # type: ignore[assignment, attr-defined]
def __init__(self) -> None:
"""Create a :class:`SequentialEventletHandler` instance"""
- self.callback_queue = (
+ self._callback_queue = (
self.queue_impl() # type: ignore[no-untyped-call]
)
- self.completion_queue = (
+ self._completion_queue = (
self.queue_impl() # type: ignore[no-untyped-call]
)
self._workers: list[ # type: ignore[name-defined]
- tuple[
- eventlet.GreenThread,
- green_queue.LightQueue,
- ]
+ tuple[eventlet.GreenThread, green_queue.LightQueue]
] = []
self._started = False
- @staticmethod
- def sleep_func(wait: float) -> None:
- green_time.sleep(wait) # type: ignore[attr-defined, no-untyped-call]
-
@property
def running(self) -> bool:
return self._started
@@ -127,7 +114,7 @@ def running(self) -> bool:
def _process_completion_queue(self) -> None:
while True:
- cb = self.completion_queue.get() # type: ignore[no-untyped-call]
+ cb = self._completion_queue.get() # type: ignore[no-untyped-call]
if cb is _STOP:
break
try:
@@ -143,7 +130,7 @@ def _process_completion_queue(self) -> None:
def _process_callback_queue(self) -> None:
while True:
- cb = self.callback_queue.get() # type: ignore[no-untyped-call]
+ cb = self._callback_queue.get() # type: ignore[no-untyped-call]
if cb is _STOP:
break
try:
@@ -165,11 +152,11 @@ def start(self) -> None:
w = eventlet.spawn(
self._process_completion_queue # type: ignore[no-untyped-call]
)
- self._workers.append((w, self.completion_queue))
+ self._workers.append((w, self._completion_queue))
w = eventlet.spawn(
self._process_callback_queue # type: ignore[no-untyped-call]
)
- self._workers.append((w, self.callback_queue))
+ self._workers.append((w, self._callback_queue))
self._started = True
atexit.register(self.stop)
@@ -236,6 +223,18 @@ def spawn(
return t
def dispatch_callback(self, callback: Callback) -> None:
- self.callback_queue.put( # type: ignore[no-untyped-call]
+ self._callback_queue.put( # type: ignore[no-untyped-call]
lambda: callback.func(*callback.args)
)
+
+ def add_to_completion_queue(self, callback: SpawnedFunc) -> None:
+ """Add a method to callback on completion.
+
+ :param callback: A :class:`~kazoo.protocol.states.Callback`
+ object to be called.
+
+ The supplied method will be called whn an event completes.
+
+ Note: This seems an odd place to store these.
+ """
+ self._completion_queue.put(callback) # type: ignore[no-untyped-call]
diff --git a/kazoo/handlers/gevent.py b/kazoo/handlers/gevent.py
index 2f782fa28..3a390fb24 100644
--- a/kazoo/handlers/gevent.py
+++ b/kazoo/handlers/gevent.py
@@ -3,33 +3,40 @@
from __future__ import annotations
import atexit
+import importlib.util
import logging
from typing import Any, Callable, Iterable, TYPE_CHECKING, cast
+from typing_extensions import TypeAlias
from kazoo.handlers.utils import selector_select
from kazoo.handlers import utils
-
-# FIXME This is messy.
-# We don't want to force the user to install gevent, so we need to handle the
-# case where it's not available, but there should be a cleaner we of doing this
-# than by importing all of gevent and ignoring all the import errors.
-import gevent # type: ignore[import]
-from gevent import socket # type: ignore[import]
-import gevent.event # type: ignore[import]
-import gevent.queue # type: ignore[import]
-
-import gevent.thread # type: ignore[import]
-import gevent.selectors # type: ignore[import]
-from gevent.lock import Semaphore, RLock # type: ignore[import]
+from kazoo.interfaces import IAsyncResult, IHandler, SpawnedFunc
if TYPE_CHECKING:
- from kazoo.interfaces import FdLike, Lockable, Socket
+ from kazoo.interfaces import (
+ Event,
+ FdLike,
+ Lockable,
+ ReentrantLock,
+ Socket,
+ Threadlike,
+ )
from kazoo.protocol.states import Callback
- from gevent import Greenlet
-_using_libevent = gevent.__version__.startswith("0.")
+ GEVENT_AVAILABLE: bool = True
+else:
+ GEVENT_AVAILABLE = importlib.util.find_spec("gevent") is not None
+
+if GEVENT_AVAILABLE:
+ import gevent
+ from gevent import socket
+ import gevent.event
+ import gevent.queue
+ import gevent.thread
+ import gevent.selectors
+ from gevent.lock import Semaphore, RLock
log = logging.getLogger(__name__)
@@ -38,14 +45,11 @@
AsyncResult = gevent.event.AsyncResult
-# The following would be great typenames, but python3.8 complains about type
-# objects not being indexable.
-# GCallback = Callable[..., None]
-# Worker = Greenlet[..., Any]
-# CallbackQueue = gevent.queue.Queue[Callable[..., None]]
+Worker: TypeAlias = "gevent.Greenlet[..., Any]"
+CallbackQueue: TypeAlias = "gevent.queue.Queue[Callable[..., None]]"
-class SequentialGeventHandler:
+class SequentialGeventHandler(IHandler):
"""Gevent handler for sequentially executing callbacks.
This handler executes callbacks in a sequential manner. A queue is
@@ -67,31 +71,26 @@ class SequentialGeventHandler:
"""
name = "sequential_gevent_handler"
- queue_impl = gevent.queue.Queue
+ queue_impl = gevent.queue.Queue # type: ignore[assignment]
queue_empty = gevent.queue.Empty
- sleep_func = staticmethod(gevent.sleep)
+ sleep_func = staticmethod(gevent.sleep) # type: ignore[assignment]
def __init__(self) -> None:
"""Create a :class:`SequentialGeventHandler` instance"""
- self.callback_queue: gevent.queue.Queue[Callable[..., None]] = (
- self.queue_impl()
- )
+ self._callback_queue: CallbackQueue = self.queue_impl()
+
self._running = False
self._async = None
self._state_change = Semaphore()
- self._workers: list[Greenlet[..., Any]] = []
+ self._workers: list[Worker] = []
@property
def running(self) -> bool:
return self._running
- class timeout_exception(gevent.Timeout):
- def __init__(self, msg: Any):
- gevent.Timeout.__init__(self, exception=msg)
+ timeout_exception = gevent.timeout.Timeout
- def _create_greenlet_worker(
- self, queue: gevent.queue.Queue[Callable[..., None]]
- ) -> Greenlet[..., Any]:
+ def _create_greenlet_worker(self, queue: CallbackQueue) -> Worker:
def greenlet_worker() -> None:
while True:
try:
@@ -121,7 +120,7 @@ def start(self) -> None:
# Spawn our worker greenlets, we have
# - A callback worker for watch events to be called
# FIXME Why the loop?
- for queue in (self.callback_queue,):
+ for queue in (self._callback_queue,):
w = self._create_greenlet_worker(queue)
self._workers.append(w)
atexit.register(self.stop)
@@ -134,7 +133,7 @@ def stop(self) -> None:
self._running = False
- for queue in (self.callback_queue,):
+ for queue in (self._callback_queue,):
queue.put(cast("Callable[..., None]", _STOP))
while self._workers:
@@ -142,7 +141,7 @@ def stop(self) -> None:
worker.join()
# Clear the queues
- self.callback_queue = self.queue_impl() # pragma: nocover
+ self._callback_queue = self.queue_impl() # pragma: nocover
atexit.unregister(self.stop)
@@ -167,7 +166,7 @@ def create_connection(self, *args: Any, **kwargs: Any) -> Socket:
def create_socket_pair(self) -> tuple[Socket, Socket]:
return utils.create_socket_pair(socket)
- def event_object(self) -> gevent.event.Event:
+ def event_object(self) -> Event:
"""Create an appropriate Event object"""
return gevent.event.Event()
@@ -178,11 +177,11 @@ def lock_object(self) -> Lockable:
gevent.thread.allocate_lock(), # type: ignore[no-untyped-call]
)
- def rlock_object(self) -> RLock:
+ def rlock_object(self) -> ReentrantLock:
"""Create an appropriate RLock object"""
return RLock()
- def async_result(self) -> AsyncResult[Any]:
+ def async_result(self) -> IAsyncResult:
"""Create a :class:`AsyncResult` instance
The :class:`AsyncResult` instance will have its completion
@@ -193,9 +192,7 @@ def async_result(self) -> AsyncResult[Any]:
"""
return AsyncResult()
- def spawn(
- self, func: Any, *args: Any, **kwargs: Any
- ) -> gevent.Greenlet[..., Any]:
+ def spawn(self, func: Any, *args: Any, **kwargs: Any) -> Threadlike:
"""Spawn a function to run asynchronously"""
return gevent.spawn(func, *args, **kwargs)
@@ -206,4 +203,16 @@ def dispatch_callback(self, callback: Callback) -> None:
type as documented for the :class:`SequentialGeventHandler`.
"""
- self.callback_queue.put(lambda: callback.func(*callback.args))
+ self._callback_queue.put(lambda: callback.func(*callback.args))
+
+ def add_to_completion_queue(self, callback: SpawnedFunc) -> None:
+ """Add a method to callback on completion.
+
+ :param callback: A :class:`~kazoo.protocol.states.Callback`
+ object to be called.
+
+ The supplied method will be called whn an event completes.
+
+ Note: This seems an odd place to store these.
+ """
+ raise NotImplementedError()
diff --git a/kazoo/handlers/threading.py b/kazoo/handlers/threading.py
index f816652fb..20758c24d 100644
--- a/kazoo/handlers/threading.py
+++ b/kazoo/handlers/threading.py
@@ -113,7 +113,7 @@ def __init__(self) -> None:
self.callback_queue: queue.Queue[Callable[..., None]] = (
self.queue_impl()
)
- self.completion_queue: queue.Queue[Callable[..., None]] = (
+ self._completion_queue: queue.Queue[Callable[..., None]] = (
self.queue_impl()
)
self._running = False
@@ -155,7 +155,7 @@ def start(self) -> None:
# Spawn our worker threads, we have
# - A callback worker for watch events to be called
# - A completion worker for completion events to be called
- for work_queue in (self.completion_queue, self.callback_queue):
+ for work_queue in (self._completion_queue, self.callback_queue):
w = self._create_thread_worker(work_queue)
self._workers.append(w)
self._running = True
@@ -169,7 +169,7 @@ def stop(self) -> None:
self._running = False
- for work_queue in (self.completion_queue, self.callback_queue):
+ for work_queue in (self._completion_queue, self.callback_queue):
work_queue.put(cast("Callable[..., None]", _STOP))
self._workers.reverse()
@@ -179,7 +179,7 @@ def stop(self) -> None:
# Clear the queues
self.callback_queue = self.queue_impl()
- self.completion_queue = self.queue_impl()
+ self._completion_queue = self.queue_impl()
atexit.unregister(self.stop)
def select(
@@ -202,9 +202,7 @@ def event_object(self) -> Event:
def lock_object(self) -> Lockable:
"""Create a lock object"""
- # Note: This is not ideal, but the ContextManager Protocol seems to
- # think you should return an object of the same type.
- return cast("Lockable", threading.Lock())
+ return threading.Lock()
def rlock_object(self) -> ReentrantLock:
"""Create an appropriate RLock object"""
@@ -230,3 +228,15 @@ def dispatch_callback(self, callback: Callback) -> None:
"""
self.callback_queue.put(lambda: callback.func(*callback.args))
+
+ def add_to_completion_queue(self, callback: SpawnedFunc) -> None:
+ """Add a method to callback on completion.
+
+ :param callback: A :class:`~kazoo.protocol.states.Callback`
+ object to be called.
+
+ The supplied method will be called when an event completes.
+
+ Note: This seems an odd place to store these.
+ """
+ self._completion_queue.put(callback)
diff --git a/kazoo/handlers/utils.py b/kazoo/handlers/utils.py
index c7741087b..0950911cb 100644
--- a/kazoo/handlers/utils.py
+++ b/kazoo/handlers/utils.py
@@ -13,7 +13,7 @@
from typing import Any, Callable, Iterable, TypeVar, TYPE_CHECKING
from typing_extensions import ParamSpec
-from kazoo.interfaces import IAsyncResult, FdLike
+from kazoo.interfaces import IAsyncResult, IHandler, FdLike
if TYPE_CHECKING:
from types import ModuleType
@@ -26,14 +26,7 @@
HAS_FNCTL = False
-# sentinel objects
-# Note: This needs to be a unique object that is not None, as None is used to
-# indicate a successful result in AsyncResult.
-# This should probably be an Enum, it would certainly be cleaner, but don't
-# want to change the code too much.
-_NONE = object()
-
-CallbackFunc = Callable[..., None]
+CallbackFunc = Callable[["AsyncResult"], None]
class AsyncResult(IAsyncResult):
@@ -41,12 +34,13 @@ class AsyncResult(IAsyncResult):
def __init__(
self,
- handler: Any,
+ handler: IHandler,
condition_factory: Callable[[], Any],
timeout_factory: Callable[[], Any],
) -> None:
self._handler = handler
- self._exception: object | Exception | None = _NONE
+ self._exception: Exception | None = None
+ self._ready = False
self._condition = condition_factory()
self._callbacks: list[CallbackFunc] = []
self._timeout_factory = timeout_factory
@@ -55,32 +49,33 @@ def __init__(
def ready(self) -> bool:
"""Return true if and only if it holds a value or an
exception"""
- return self._exception is not _NONE
+ return self._ready
def successful(self) -> bool:
"""Return true if and only if it is ready and holds a value"""
- return self._exception is None
+ return self._ready and self._exception is None
@property
def exception(self) -> Exception | None:
- if self._exception is not _NONE:
- return self._exception # type: ignore[return-value]
- return None
+ return self._exception
def set(self, value: Any = None) -> None:
"""Store the value. Wake up the waiters."""
with self._condition:
self.value = value
- self._exception = None
- self._do_callbacks()
- self._condition.notify_all()
+ self._wakeup()
+
+ def _wakeup(self) -> None:
+ """All done, set ready and wake up any waiters"""
+ self._ready = True
+ self._do_callbacks()
+ self._condition.notify_all()
def set_exception(self, exception: Exception) -> None:
"""Store the exception. Wake up the waiters."""
with self._condition:
self._exception = exception
- self._do_callbacks()
- self._condition.notify_all()
+ self._wakeup()
def get(self, block: bool = True, timeout: float | None = None) -> Any:
"""Return the stored value or raise the exception.
@@ -89,16 +84,16 @@ def get(self, block: bool = True, timeout: float | None = None) -> Any:
"""
with self._condition:
- if self._exception is not _NONE:
+ if self._ready:
if self._exception is None:
return self.value
- raise self._exception # type: ignore[misc]
- elif block:
+ raise self._exception
+ if block:
self._condition.wait(timeout)
- if self._exception is not _NONE:
+ if self.ready():
if self._exception is None:
return self.value
- raise self._exception # type: ignore[misc]
+ raise self._exception
# if we get to this point we timeout
raise self._timeout_factory()
@@ -116,7 +111,7 @@ def wait(self, timeout: float | None = None) -> bool:
with self._condition:
if not self.ready():
self._condition.wait(timeout)
- return self._exception is not _NONE
+ return self._ready
def rawlink(self, callback: CallbackFunc) -> None:
"""Register a callback to call when a value or an exception is
@@ -147,7 +142,7 @@ def _do_callbacks(self) -> None:
for callback in self._callbacks:
if self._handler.running:
- self._handler.completion_queue.put(
+ self._handler.add_to_completion_queue(
functools.partial(callback, self)
)
else:
@@ -382,33 +377,6 @@ def captured_function(
return capture
-def fileobj_to_fd(fileobj: FdLike) -> int:
- """Return a file descriptor from a file object.
-
- Parameters:
- fileobj -- file object or file descriptor
-
- Returns:
- corresponding file descriptor
-
- Raises:
- TypeError if the object is invalid
- """
- if isinstance(fileobj, int):
- fd = fileobj
- else:
- # FIXME given the protocol I don't think the try/catch/int are
- # required.
- try:
- fd = int(fileobj.fileno())
- except (AttributeError, TypeError, ValueError):
- raise TypeError("Invalid file object: {!r}".format(fileobj))
- # FIXME Questionable, just let select deal with it.
- if fd < 0:
- raise TypeError("Invalid file descriptor: {}".format(fd))
- return fd
-
-
def selector_select(
rlist: Iterable[FdLike],
wlist: Iterable[FdLike],
@@ -434,7 +402,7 @@ def selector_select(
for event, fileobjs in events_mapping.items():
for fileobj in fileobjs:
- fd = fileobj_to_fd(fileobj)
+ fd = fileobj if isinstance(fileobj, int) else fileobj.fileno()
fd_events[fd] |= event
fd_fileobjs[fd].append(fileobj)
diff --git a/kazoo/hosts.py b/kazoo/hosts.py
index cda746a3a..675f734f4 100644
--- a/kazoo/hosts.py
+++ b/kazoo/hosts.py
@@ -3,9 +3,7 @@
import urllib.parse
-def collect_hosts(
- hosts: str | list[str],
-) -> tuple[list[tuple[str, int]], str | None]:
+def collect_hosts(hosts: str | list[str]) -> tuple[list[tuple[str, int]], str]:
"""
Collect a set of hosts and an optional chroot from
a string or a list of strings.
@@ -14,11 +12,12 @@ def collect_hosts(
if hosts[-1].strip().startswith("/"):
host_ports, chroot = hosts[:-1], hosts[-1]
else:
- host_ports, chroot = hosts, None
+ host_ports, chroot = hosts, ""
else:
host_ports_1, chroot = hosts.partition("/")[::2]
host_ports = host_ports_1.split(",")
- chroot = "/" + chroot if chroot else None
+ if chroot != "":
+ chroot = "/" + chroot
result = []
for host_port in host_ports:
@@ -28,7 +27,7 @@ def collect_hosts(
host = res.hostname
if host is None:
raise ValueError("bad hostname")
- port = int(res.port) if res.port else 2181
+ port = 2181 if res.port is None else res.port
result.append((host.strip(), port))
return result, chroot
diff --git a/kazoo/interfaces.py b/kazoo/interfaces.py
index b7b685bb0..d0d732f7d 100644
--- a/kazoo/interfaces.py
+++ b/kazoo/interfaces.py
@@ -10,13 +10,14 @@
from __future__ import annotations
-
from typing import (
Any,
Callable,
Iterable,
+ Literal,
Protocol,
Union,
+ overload,
TYPE_CHECKING,
)
@@ -70,11 +71,11 @@ def shutdown(self, flag: int) -> None: ...
class Lockable(Protocol):
"""This is what threading.Lock implements.
- In python 3.9+ it's available natively. Though given it has some
- very odd typing, I wouldn't put money on it.
+ In python 3.9+ Lockable is meant to be available natively. Though I haven't
+ seen it yet.
"""
- def __enter__(self) -> None: ...
+ def __enter__(self) -> bool: ...
def __exit__(
self,
@@ -98,7 +99,10 @@ class ReentrantLock(Protocol):
In python 3.14+, it's the same as Lock, which adds to the fun.
"""
- def __enter__(self) -> None: ...
+ # Note: GreenLet Rlock returns a bool, but the threading.RLock returns
+ # None, unlike threading.Lock. So we have to use a union type here to
+ # satisfy both. This is a hack
+ def __enter__(self) -> bool | None: ...
def __exit__(
self,
@@ -121,14 +125,25 @@ def set(self) -> None: ...
def clear(self) -> None: ...
+ # This overload is for the case where timeout is None, which means wait
+ # indefinitely. gevent's Event.wait explicitly returns True in this case,
+ # so we reflect that in the type.
+ # The threading.Event.wait actually conforms to this overload as well,
+ # since it returns True if the event is set, and if timeout is None, it
+ # will always return True, but it doesn't explicitly say that in the docs,
+ # so we have to be a bit more conservative with the typing.
+ @overload
+ def wait(self, timeout: None = None) -> bool | Literal[True]: ...
+
+ @overload
+ def wait(self, timeout: float) -> bool: ...
+
def wait(self, timeout: float | None = None) -> bool: ...
class Threadlike(Protocol):
"""Protocol for something like a thread."""
- def is_alive(self) -> bool: ...
-
def join(self, timeout: float | None = None) -> None: ...
@@ -170,7 +185,7 @@ class IHandler(Protocol):
# FIXME These should be methods but I want to avoid changing the API for
# now.
- timeout_exception: type[Exception] = None # type: ignore[assignment]
+ timeout_exception: type[BaseException] = None # type: ignore[assignment]
sleep_func: staticmethod[[float], None] = None # type: ignore[assignment]
queue_impl: type[queue.Queue[Any]] = None # type: ignore[assignment]
@@ -181,6 +196,10 @@ def stop(self) -> None:
"""Stop the handler. Should block until the handler is safely
stopped."""
+ @property
+ def running(self) -> bool:
+ """Return True if the handler is running, False otherwise."""
+
def select(
self,
rlist: Iterable[FdLike],
@@ -240,6 +259,18 @@ def dispatch_callback(self, callback: Callback) -> None:
"""
+ def add_to_completion_queue(self, callback: SpawnedFunc) -> None:
+ """Add a method to callback on completion.
+
+ :param callback: A :class:`~kazoo.protocol.states.Callback`
+ object to be called.
+
+ The supplied method will be called when an event completes.
+
+ This is called from utils.ASyncResult, the GeventHandler doesn't
+ support this.
+ """
+
class IAsyncResult(Protocol):
"""An Async Result object that can be queried for a value that has
@@ -344,6 +375,10 @@ def unlink(self, callback: Callable[[IAsyncResult], None]) -> None:
"""
@property
- def exception(self) -> Exception | None:
+ def exception(self) -> BaseException | None:
"""The exception set by :meth:`set_exception` or `None` if no
- exception has been set"""
+ exception has been set
+
+ gevent specifies BaseException here so we have to be compatible with
+ that, though it seems a little OTT.
+ """
diff --git a/kazoo/protocol/connection.py b/kazoo/protocol/connection.py
index d35952be5..8b1214a48 100644
--- a/kazoo/protocol/connection.py
+++ b/kazoo/protocol/connection.py
@@ -5,6 +5,7 @@
from binascii import hexlify
from contextlib import contextmanager
import copy
+import importlib.util
import logging
import random
import select
@@ -54,25 +55,19 @@
WatchedEvent,
EVENT_TYPE_MAP,
)
-from kazoo.retry import (
- ForceRetryError,
- KazooRetry,
- RetryFailedError,
-)
+from kazoo.retry import ForceRetryError, KazooRetry, RetryFailedError
if TYPE_CHECKING:
from kazoo.client import KazooClient, WatchFunc
from kazoo.interfaces import Socket, Threadlike
-# FIXME This is NOT pretty, but we don't want to force users to have to
-# install puresasl. Can we avoid some of the type: ignore stuff?
-try:
- import puresasl # type: ignore[import-untyped,import-not-found]
- import puresasl.client # type: ignore[import-untyped,import-not-found]
+ PURESASL_AVAILABLE: bool = True
+else:
+ PURESASL_AVAILABLE = importlib.util.find_spec("puresasl") is not None
- PURESASL_AVAILABLE = True
-except ImportError:
- PURESASL_AVAILABLE = False
+if PURESASL_AVAILABLE:
+ import puresasl
+ import puresasl.client
log = logging.getLogger(__name__)
@@ -215,7 +210,7 @@ def __init__(
self._connection_routine: Threadlike | None = None
- self.sasl_options = sasl_options
+ self._sasl_options = sasl_options
self.sasl_cli = None
# This is instance specific to avoid odd thread bug issues in Python
@@ -462,7 +457,7 @@ def _read_watch_event(self, buffer: bytes, offset: int) -> None:
elif watch.type == CHILD_EVENT:
watchers.extend(client._child_watchers.pop(path, []))
else:
- self.logger.warn("Received unknown event %r", watch.type)
+ self.logger.warning("Received unknown event %r", watch.type)
return
# Strip the chroot if needed
@@ -478,10 +473,7 @@ def _read_watch_event(self, buffer: bytes, offset: int) -> None:
client.handler.dispatch_callback(Callback("watch", watch1, (ev,)))
def _read_response(
- self,
- header: ReplyHeader,
- buffer: bytes,
- offset: int,
+ self, header: ReplyHeader, buffer: bytes, offset: int
) -> object | None:
client = self.client
request, async_object, xid = client._pending.popleft()
@@ -489,9 +481,7 @@ def _read_response(
client.last_zxid = header.zxid
if header.xid != xid:
exc = RuntimeError(
- "xids do not match, expected %r received %r",
- xid,
- header.xid,
+ "xids do not match, expected %r received %r", xid, header.xid
)
async_object.set_exception(exc)
raise exc
@@ -578,9 +568,7 @@ def _read_socket(self, read_timeout: float) -> object | None:
return None
def _send_request(
- self,
- read_timeout: float,
- connect_timeout: float,
+ self, read_timeout: float, connect_timeout: float
) -> None:
"""Called when we have something to send out on the socket"""
client = self.client
@@ -711,11 +699,7 @@ def _connect_loop(self, retry: KazooRetry) -> object:
raise ForceRetryError("Reconnecting")
def _connect_attempt(
- self,
- host: str,
- hostip: str,
- port: int,
- retry: KazooRetry,
+ self, host: str, hostip: str, port: int, retry: KazooRetry
) -> object:
client = self.client
KazooTimeoutError = self.handler.timeout_exception
@@ -828,10 +812,7 @@ def _connect_attempt(
return None
def _connect(
- self,
- host: str,
- hostip: str,
- port: int,
+ self, host: str, hostip: str, port: int
) -> tuple[float, float]:
client = self.client
self.logger.info(
@@ -912,8 +893,10 @@ def _connect(
client._session_callback(KeeperState.CONNECTED)
self._ro_mode = None
- if self.sasl_options is not None:
- self._authenticate_with_sasl(host, connect_timeout / 1000.0)
+ if self._sasl_options is not None:
+ self._authenticate_with_sasl(
+ self._sasl_options, host, connect_timeout / 1000.0
+ )
# Get a copy of the auth data before iterating, in case it is
# changed.
@@ -927,36 +910,26 @@ def _connect(
return read_timeout, connect_timeout
- def _authenticate_with_sasl(self, host: str, timeout: float) -> None:
+ def _authenticate_with_sasl(
+ self, sasl_options: dict[str, str], host: str, timeout: float
+ ) -> None:
"""Establish a SASL authenticated connection to the server."""
if not PURESASL_AVAILABLE:
raise SASLException("Missing SASL support")
- # Although this can only be called if sasl_options is not None, we
- # really should just have make self.sasl_options into an empty dict
- # in the constructor. However, I want to avoid code changes in as
- # much as possible.
- if "service" not in self.sasl_options: # type: ignore[operator]
- self.sasl_options["service"] = "zookeeper" # type: ignore[index]
+ if "service" not in sasl_options:
+ sasl_options["service"] = "zookeeper"
# NOTE: Zookeeper hardcoded the domain for Digest authentication
# instead of using the hostname. See
# zookeeper/util/SecurityUtils.java#L74 and Server/Client
# initializations.
- if (
- self.sasl_options["mechanism"] # type: ignore[index]
- == "DIGEST-MD5"
- ):
+ if sasl_options["mechanism"] == "DIGEST-MD5":
host = "zk-sasl-md5"
- # I don't think the client.sasl_cli attribute is actually used
- # anywhere else, so not sure why we need to set it on the client,
- # but again, I want to avoid code changes as much as possible.
- sasl_cli = self.client.sasl_cli = ( # type: ignore[attr-defined]
- puresasl.client.SASLClient( # type: ignore[no-untyped-call]
- host=host,
- **self.sasl_options, # type: ignore[arg-type]
- )
+ # FIXME puresasl isn't properly type hinted.
+ sasl_cli = puresasl.client.SASLClient( # type: ignore
+ host=host, **sasl_options
)
# Initialize the process with an empty challenge token
diff --git a/kazoo/protocol/serialization.py b/kazoo/protocol/serialization.py
index 4653c1e59..b989cb6aa 100644
--- a/kazoo/protocol/serialization.py
+++ b/kazoo/protocol/serialization.py
@@ -392,8 +392,36 @@ def serialize(self) -> bytearray:
return b
-# FIXME Transaction class should move after Create2
-Transaction_Types = Union[Create, "Create2", Delete, SetData, CheckVersion]
+class Create2(namedtuple("Create2", "path data acl flags")):
+ path: str
+ data: bytes | None
+ acl: Sequence[ACL]
+ flags: int
+
+ type: ClassVar[int] = 15
+
+ def serialize(self) -> bytearray:
+ b = bytearray()
+ b.extend(write_string(self.path))
+ b.extend(write_buffer(self.data))
+ b.extend(int_struct.pack(len(self.acl)))
+ for acl in self.acl:
+ b.extend(
+ int_struct.pack(acl.perms)
+ + write_string(acl.id.scheme)
+ + write_string(acl.id.id)
+ )
+ b.extend(int_struct.pack(self.flags))
+ return b
+
+ @classmethod
+ def deserialize(cls, bytes: bytes, offset: int) -> tuple[str, ZnodeStat]:
+ path, offset = read_string(bytes, offset)
+ stat = ZnodeStat(*stat_struct.unpack_from(bytes, offset))
+ return path, stat
+
+
+Transaction_Types = Union[Create, Create2, Delete, SetData, CheckVersion]
Transaction_Response = Union[str, bool, ZnodeStat, ZookeeperError, None]
@@ -449,35 +477,6 @@ def unchroot(
return resp
-class Create2(namedtuple("Create2", "path data acl flags")):
- path: str
- data: bytes | None
- acl: Sequence[ACL]
- flags: int
-
- type: ClassVar[int] = 15
-
- def serialize(self) -> bytearray:
- b = bytearray()
- b.extend(write_string(self.path))
- b.extend(write_buffer(self.data))
- b.extend(int_struct.pack(len(self.acl)))
- for acl in self.acl:
- b.extend(
- int_struct.pack(acl.perms)
- + write_string(acl.id.scheme)
- + write_string(acl.id.id)
- )
- b.extend(int_struct.pack(self.flags))
- return b
-
- @classmethod
- def deserialize(cls, bytes: bytes, offset: int) -> tuple[str, ZnodeStat]:
- path, offset = read_string(bytes, offset)
- stat = ZnodeStat(*stat_struct.unpack_from(bytes, offset))
- return path, stat
-
-
class Reconfig(
namedtuple("Reconfig", "joining leaving new_members config_id")
):
diff --git a/kazoo/recipe/cache.py b/kazoo/recipe/cache.py
index 107d6849a..bfe259d45 100644
--- a/kazoo/recipe/cache.py
+++ b/kazoo/recipe/cache.py
@@ -187,9 +187,6 @@ def get_children(
does not exist.
:raises ValueError: If the path is outside of this subtree.
:returns: The :class:`frozenset` which including children names.
-
- # FIXME the default return value should be an empty frozenset,
- # returning None is confusing.
"""
node = self._find_node(path)
return default if node is None else frozenset(node._children)
diff --git a/kazoo/recipe/counter.py b/kazoo/recipe/counter.py
index 77d68cb55..bde47e57b 100644
--- a/kazoo/recipe/counter.py
+++ b/kazoo/recipe/counter.py
@@ -107,25 +107,15 @@ def _ensure_node(self) -> None:
def _value(self) -> tuple[Number, int]:
self._ensure_node()
- # FIXME: This is astonishingly hard to follow...
- # Should probably be refactored to be more clear.
- # val, state = ...
- # if val == b"":
- # old = self.default
- # elif self.support_curator:
- # old = struct.unpack(">i", val)[0]
- # else:
- # old = val.decode("ascii")
- # maybe (not sure it does anything for the messy type though)
- old: Union[bytes, str, Number]
- old, stat = self.client.get(self.path)
- if self.support_curator:
- old = struct.unpack(">i", old)[0] if old != b"" else self.default
+ old: Union[str, Number]
+ val, stat = self.client.get(self.path)
+ if val == b"":
+ old = self.default
+ elif self.support_curator:
+ old = int(struct.unpack(">i", val)[0])
else:
- old = old.decode("ascii") if old != b"" else self.default
- version = stat.version
- data = self.default_type(old)
- return data, version
+ old = val.decode("ascii")
+ return self.default_type(old), stat.version
@property
def value(self) -> Number:
diff --git a/kazoo/recipe/election.py b/kazoo/recipe/election.py
index 4c4400032..eba33e1d4 100644
--- a/kazoo/recipe/election.py
+++ b/kazoo/recipe/election.py
@@ -34,10 +34,7 @@ class Election:
"""
def __init__(
- self,
- client: KazooClient,
- path: str,
- identifier: str | None = None,
+ self, client: KazooClient, path: str, identifier: str | None = None
):
"""Create a Kazoo Leader Election
diff --git a/kazoo/recipe/lock.py b/kazoo/recipe/lock.py
index 1737802b7..8a44ff9d0 100644
--- a/kazoo/recipe/lock.py
+++ b/kazoo/recipe/lock.py
@@ -20,12 +20,7 @@
import re
import time
import uuid
-from typing import (
- Iterable,
- Literal,
- Pattern,
- TYPE_CHECKING,
-)
+from typing import Iterable, Literal, Pattern, TYPE_CHECKING
from kazoo.exceptions import (
CancelledError,
@@ -34,11 +29,7 @@
NoNodeError,
)
from kazoo.protocol.states import KazooState, WatchedEvent
-from kazoo.retry import (
- ForceRetryError,
- KazooRetry,
- RetryFailedError,
-)
+from kazoo.retry import ForceRetryError, KazooRetry, RetryFailedError
if TYPE_CHECKING:
from types import TracebackType
@@ -48,26 +39,13 @@
class _Watch:
def __init__(self, duration: float | None = None):
self.duration = duration
- self.started_at: float | None = None
-
- def start(self) -> None:
self.started_at = time.monotonic()
def leftover(self) -> float | None:
if self.duration is None:
return None
- else:
- # We should probably set started_at to either 0 or
- # time.monotonic() in __init__ to avoid the type ignore
- # here, but this is a private class and it's pretty clear
- # that start() should be called before leftover() so I'm
- # not sure it's worth it.
- # FIXME raise an exception if start() hasn't been called yet
- # i.e. self.started_at is None
- elapsed = (
- time.monotonic() - self.started_at # type: ignore[operator]
- )
- return max(0, self.duration - elapsed)
+ elapsed = time.monotonic() - self.started_at
+ return max(0, self.duration - elapsed)
class Lock:
@@ -242,10 +220,7 @@ def _watch_session(self, state: KazooState) -> bool:
return True
def _inner_acquire(
- self,
- blocking: bool,
- timeout: float | None,
- ephemeral: bool = True,
+ self, blocking: bool, timeout: float | None, ephemeral: bool = True
) -> bool:
# wait until it's our chance to get it..
if self.is_acquired:
@@ -607,9 +582,7 @@ def cancel(self) -> None:
self.wake_event.set()
def acquire(
- self,
- blocking: bool = True,
- timeout: float | None = None,
+ self, blocking: bool = True, timeout: float | None = None
) -> bool:
"""Acquire the semaphore. By defaults blocks and waits forever.
@@ -649,9 +622,7 @@ def acquire(
return self.is_acquired
def _inner_acquire(
- self,
- blocking: bool,
- timeout: float | None = None,
+ self, blocking: bool, timeout: float | None = None
) -> bool:
"""Inner loop that runs from the top anytime a command hits a
retryable Zookeeper exception."""
@@ -666,7 +637,6 @@ def _inner_acquire(
return True
w = _Watch(duration=timeout)
- w.start()
# FIXME This is passing bytes data, but self.client.Lock expects a str,
# which I think is a bug in this code. However, I don't want to
# change any code at this point, so we just ignore the type error here.
diff --git a/kazoo/recipe/party.py b/kazoo/recipe/party.py
index a51fc2a88..990fe7ec7 100644
--- a/kazoo/recipe/party.py
+++ b/kazoo/recipe/party.py
@@ -23,10 +23,7 @@ class BaseParty:
"""Base implementation of a party."""
def __init__(
- self,
- client: KazooClient,
- path: str,
- identifier: str | None = None,
+ self, client: KazooClient, path: str, identifier: str | None = None
):
"""
:param client: A :class:`~kazoo.client.KazooClient` instance.
@@ -132,10 +129,7 @@ class ShallowParty(BaseParty):
"""
def __init__(
- self,
- client: KazooClient,
- path: str,
- identifier: str | None = None,
+ self, client: KazooClient, path: str, identifier: str | None = None
):
BaseParty.__init__(self, client, path, identifier=identifier)
self.node = "-".join([uuid.uuid4().hex, self.data.decode("utf-8")])
diff --git a/kazoo/recipe/watchers.py b/kazoo/recipe/watchers.py
index 2a504ee2b..086081fbb 100644
--- a/kazoo/recipe/watchers.py
+++ b/kazoo/recipe/watchers.py
@@ -119,10 +119,7 @@ def my_func(data, stat, event):
@overload
def __init__(
- self,
- client: KazooClient,
- path: str,
- func: DataWatchFunc | None = None,
+ self, client: KazooClient, path: str, func: DataWatchFunc | None = None
): ...
@overload
diff --git a/kazoo/retry.py b/kazoo/retry.py
index f5a544288..0f7c47650 100644
--- a/kazoo/retry.py
+++ b/kazoo/retry.py
@@ -116,10 +116,7 @@ def copy(self) -> KazooRetry:
return obj
def __call__(
- self,
- func: Callable[..., RETRY_RETURN],
- *args: Any,
- **kwargs: Any,
+ self, func: Callable[..., RETRY_RETURN], *args: Any, **kwargs: Any
) -> RETRY_RETURN:
"""Call a function with arguments until it completes without
throwing a Kazoo exception
diff --git a/kazoo/testing/__init__.py b/kazoo/testing/__init__.py
index 0d4a57519..4c806ada7 100644
--- a/kazoo/testing/__init__.py
+++ b/kazoo/testing/__init__.py
@@ -1,6 +1,3 @@
from kazoo.testing.harness import KazooTestCase, KazooTestHarness
-__all__ = (
- "KazooTestHarness",
- "KazooTestCase",
-)
+__all__ = ("KazooTestHarness", "KazooTestCase")
diff --git a/kazoo/testing/async_result_common.py b/kazoo/testing/async_result_common.py
new file mode 100644
index 000000000..638f80d21
--- /dev/null
+++ b/kazoo/testing/async_result_common.py
@@ -0,0 +1,333 @@
+from __future__ import annotations
+
+import contextlib
+from abc import ABC, abstractmethod
+from types import TracebackType
+from typing import Any, Callable, Generator
+from unittest.mock import Mock
+
+import pytest
+
+from kazoo.interfaces import IAsyncResult, IHandler
+from kazoo.handlers.utils import capture_exceptions, wrap
+
+
+class ThreadlikeContext(ABC):
+ def __init__(self, target: Callable[[], None]):
+ self._target = target
+
+ @abstractmethod
+ def __enter__(self) -> None:
+ raise NotImplementedError
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> None:
+ pass
+
+
+# FIXME It's not clear whether this needs all the tests to be run with and
+# without a running handler to test all the paths. The original mocks always
+# returned a truth value for `running``.
+class AsyncResultCommon:
+ def __init__(
+ self,
+ handler_factory: Callable[[], IHandler],
+ async_result_factory: Callable[[IHandler], IAsyncResult],
+ *args: Any,
+ thread_context_factory: Callable[
+ [Callable[[], None]], ThreadlikeContext
+ ],
+ **kwargs: Any,
+ ) -> None:
+ self._handler_factory = handler_factory
+ self._async_result_factory = async_result_factory
+ self._thread_context_factory = thread_context_factory
+ super().__init__(*args, **kwargs)
+
+ @contextlib.contextmanager
+ def _start_and_stop_handler(self) -> Generator[IHandler, None, None]:
+ self._handler.start()
+ try:
+ yield self._handler
+ finally:
+ self._handler.stop()
+
+ def setUp(self) -> None:
+ self._handler = self._handler_factory()
+ self._async_result = self._async_result_factory(self._handler)
+
+ def test_ready(self) -> None:
+ assert self._async_result.ready() is False
+ self._async_result.set(None)
+ assert self._async_result.ready() is True
+ assert self._async_result.successful() is True
+ assert self._async_result.exception is None
+ assert self._async_result.get() is None
+
+ def test_ready_value(self) -> None:
+ assert self._async_result.ready() is False
+ self._async_result.set("value")
+ assert self._async_result.ready() is True
+ assert self._async_result.successful() is True
+ assert self._async_result.exception is None
+ assert self._async_result.get() == "value"
+
+ def test_callback_queued(self) -> None:
+ callback_called = self._handler.event_object()
+
+ def add_on(async_result: IAsyncResult) -> None:
+ callback_called.set()
+
+ self._async_result.rawlink(add_on)
+ self._async_result.set("val")
+
+ assert callback_called.wait(10)
+
+ def test_set_exception(self) -> None:
+ callback_called = self._handler.event_object()
+
+ def add_on(async_result: IAsyncResult) -> None:
+ callback_called.set()
+
+ self._async_result.rawlink(add_on)
+ self._async_result.set_exception(ImportError("Error occurred"))
+
+ assert self._async_result.ready() is True
+ assert self._async_result.successful() is False
+ assert isinstance(self._async_result.exception, ImportError)
+
+ assert callback_called.wait(10)
+
+ def test_get_wait_while_setting(self) -> None:
+ lst = []
+ bv = self._handler.event_object()
+ cv = self._handler.event_object()
+
+ def wait_for_val() -> None:
+ bv.set()
+ val = self._async_result.get()
+ lst.append(val)
+ cv.set()
+
+ with self._thread_context_factory(wait_for_val):
+ bv.wait()
+ self._async_result.set("fred")
+ cv.wait()
+ assert lst == ["fred"]
+
+ def test_get_with_nowait(self) -> None:
+ timeout = self._handler.timeout_exception
+
+ with pytest.raises(timeout):
+ self._async_result.get(block=False)
+
+ with pytest.raises(timeout):
+ self._async_result.get_nowait()
+
+ def test_get_with_exception(self) -> None:
+ lst = []
+ bv = self._handler.event_object()
+ cv = self._handler.event_object()
+
+ def wait_for_val() -> None:
+ bv.set()
+ try:
+ val = self._async_result.get()
+ except ImportError:
+ lst.append("oops")
+ else:
+ lst.append(val)
+ cv.set()
+
+ with self._thread_context_factory(wait_for_val):
+ bv.wait()
+ self._async_result.set_exception(ImportError())
+ cv.wait()
+ assert lst == ["oops"]
+
+ def test_wait(self, *, is_green: bool = False) -> None:
+ lst: list[bool | str] = []
+ bv = self._handler.event_object()
+ cv = self._handler.event_object()
+
+ expected_list = ["fred" if is_green else True]
+
+ def wait_for_val() -> None:
+ bv.set()
+ try:
+ val = self._async_result.wait(15)
+ except self._handler.timeout_exception:
+ lst.append("oops")
+ else:
+ lst.append(val)
+ cv.set()
+
+ with self._thread_context_factory(wait_for_val):
+ bv.wait(10)
+ self._async_result.set("fred")
+ cv.wait(15)
+
+ assert lst == expected_list
+
+ def test_wait_race(self) -> None:
+ """Test that there is no race condition in `IAsyncResult.wait()`.
+
+ Guards against the reappearance of:
+ https://github.com/python-zk/kazoo/issues/485
+ """
+ self._async_result.set("immediate")
+
+ cv = self._handler.event_object()
+
+ def wait_for_val() -> None:
+ # NB: should not sleep
+ self._async_result.wait(20)
+ cv.set()
+
+ with self._thread_context_factory(wait_for_val):
+ self._async_result.set("immediate")
+ cv.wait(10)
+
+ # if the wait() didn't sleep (correctly), cv will be set quickly
+ # if it did sleep, the cv will not be set yet and this will timeout
+ cv.wait(10)
+ assert cv.is_set()
+
+ def test_set_before_wait(self) -> None:
+ lst = []
+ cv = self._handler.event_object()
+ self._async_result.set("fred")
+
+ def wait_for_val() -> None:
+ val = self._async_result.get()
+ lst.append(val)
+ cv.set()
+
+ with self._thread_context_factory(wait_for_val):
+ self._async_result.set("fred")
+ cv.wait()
+ assert lst == ["fred"]
+
+ def test_set_exc_before_wait(self) -> None:
+ lst = []
+ cv = self._handler.event_object()
+ self._async_result.set_exception(ImportError())
+
+ def wait_for_val() -> None:
+ try:
+ val = self._async_result.get()
+ except ImportError:
+ lst.append("ooops")
+ else:
+ lst.append(val)
+ cv.set()
+
+ with self._thread_context_factory(wait_for_val):
+ self._async_result.set_exception(ImportError())
+ cv.wait()
+
+ assert lst == ["ooops"]
+
+ def test_linkage(self) -> None:
+ cv = self._handler.event_object()
+ callback_called = self._handler.event_object()
+
+ def add_on(async_result: IAsyncResult) -> None:
+ callback_called.set()
+
+ def wait_for_val() -> None:
+ self._async_result.get()
+ cv.set()
+
+ with self._thread_context_factory(wait_for_val):
+ self._async_result.rawlink(add_on)
+ self._async_result.set(b"fred")
+ cv.wait()
+ self._async_result.unlink(add_on)
+
+ assert callback_called.wait(10)
+ assert self._async_result.get() == b"fred"
+
+ def test_linkage_not_ready(self) -> None:
+ callback_called = self._handler.event_object()
+ lst = []
+
+ def add_on(async_result: IAsyncResult) -> None:
+ lst.append(True)
+ callback_called.set()
+
+ self._async_result.set("fred")
+ self._async_result.rawlink(add_on)
+ assert callback_called.wait(10)
+ assert lst == [True]
+
+ def test_link_and_unlink(self) -> None:
+ lst = []
+
+ def add_on(async_result: IAsyncResult) -> None:
+ lst.append(True)
+
+ self._async_result.rawlink(add_on)
+ self._async_result.unlink(add_on)
+ self._async_result.set("fred")
+ assert lst == []
+
+ def test_captured_exception(self) -> None:
+ @capture_exceptions(self._async_result)
+ def exceptional_function() -> float:
+ return 1 / 0
+
+ exceptional_function()
+
+ with pytest.raises(ZeroDivisionError):
+ self._async_result.get()
+
+ def test_no_capture_exceptions(self) -> None:
+ lst = []
+
+ def add_on(async_result: IAsyncResult) -> None:
+ lst.append(True)
+
+ self._async_result.rawlink(add_on)
+
+ @capture_exceptions(self._async_result)
+ def regular_function() -> bool:
+ return True
+
+ regular_function()
+
+ assert lst == []
+
+ def test_wraps(self) -> None:
+ callback_called = self._handler.event_object()
+ lst = []
+
+ def add_on(result: IAsyncResult) -> None:
+ lst.append(result.get())
+ callback_called.set()
+
+ self._async_result.rawlink(add_on)
+
+ @wrap(self._async_result)
+ def regular_function() -> str:
+ return "hello"
+
+ assert regular_function() == "hello"
+ assert callback_called.wait(10)
+ assert self._async_result.get() == "hello"
+
+ def test_multiple_callbacks(self) -> None:
+ mockback1 = Mock(name="mockback1")
+ mockback2 = Mock(name="mockback2")
+
+ self._async_result.rawlink(mockback1)
+ self._async_result.rawlink(mockback2)
+ self._async_result.set("howdy")
+ self._async_result.wait()
+
+ mockback2.assert_called_once_with(self._async_result)
+ mockback1.assert_called_once_with(self._async_result)
diff --git a/kazoo/testing/common.py b/kazoo/testing/common.py
index 2c2d8e276..70f1c2269 100644
--- a/kazoo/testing/common.py
+++ b/kazoo/testing/common.py
@@ -1,604 +1,604 @@
-#
-# Copyright (C) 2010-2011, 2011 Canonical Ltd. All Rights Reserved
-#
-# This file was originally taken from txzookeeper and modified later.
-#
-# Authors:
-# Kapil Thangavelu and the Kazoo team
-#
-# txzookeeper is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# txzookeeper is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with txzookeeper. If not, see .
-from __future__ import annotations
-
-import code
-import datetime
-import logging
-import os
-import os.path
-import pathlib
-import shutil
-import signal
-import subprocess
-import tempfile
-import traceback
-from collections import namedtuple
-from glob import glob
-from itertools import chain
-from typing import TYPE_CHECKING, Any, TypedDict
-
-import jks
-from cryptography import x509
-from cryptography.hazmat.primitives import asymmetric, hashes, serialization
-from cryptography.x509.oid import NameOID
-
-if TYPE_CHECKING:
- from collections.abc import Iterator
- from types import FrameType
-
-log = logging.getLogger(__name__)
-
-
-def debug(sig: int, frame: FrameType | None) -> None:
- """Interrupt running process, and provide a python prompt for
- interactive debugging."""
- d = {"_frame": frame} # Allow access to frame object.
- if frame is not None:
- d.update(frame.f_globals) # Unless shadowed by global
- d.update(frame.f_locals)
-
- i = code.InteractiveConsole(d)
- message = "Signal received : entering python shell.\nTraceback:\n"
- message += "".join(traceback.format_stack(frame))
- i.interact(message)
-
-
-def listen() -> None:
- if os.name != "nt": # SIGUSR1 is not supported on Windows
- signal.signal(signal.SIGUSR1, debug) # Register handler
-
-
-listen()
-
-
-def to_java_compatible_path(path: str) -> str:
- if os.name == "nt":
- path = path.replace("\\", "/")
- return path
-
-
-def _x509_common_name(value: str) -> x509.Name:
- return x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, value)])
-
-
-ServerInfo = namedtuple(
- "ServerInfo",
- "server_id client_port secure_client_port "
- "election_port leader_port admin_port peer_type",
-)
-
-
-class SSLCerts(TypedDict):
- """TypedDict for SSL certificate information."""
-
- ca_cert: x509.Certificate
- ca_key: asymmetric.rsa.RSAPrivateKey
- ca_cert_pem: bytes
- server_cert: x509.Certificate
- server_key: asymmetric.rsa.RSAPrivateKey
- client_cert: x509.Certificate
- client_key: asymmetric.rsa.RSAPrivateKey
- client_cert_pem: bytes
- client_key_pem: bytes
- truststore: jks.KeyStore
- keystore: jks.KeyStore
-
-
-class SSLClientConfiguration(TypedDict):
- """TypedDict for SSL Client configuration."""
-
- client_key: bytes
- client_cert: bytes
- ca_cert: bytes
-
-
-class ManagedZooKeeper:
- """Class to manage the running of a ZooKeeper instance for testing.
-
- Note: no attempt is made to probe the ZooKeeper instance is
- actually available, or that the selected port is free. In the
- future, we may want to do that, especially when run in a
- Hudson/Buildbot context, to ensure more test robustness."""
-
- def __init__(
- self,
- software_path: str,
- server_info: ServerInfo,
- peers: list[ServerInfo],
- *,
- classpath: str,
- configuration_entries: list[str],
- java_system_properties: list[str],
- ssl_configuration: SSLCerts,
- jaas_config: str | None = None,
- ):
- """Define the ZooKeeper test instance.
-
- @param install_path: The path to the install for ZK
- @param port: The port to run the managed ZK instance
- """
- self.install_path = software_path
- self._classpath = classpath
- self.server_info = server_info
- self.host = "127.0.0.1"
- self.peers = peers
- self.working_path: str = tempfile.mkdtemp()
- self._running: bool = False
- self.configuration_entries = configuration_entries
- self.java_system_properties = java_system_properties
- self.jaas_config = jaas_config
- self.ssl_configuration = ssl_configuration
-
- def run(self) -> None:
- """Run the ZooKeeper instance under a temporary directory.
-
- Writes ZK log messages to zookeeper.log in the current directory.
- """
- if self.running:
- return
- config_path = os.path.join(self.working_path, "zoo.cfg")
- jaas_config_path = os.path.join(self.working_path, "jaas.conf")
- log_path = os.path.join(self.working_path, "log")
- log4j_path = os.path.join(self.working_path, "log4j.properties")
- data_path = os.path.join(self.working_path, "data")
- truststore_path = os.path.join(self.working_path, "truststore.jks")
- keystore_path = os.path.join(self.working_path, "keystore.jks")
-
- # various setup steps
- if not os.path.exists(self.working_path):
- os.mkdir(self.working_path)
- if not os.path.exists(log_path):
- os.mkdir(log_path)
- if not os.path.exists(data_path):
- os.mkdir(data_path)
-
- try:
- self.ssl_configuration["truststore"].save(
- truststore_path, "apassword"
- )
- self.ssl_configuration["keystore"].save(keystore_path, "apassword")
- except Exception:
- log.exception("Unable to perform SSL configuration: ")
- raise
-
- with open(config_path, "w") as config:
- config.write(
- """
-tickTime=2000
-dataDir=%s
-clientPort=%s
-secureClientPort=%s
-maxClientCnxns=0
-admin.serverPort=%s
-serverCnxnFactory=org.apache.zookeeper.server.NettyServerCnxnFactory
-authProvider.1=org.apache.zookeeper.server.auth.SASLAuthenticationProvider
-ssl.keyStore.location=%s
-ssl.keyStore.password=apassword
-ssl.trustStore.location=%s
-ssl.trustStore.password=apassword
-%s
-"""
- % (
- to_java_compatible_path(data_path),
- self.server_info.client_port,
- self.server_info.secure_client_port,
- self.server_info.admin_port,
- to_java_compatible_path(keystore_path),
- to_java_compatible_path(truststore_path),
- "\n".join(self.configuration_entries),
- )
- )
-
- # setup a replicated setup if peers are specified
- if self.peers:
- servers_cfg = []
- for p in chain((self.server_info,), self.peers):
- servers_cfg.append(
- "server.%s=localhost:%s:%s:%s"
- % (
- p.server_id,
- p.leader_port,
- p.election_port,
- p.peer_type,
- )
- )
-
- with open(config_path, "a") as config:
- config.write(
- """
-initLimit=4
-syncLimit=2
-%s
-peerType=%s
-"""
- % ("\n".join(servers_cfg), self.server_info.peer_type)
- )
-
- # Write server ids into datadir
- with open(os.path.join(data_path, "myid"), "w") as myid_file:
- myid_file.write(str(self.server_info.server_id))
- # Write JAAS configuration
- with open(jaas_config_path, "w") as jaas_file:
- jaas_file.write(self.jaas_config or "")
- with open(log4j_path, "w") as log4j:
- log4j.write(
- """
-# DEFAULT: console appender only
-log4j.rootLogger=INFO, ROLLINGFILE
-log4j.appender.ROLLINGFILE.layout=org.apache.log4j.PatternLayout
-log4j.appender.ROLLINGFILE.layout.ConversionPattern=%d{ISO8601} \
-[myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n
-log4j.appender.ROLLINGFILE=org.apache.log4j.RollingFileAppender
-log4j.appender.ROLLINGFILE.Threshold=DEBUG
-log4j.appender.ROLLINGFILE.File="""
- + to_java_compatible_path(
- self.working_path + os.sep + "zookeeper.log\n"
- )
- )
-
- args = (
- [
- "java",
- "-cp",
- self.classpath,
- # make_digest_acl_credential assumes UTF-8, but ZK decodes
- # digest auth packets using the JVM's default "charset"--which
- # depends on the environment. Force it to use UTF-8 to avoid
- # test failures.
- "-Dfile.encoding=UTF-8",
- # "-Dlog4j.debug",
- "-Dreadonlymode.enabled=true",
- "-Dzookeeper.log.dir=%s" % log_path,
- "-Dzookeeper.root.logger=INFO,CONSOLE",
- "-Dlog4j.configuration=file:%s" % log4j_path,
- # OS X: Prevent java from appearing in menu bar, process dock
- # and from activation of the main workspace on run.
- "-Djava.awt.headless=true",
- # JAAS configuration for SASL authentication
- "-Djava.security.auth.login.config=%s" % jaas_config_path,
- ]
- + list(self.java_system_properties)
- + [
- "org.apache.zookeeper.server.quorum.QuorumPeerMain",
- config_path,
- ]
- )
- self.process = subprocess.Popen(args=args)
- log.info(
- "Started zookeeper process %s on port %s using args %s",
- self.process.pid,
- self.server_info.client_port,
- args,
- )
- self._running = True
-
- @property
- def classpath(self) -> str:
- """Get the classpath necessary to run ZooKeeper."""
-
- if self._classpath:
- return self._classpath
-
- # Two possibilities, as seen in zkEnv.sh:
- # Check for a release - top-level zookeeper-*.jar?
- jars = glob(os.path.join(self.install_path, "zookeeper-*.jar"))
- if jars:
- # Release build (`ant package`)
- jars.extend(glob(os.path.join(self.install_path, "lib", "*.jar")))
- jars.extend(glob(os.path.join(self.install_path, "*.jar")))
- # support for different file locations on Debian/Ubuntu
- jars.extend(glob(os.path.join(self.install_path, "log4j-*.jar")))
- jars.extend(
- glob(os.path.join(self.install_path, "slf4j-api-*.jar"))
- )
- jars.extend(
- glob(os.path.join(self.install_path, "slf4j-log4j*.jar"))
- )
- else:
- # Development build (plain `ant`)
- jars = glob(
- os.path.join(self.install_path, "build", "zookeeper-*.jar")
- )
- jars.extend(
- glob(os.path.join(self.install_path, "build", "lib", "*.jar"))
- )
-
- return os.pathsep.join(jars)
-
- @property
- def address(self) -> str:
- """Get the address of the ZooKeeper instance."""
- return "%s:%s" % (self.host, self.client_port)
-
- @property
- def secure_address(self) -> str:
- """Get the address of the SSL ZooKeeper instance."""
- return "%s:%s" % (self.host, self.secure_client_port)
-
- @property
- def running(self) -> bool:
- return self._running
-
- @property
- def client_port(self) -> Any:
- return self.server_info.client_port
-
- @property
- def secure_client_port(self) -> Any:
- return self.server_info.secure_client_port
-
- def reset(self) -> None:
- """Stop the zookeeper instance, cleaning out its on disk-data."""
- self.stop()
- shutil.rmtree(os.path.join(self.working_path, "data"), True)
- os.mkdir(os.path.join(self.working_path, "data"))
- with open(os.path.join(self.working_path, "data", "myid"), "w") as fh:
- fh.write(str(self.server_info.server_id))
-
- def stop(self) -> None:
- """Stop the Zookeeper instance, retaining on disk state."""
- if not self.running:
- return
- self.process.terminate()
- self.process.wait()
- if self.process.returncode != 0:
- log.warning(
- "Zookeeper process %s failed to terminate with"
- " non-zero return code (it terminated with %s return"
- " code instead)",
- self.process.pid,
- self.process.returncode,
- )
- self._running = False
-
- def destroy(self) -> None:
- """Stop the ZooKeeper instance and destroy its on disk-state"""
- # called by at exit handler, reimport to avoid cleanup race.
- self.stop()
-
- shutil.rmtree(self.working_path, True)
-
- def get_logs(self, num_lines: int = 100) -> list[str]:
- log_path = pathlib.Path(self.working_path, "zookeeper.log")
- if log_path.exists():
- with log_path.open("r") as log_file:
- lines = log_file.readlines()
- return lines[-num_lines:]
- return []
-
-
-class ZookeeperCluster:
- def __init__(
- self,
- install_path: str,
- classpath: str,
- size: int,
- port_offset: int,
- observer_start_id: int,
- configuration_entries: list[str],
- java_system_properties: list[str],
- jaas_config: str | None,
- ):
- self._install_path = install_path
- self._classpath = classpath
- self._servers = []
- self._ssl_configuration = self._perform_ssl_certs_generation()
-
- # Calculate ports and peer group
- port = port_offset
- peers: list[ServerInfo] = []
-
- for i in range(size):
- server_id = i + 1
- if observer_start_id != -1 and server_id >= observer_start_id:
- peer_type = "observer"
- else:
- peer_type = "participant"
- info = ServerInfo(
- server_id,
- port,
- port + 4,
- port + 1,
- port + 2,
- port + 3,
- peer_type,
- )
- peers.append(info)
- port += 10
-
- # Instantiate Managed ZK Servers
- for i in range(size):
- server_peers = list(peers)
- server_info = server_peers.pop(i)
- self._servers.append(
- ManagedZooKeeper(
- self._install_path,
- server_info,
- server_peers,
- classpath=self._classpath,
- configuration_entries=configuration_entries,
- java_system_properties=java_system_properties,
- jaas_config=jaas_config,
- ssl_configuration=self._ssl_configuration,
- )
- )
-
- def __getitem__(self, k: int) -> ManagedZooKeeper:
- return self._servers[k]
-
- def __iter__(self) -> Iterator[ManagedZooKeeper]:
- return iter(self._servers)
-
- def start(self) -> None:
- # Zookeeper client expresses a preference for either lower ports or
- # lexicographical ordering of hosts, to ensure that all servers have a
- # chance to startup, start them in reverse order.
- for server in reversed(list(self)):
- server.run()
- # Giving the servers a moment to start, decreases the overall time
- # required for a client to successfully connect (2s vs. 4s without
- # the sleep).
- import time
-
- time.sleep(2)
-
- def stop(self) -> None:
- for server in self:
- server.stop()
- self._servers = []
-
- def terminate(self) -> None:
- for server in self:
- server.destroy()
-
- def reset(self) -> None:
- for server in self:
- server.reset()
-
- def get_logs(self) -> list[str]:
- logs = []
- for server in self:
- logs += server.get_logs()
- return logs
-
- def _perform_ssl_certs_generation(self) -> SSLCerts:
- ca_private_key = asymmetric.rsa.generate_private_key(
- public_exponent=65537, key_size=2048
- )
- ca_name = _x509_common_name("ca.kazoo.org")
- ca_public_key = ca_private_key.public_key()
- not_valid_before = datetime.datetime.now(tz=datetime.timezone.utc)
- not_valid_after = not_valid_before + datetime.timedelta(days=1)
- ca_cert = x509.CertificateBuilder(
- issuer_name=ca_name,
- subject_name=ca_name,
- public_key=ca_public_key,
- serial_number=1,
- not_valid_before=not_valid_before,
- not_valid_after=not_valid_after,
- extensions=[
- x509.Extension(
- x509.BasicConstraints.oid,
- critical=True,
- value=x509.BasicConstraints(ca=True, path_length=0),
- ),
- x509.Extension(
- x509.KeyUsage.oid,
- critical=True,
- value=x509.KeyUsage(
- digital_signature=False,
- content_commitment=False,
- key_encipherment=False,
- data_encipherment=False,
- key_agreement=False,
- key_cert_sign=True,
- crl_sign=True,
- encipher_only=False,
- decipher_only=False,
- ),
- ),
- x509.Extension(
- x509.SubjectKeyIdentifier.oid,
- critical=False,
- value=x509.SubjectKeyIdentifier.from_public_key(
- ca_public_key
- ),
- ),
- ],
- ).sign(ca_private_key, hashes.SHA256())
-
- # generate server cert
- server_private_key = asymmetric.rsa.generate_private_key(
- public_exponent=65537, key_size=2048
- )
- server_public_key = server_private_key.public_key()
-
- server_cert = x509.CertificateBuilder(
- issuer_name=ca_name,
- subject_name=_x509_common_name("localhost"),
- public_key=server_public_key,
- serial_number=2,
- not_valid_before=not_valid_before,
- not_valid_after=not_valid_after,
- ).sign(ca_private_key, hashes.SHA256())
-
- # generate client cert
- client_private_key = asymmetric.rsa.generate_private_key(
- public_exponent=65537, key_size=2048
- )
- client_public_key = client_private_key.public_key()
-
- client_cert = x509.CertificateBuilder(
- issuer_name=ca_name,
- subject_name=_x509_common_name("client"),
- public_key=client_public_key,
- serial_number=3,
- not_valid_before=not_valid_before,
- not_valid_after=not_valid_after,
- ).sign(ca_private_key, hashes.SHA256())
-
- dumped_ca_cert = ca_cert.public_bytes(serialization.Encoding.DER)
-
- tce = jks.TrustedCertEntry.new("kazoo ca", dumped_ca_cert)
- truststore = jks.KeyStore.new("jks", [tce])
-
- dumped_server_cert = server_cert.public_bytes(
- serialization.Encoding.DER
- )
- dumped_server_key = server_private_key.private_bytes(
- encoding=serialization.Encoding.DER,
- format=serialization.PrivateFormat.TraditionalOpenSSL,
- encryption_algorithm=serialization.NoEncryption(),
- )
-
- server_pke = jks.PrivateKeyEntry.new(
- "server cert", [dumped_server_cert], dumped_server_key, "rsa_raw"
- )
-
- keystore = jks.KeyStore.new("jks", [server_pke])
-
- return {
- "ca_cert": ca_cert,
- "ca_key": ca_private_key,
- "ca_cert_pem": ca_cert.public_bytes(serialization.Encoding.PEM),
- "server_cert": server_cert,
- "server_key": server_private_key,
- "client_cert": client_cert,
- "client_key": client_private_key,
- "client_cert_pem": client_cert.public_bytes(
- serialization.Encoding.PEM
- ),
- "client_key_pem": client_private_key.private_bytes(
- encoding=serialization.Encoding.PEM,
- format=serialization.PrivateFormat.TraditionalOpenSSL,
- encryption_algorithm=serialization.NoEncryption(),
- ),
- "truststore": truststore,
- "keystore": keystore,
- }
-
- def get_ssl_client_configuration(self) -> SSLClientConfiguration:
- return {
- "client_key": self._ssl_configuration["client_key_pem"],
- "client_cert": self._ssl_configuration["client_cert_pem"],
- "ca_cert": self._ssl_configuration["ca_cert_pem"],
- }
+#
+# Copyright (C) 2010-2011, 2011 Canonical Ltd. All Rights Reserved
+#
+# This file was originally taken from txzookeeper and modified later.
+#
+# Authors:
+# Kapil Thangavelu and the Kazoo team
+#
+# txzookeeper is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# txzookeeper is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with txzookeeper. If not, see .
+from __future__ import annotations
+
+import code
+import datetime
+import logging
+import os
+import os.path
+import pathlib
+import shutil
+import signal
+import subprocess
+import tempfile
+import traceback
+from collections import namedtuple
+from glob import glob
+from itertools import chain
+from typing import TYPE_CHECKING, Any, TypedDict
+
+import jks
+from cryptography import x509
+from cryptography.hazmat.primitives import asymmetric, hashes, serialization
+from cryptography.x509.oid import NameOID
+
+if TYPE_CHECKING:
+ from collections.abc import Iterator
+ from types import FrameType
+
+log = logging.getLogger(__name__)
+
+
+def debug(sig: int, frame: FrameType | None) -> None:
+ """Interrupt running process, and provide a python prompt for
+ interactive debugging."""
+ d = {"_frame": frame} # Allow access to frame object.
+ if frame is not None:
+ d.update(frame.f_globals) # Unless shadowed by global
+ d.update(frame.f_locals)
+
+ i = code.InteractiveConsole(d)
+ message = "Signal received : entering python shell.\nTraceback:\n"
+ message += "".join(traceback.format_stack(frame))
+ i.interact(message)
+
+
+def listen() -> None:
+ if os.name != "nt": # SIGUSR1 is not supported on Windows
+ signal.signal(signal.SIGUSR1, debug) # Register handler
+
+
+listen()
+
+
+def to_java_compatible_path(path: str) -> str:
+ if os.name == "nt":
+ path = path.replace("\\", "/")
+ return path
+
+
+def _x509_common_name(value: str) -> x509.Name:
+ return x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, value)])
+
+
+ServerInfo = namedtuple(
+ "ServerInfo",
+ "server_id client_port secure_client_port "
+ "election_port leader_port admin_port peer_type",
+)
+
+
+class SSLCerts(TypedDict):
+ """TypedDict for SSL certificate information."""
+
+ ca_cert: x509.Certificate
+ ca_key: asymmetric.rsa.RSAPrivateKey
+ ca_cert_pem: bytes
+ server_cert: x509.Certificate
+ server_key: asymmetric.rsa.RSAPrivateKey
+ client_cert: x509.Certificate
+ client_key: asymmetric.rsa.RSAPrivateKey
+ client_cert_pem: bytes
+ client_key_pem: bytes
+ truststore: jks.KeyStore
+ keystore: jks.KeyStore
+
+
+class SSLClientConfiguration(TypedDict):
+ """TypedDict for SSL Client configuration."""
+
+ client_key: bytes
+ client_cert: bytes
+ ca_cert: bytes
+
+
+class ManagedZooKeeper:
+ """Class to manage the running of a ZooKeeper instance for testing.
+
+ Note: no attempt is made to probe the ZooKeeper instance is
+ actually available, or that the selected port is free. In the
+ future, we may want to do that, especially when run in a
+ Hudson/Buildbot context, to ensure more test robustness."""
+
+ def __init__(
+ self,
+ software_path: str,
+ server_info: ServerInfo,
+ peers: list[ServerInfo],
+ *,
+ classpath: str,
+ configuration_entries: list[str],
+ java_system_properties: list[str],
+ ssl_configuration: SSLCerts,
+ jaas_config: str | None = None,
+ ):
+ """Define the ZooKeeper test instance.
+
+ @param install_path: The path to the install for ZK
+ @param port: The port to run the managed ZK instance
+ """
+ self.install_path = software_path
+ self._classpath = classpath
+ self.server_info = server_info
+ self.host = "127.0.0.1"
+ self.peers = peers
+ self.working_path: str = tempfile.mkdtemp()
+ self._running: bool = False
+ self.configuration_entries = configuration_entries
+ self.java_system_properties = java_system_properties
+ self.jaas_config = jaas_config
+ self.ssl_configuration = ssl_configuration
+
+ def run(self) -> None:
+ """Run the ZooKeeper instance under a temporary directory.
+
+ Writes ZK log messages to zookeeper.log in the current directory.
+ """
+ if self.running:
+ return
+ config_path = os.path.join(self.working_path, "zoo.cfg")
+ jaas_config_path = os.path.join(self.working_path, "jaas.conf")
+ log_path = os.path.join(self.working_path, "log")
+ log4j_path = os.path.join(self.working_path, "log4j.properties")
+ data_path = os.path.join(self.working_path, "data")
+ truststore_path = os.path.join(self.working_path, "truststore.jks")
+ keystore_path = os.path.join(self.working_path, "keystore.jks")
+
+ # various setup steps
+ if not os.path.exists(self.working_path):
+ os.mkdir(self.working_path)
+ if not os.path.exists(log_path):
+ os.mkdir(log_path)
+ if not os.path.exists(data_path):
+ os.mkdir(data_path)
+
+ try:
+ self.ssl_configuration["truststore"].save(
+ truststore_path, "apassword"
+ )
+ self.ssl_configuration["keystore"].save(keystore_path, "apassword")
+ except Exception:
+ log.exception("Unable to perform SSL configuration: ")
+ raise
+
+ with open(config_path, "w") as config:
+ config.write(
+ """
+tickTime=2000
+dataDir=%s
+clientPort=%s
+secureClientPort=%s
+maxClientCnxns=0
+admin.serverPort=%s
+serverCnxnFactory=org.apache.zookeeper.server.NettyServerCnxnFactory
+authProvider.1=org.apache.zookeeper.server.auth.SASLAuthenticationProvider
+ssl.keyStore.location=%s
+ssl.keyStore.password=apassword
+ssl.trustStore.location=%s
+ssl.trustStore.password=apassword
+%s
+"""
+ % (
+ to_java_compatible_path(data_path),
+ self.server_info.client_port,
+ self.server_info.secure_client_port,
+ self.server_info.admin_port,
+ to_java_compatible_path(keystore_path),
+ to_java_compatible_path(truststore_path),
+ "\n".join(self.configuration_entries),
+ )
+ )
+
+ # setup a replicated setup if peers are specified
+ if self.peers:
+ servers_cfg = []
+ for p in chain((self.server_info,), self.peers):
+ servers_cfg.append(
+ "server.%s=localhost:%s:%s:%s"
+ % (
+ p.server_id,
+ p.leader_port,
+ p.election_port,
+ p.peer_type,
+ )
+ )
+
+ with open(config_path, "a") as config:
+ config.write(
+ """
+initLimit=4
+syncLimit=2
+%s
+peerType=%s
+"""
+ % ("\n".join(servers_cfg), self.server_info.peer_type)
+ )
+
+ # Write server ids into datadir
+ with open(os.path.join(data_path, "myid"), "w") as myid_file:
+ myid_file.write(str(self.server_info.server_id))
+ # Write JAAS configuration
+ with open(jaas_config_path, "w") as jaas_file:
+ jaas_file.write(self.jaas_config or "")
+ with open(log4j_path, "w") as log4j:
+ log4j.write(
+ """
+# DEFAULT: console appender only
+log4j.rootLogger=INFO, ROLLINGFILE
+log4j.appender.ROLLINGFILE.layout=org.apache.log4j.PatternLayout
+log4j.appender.ROLLINGFILE.layout.ConversionPattern=%d{ISO8601} \
+[myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n
+log4j.appender.ROLLINGFILE=org.apache.log4j.RollingFileAppender
+log4j.appender.ROLLINGFILE.Threshold=DEBUG
+log4j.appender.ROLLINGFILE.File="""
+ + to_java_compatible_path(
+ self.working_path + os.sep + "zookeeper.log\n"
+ )
+ )
+
+ args = (
+ [
+ "java",
+ "-cp",
+ self.classpath,
+ # make_digest_acl_credential assumes UTF-8, but ZK decodes
+ # digest auth packets using the JVM's default "charset"--which
+ # depends on the environment. Force it to use UTF-8 to avoid
+ # test failures.
+ "-Dfile.encoding=UTF-8",
+ # "-Dlog4j.debug",
+ "-Dreadonlymode.enabled=true",
+ "-Dzookeeper.log.dir=%s" % log_path,
+ "-Dzookeeper.root.logger=INFO,CONSOLE",
+ "-Dlog4j.configuration=file:%s" % log4j_path,
+ # OS X: Prevent java from appearing in menu bar, process dock
+ # and from activation of the main workspace on run.
+ "-Djava.awt.headless=true",
+ # JAAS configuration for SASL authentication
+ "-Djava.security.auth.login.config=%s" % jaas_config_path,
+ ]
+ + list(self.java_system_properties)
+ + [
+ "org.apache.zookeeper.server.quorum.QuorumPeerMain",
+ config_path,
+ ]
+ )
+ self.process = subprocess.Popen(args=args)
+ log.info(
+ "Started zookeeper process %s on port %s using args %s",
+ self.process.pid,
+ self.server_info.client_port,
+ args,
+ )
+ self._running = True
+
+ @property
+ def classpath(self) -> str:
+ """Get the classpath necessary to run ZooKeeper."""
+
+ if self._classpath:
+ return self._classpath
+
+ # Two possibilities, as seen in zkEnv.sh:
+ # Check for a release - top-level zookeeper-*.jar?
+ jars = glob(os.path.join(self.install_path, "zookeeper-*.jar"))
+ if jars:
+ # Release build (`ant package`)
+ jars.extend(glob(os.path.join(self.install_path, "lib", "*.jar")))
+ jars.extend(glob(os.path.join(self.install_path, "*.jar")))
+ # support for different file locations on Debian/Ubuntu
+ jars.extend(glob(os.path.join(self.install_path, "log4j-*.jar")))
+ jars.extend(
+ glob(os.path.join(self.install_path, "slf4j-api-*.jar"))
+ )
+ jars.extend(
+ glob(os.path.join(self.install_path, "slf4j-log4j*.jar"))
+ )
+ else:
+ # Development build (plain `ant`)
+ jars = glob(
+ os.path.join(self.install_path, "build", "zookeeper-*.jar")
+ )
+ jars.extend(
+ glob(os.path.join(self.install_path, "build", "lib", "*.jar"))
+ )
+
+ return os.pathsep.join(jars)
+
+ @property
+ def address(self) -> str:
+ """Get the address of the ZooKeeper instance."""
+ return "%s:%s" % (self.host, self.client_port)
+
+ @property
+ def secure_address(self) -> str:
+ """Get the address of the SSL ZooKeeper instance."""
+ return "%s:%s" % (self.host, self.secure_client_port)
+
+ @property
+ def running(self) -> bool:
+ return self._running
+
+ @property
+ def client_port(self) -> Any:
+ return self.server_info.client_port
+
+ @property
+ def secure_client_port(self) -> Any:
+ return self.server_info.secure_client_port
+
+ def reset(self) -> None:
+ """Stop the zookeeper instance, cleaning out its on disk-data."""
+ self.stop()
+ shutil.rmtree(os.path.join(self.working_path, "data"), True)
+ os.mkdir(os.path.join(self.working_path, "data"))
+ with open(os.path.join(self.working_path, "data", "myid"), "w") as fh:
+ fh.write(str(self.server_info.server_id))
+
+ def stop(self) -> None:
+ """Stop the Zookeeper instance, retaining on disk state."""
+ if not self.running:
+ return
+ self.process.terminate()
+ self.process.wait()
+ if self.process.returncode != 0:
+ log.warning(
+ "Zookeeper process %s failed to terminate with"
+ " non-zero return code (it terminated with %s return"
+ " code instead)",
+ self.process.pid,
+ self.process.returncode,
+ )
+ self._running = False
+
+ def destroy(self) -> None:
+ """Stop the ZooKeeper instance and destroy its on disk-state"""
+ # called by at exit handler, reimport to avoid cleanup race.
+ self.stop()
+
+ shutil.rmtree(self.working_path, True)
+
+ def get_logs(self, num_lines: int = 100) -> list[str]:
+ log_path = pathlib.Path(self.working_path, "zookeeper.log")
+ if log_path.exists():
+ with log_path.open("r") as log_file:
+ lines = log_file.readlines()
+ return lines[-num_lines:]
+ return []
+
+
+class ZookeeperCluster:
+ def __init__(
+ self,
+ install_path: str,
+ classpath: str,
+ size: int,
+ port_offset: int,
+ observer_start_id: int,
+ configuration_entries: list[str],
+ java_system_properties: list[str],
+ jaas_config: str | None,
+ ):
+ self._install_path = install_path
+ self._classpath = classpath
+ self._servers = []
+ self._ssl_configuration = self._perform_ssl_certs_generation()
+
+ # Calculate ports and peer group
+ port = port_offset
+ peers: list[ServerInfo] = []
+
+ for i in range(size):
+ server_id = i + 1
+ if observer_start_id != -1 and server_id >= observer_start_id:
+ peer_type = "observer"
+ else:
+ peer_type = "participant"
+ info = ServerInfo(
+ server_id,
+ port,
+ port + 4,
+ port + 1,
+ port + 2,
+ port + 3,
+ peer_type,
+ )
+ peers.append(info)
+ port += 10
+
+ # Instantiate Managed ZK Servers
+ for i in range(size):
+ server_peers = list(peers)
+ server_info = server_peers.pop(i)
+ self._servers.append(
+ ManagedZooKeeper(
+ self._install_path,
+ server_info,
+ server_peers,
+ classpath=self._classpath,
+ configuration_entries=configuration_entries,
+ java_system_properties=java_system_properties,
+ jaas_config=jaas_config,
+ ssl_configuration=self._ssl_configuration,
+ )
+ )
+
+ def __getitem__(self, k: int) -> ManagedZooKeeper:
+ return self._servers[k]
+
+ def __iter__(self) -> Iterator[ManagedZooKeeper]:
+ return iter(self._servers)
+
+ def start(self) -> None:
+ # Zookeeper client expresses a preference for either lower ports or
+ # lexicographical ordering of hosts, to ensure that all servers have a
+ # chance to startup, start them in reverse order.
+ for server in reversed(list(self)):
+ server.run()
+ # Giving the servers a moment to start, decreases the overall time
+ # required for a client to successfully connect (2s vs. 4s without
+ # the sleep).
+ import time
+
+ time.sleep(2)
+
+ def stop(self) -> None:
+ for server in self:
+ server.stop()
+ self._servers = []
+
+ def terminate(self) -> None:
+ for server in self:
+ server.destroy()
+
+ def reset(self) -> None:
+ for server in self:
+ server.reset()
+
+ def get_logs(self) -> list[str]:
+ logs = []
+ for server in self:
+ logs += server.get_logs()
+ return logs
+
+ def _perform_ssl_certs_generation(self) -> SSLCerts:
+ ca_private_key = asymmetric.rsa.generate_private_key(
+ public_exponent=65537, key_size=2048
+ )
+ ca_name = _x509_common_name("ca.kazoo.org")
+ ca_public_key = ca_private_key.public_key()
+ not_valid_before = datetime.datetime.now(tz=datetime.timezone.utc)
+ not_valid_after = not_valid_before + datetime.timedelta(days=1)
+ ca_cert = x509.CertificateBuilder(
+ issuer_name=ca_name,
+ subject_name=ca_name,
+ public_key=ca_public_key,
+ serial_number=1,
+ not_valid_before=not_valid_before,
+ not_valid_after=not_valid_after,
+ extensions=[
+ x509.Extension(
+ x509.BasicConstraints.oid,
+ critical=True,
+ value=x509.BasicConstraints(ca=True, path_length=0),
+ ),
+ x509.Extension(
+ x509.KeyUsage.oid,
+ critical=True,
+ value=x509.KeyUsage(
+ digital_signature=False,
+ content_commitment=False,
+ key_encipherment=False,
+ data_encipherment=False,
+ key_agreement=False,
+ key_cert_sign=True,
+ crl_sign=True,
+ encipher_only=False,
+ decipher_only=False,
+ ),
+ ),
+ x509.Extension(
+ x509.SubjectKeyIdentifier.oid,
+ critical=False,
+ value=x509.SubjectKeyIdentifier.from_public_key(
+ ca_public_key
+ ),
+ ),
+ ],
+ ).sign(ca_private_key, hashes.SHA256())
+
+ # generate server cert
+ server_private_key = asymmetric.rsa.generate_private_key(
+ public_exponent=65537, key_size=2048
+ )
+ server_public_key = server_private_key.public_key()
+
+ server_cert = x509.CertificateBuilder(
+ issuer_name=ca_name,
+ subject_name=_x509_common_name("localhost"),
+ public_key=server_public_key,
+ serial_number=2,
+ not_valid_before=not_valid_before,
+ not_valid_after=not_valid_after,
+ ).sign(ca_private_key, hashes.SHA256())
+
+ # generate client cert
+ client_private_key = asymmetric.rsa.generate_private_key(
+ public_exponent=65537, key_size=2048
+ )
+ client_public_key = client_private_key.public_key()
+
+ client_cert = x509.CertificateBuilder(
+ issuer_name=ca_name,
+ subject_name=_x509_common_name("client"),
+ public_key=client_public_key,
+ serial_number=3,
+ not_valid_before=not_valid_before,
+ not_valid_after=not_valid_after,
+ ).sign(ca_private_key, hashes.SHA256())
+
+ dumped_ca_cert = ca_cert.public_bytes(serialization.Encoding.DER)
+
+ tce = jks.TrustedCertEntry.new("kazoo ca", dumped_ca_cert)
+ truststore = jks.KeyStore.new("jks", [tce])
+
+ dumped_server_cert = server_cert.public_bytes(
+ serialization.Encoding.DER
+ )
+ dumped_server_key = server_private_key.private_bytes(
+ encoding=serialization.Encoding.DER,
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=serialization.NoEncryption(),
+ )
+
+ server_pke = jks.PrivateKeyEntry.new(
+ "server cert", [dumped_server_cert], dumped_server_key, "rsa_raw"
+ )
+
+ keystore = jks.KeyStore.new("jks", [server_pke])
+
+ return {
+ "ca_cert": ca_cert,
+ "ca_key": ca_private_key,
+ "ca_cert_pem": ca_cert.public_bytes(serialization.Encoding.PEM),
+ "server_cert": server_cert,
+ "server_key": server_private_key,
+ "client_cert": client_cert,
+ "client_key": client_private_key,
+ "client_cert_pem": client_cert.public_bytes(
+ serialization.Encoding.PEM
+ ),
+ "client_key_pem": client_private_key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=serialization.NoEncryption(),
+ ),
+ "truststore": truststore,
+ "keystore": keystore,
+ }
+
+ def get_ssl_client_configuration(self) -> SSLClientConfiguration:
+ return {
+ "client_key": self._ssl_configuration["client_key_pem"],
+ "client_cert": self._ssl_configuration["client_cert_pem"],
+ "ca_cert": self._ssl_configuration["ca_cert_pem"],
+ }
diff --git a/kazoo/tests/test_async_result.py b/kazoo/tests/test_async_result.py
new file mode 100644
index 000000000..1a0706ff8
--- /dev/null
+++ b/kazoo/tests/test_async_result.py
@@ -0,0 +1,106 @@
+from __future__ import annotations
+
+import threading
+import unittest
+
+from typing import Any, Callable
+from unittest.mock import Mock
+
+# Using AsyncResult from kazoo.handlers.utils isn't ideal, but it allows us to
+# smoke test the AsyncResult interface without having to depend on a specific
+# handler implementation or writing a mock.
+# Actually this whole thing is probably now useless, since well call this from
+# the other handler tests, but leaving it here for now.
+from kazoo.handlers.utils import AsyncResult
+from kazoo.interfaces import IHandler, SpawnedFunc
+from kazoo.testing.async_result_common import (
+ AsyncResultCommon,
+ ThreadlikeContext,
+)
+
+
+class Mock_Condition:
+ def __init__(self) -> None:
+ self._event = threading.Event()
+
+ def __enter__(self) -> Mock_Condition:
+ return self
+
+ def __exit__(self, *exc_info: object) -> None:
+ pass
+
+ def wait(self, timeout: float | None = None) -> bool:
+ return self._event.wait(timeout)
+
+ def notify_all(self) -> None:
+ self._event.set()
+
+
+class Mock_Timeout(BaseException):
+ pass
+
+
+class Mock_Handler(Mock):
+ _running: bool
+
+ @property
+ def running(self) -> bool:
+ return self._running
+
+
+class ThreadContext(ThreadlikeContext):
+ def __init__(self, target: Callable[[], None]):
+ super().__init__(target)
+ self._thread = threading.Thread(target=self._target)
+
+ def __enter__(self) -> None:
+ self._thread.start()
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: object | None,
+ ) -> None:
+ self._thread.join()
+
+
+class TestAsyncResult(AsyncResultCommon, unittest.TestCase):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ def condition_factory() -> Mock_Condition:
+ return Mock_Condition()
+
+ def timeout_factory() -> Mock_Timeout:
+ return Mock_Timeout()
+
+ def handler_factory() -> Mock_Handler:
+ handler = Mock_Handler(spec=IHandler)
+ handler._running = False
+
+ def start() -> None:
+ handler._running = True
+
+ def stop() -> None:
+ handler._running = False
+
+ def add_to_completion_queue(callback: SpawnedFunc) -> None:
+ callback()
+
+ handler.start.side_effect = start
+ handler.stop.side_effect = stop
+ handler.add_to_completion_queue.side_effect = (
+ add_to_completion_queue
+ )
+ handler.timeout_exception = Mock_Timeout
+ handler.event_object = lambda: threading.Event()
+ return handler
+
+ super().__init__(
+ handler_factory,
+ lambda handler: AsyncResult(
+ handler, condition_factory, timeout_factory
+ ),
+ thread_context_factory=ThreadContext,
+ *args,
+ **kwargs,
+ )
diff --git a/kazoo/tests/test_cache.py b/kazoo/tests/test_cache.py
index 15a672018..1606e061a 100644
--- a/kazoo/tests/test_cache.py
+++ b/kazoo/tests/test_cache.py
@@ -4,7 +4,8 @@
import importlib
import sys
import uuid
-from typing import Any, TYPE_CHECKING
+from contextlib import contextmanager
+from typing import Any, Iterator, TYPE_CHECKING
from unittest.mock import patch, call, Mock
import pytest
@@ -53,7 +54,7 @@ def choose_an_installed_handler(
try:
mod = importlib.import_module(handler_module)
cls = getattr(mod, handler_class)
- except ImportError:
+ except (ImportError, NameError):
continue
else:
return cls() # type: ignore[no-any-return]
@@ -66,7 +67,6 @@ def setUp(self) -> None:
self._event_queue: Queue[TreeEvent] = self.client.handler.queue_impl()
self._error_queue = self.client.handler.queue_impl()
self._path: str | None = None
- self._cache: TreeCache | None = None
def tearDown(self) -> None:
if not self._error_queue.empty():
@@ -74,29 +74,22 @@ def tearDown(self) -> None:
raise self._error_queue.get()
except FakeException:
pass
- if self._cache is not None:
- self._cache.close()
- self._cache = None
super().tearDown()
- def make_cache(self) -> TreeCache:
- if self._cache is None:
- self._path = "/" + uuid.uuid4().hex
- self._cache = TreeCache(self.client, self.path)
- self._cache.listen(lambda event: self._event_queue.put(event))
- self._cache.listen_fault(
- lambda error: self._error_queue.put(error)
- )
- self._cache.start()
- return self._cache
+ @contextmanager
+ def make_cache(self) -> Iterator[TreeCache]:
+ self._path = "/" + uuid.uuid4().hex
+ assert self.count_tree_node() == 0
+ cache = TreeCache(self.client, self.path)
+ assert self.count_tree_node() == 1
+ cache.listen(lambda event: self._event_queue.put(event))
+ cache.listen_fault(lambda error: self._error_queue.put(error))
+ cache.start()
- # FIXME This is entirely for the purpose of minimising code changes.
- # Calling make_cache twice should be an error and the return value
- # should be used, not stored.
- @property
- def cache(self) -> TreeCache:
- assert self._cache is not None
- return self._cache
+ try:
+ yield cache
+ finally:
+ cache.close()
@property
def path(self) -> str:
@@ -129,8 +122,9 @@ def _wait_gc(self) -> None:
# trigger switching on some coroutine handlers
self.client.handler.sleep_func(0.1)
- completion_queue = getattr(self.handler, "completion_queue", None)
+ completion_queue = getattr(self.handler, "_completion_queue", None)
if completion_queue is not None:
+ print(completion_queue, file=sys.stderr)
while not completion_queue.empty():
self.client.handler.sleep_func(0.1)
@@ -149,317 +143,337 @@ def count_tree_node(self) -> int:
raise RuntimeError("could not count refs exactly")
def test_start(self) -> None:
- self.make_cache()
- self.wait_cache(since=TreeEvent.INITIALIZED)
+ with self.make_cache() as cache:
+ self.wait_cache(since=TreeEvent.INITIALIZED)
- stat = self.client.exists(self.path)
- assert stat is not None
- assert stat.version == 0
+ stat = self.client.exists(self.path)
+ assert stat is not None
+ assert stat.version == 0
- assert self.cache._state == TreeCache.STATE_STARTED
- assert self.cache._root._state == TreeNode.STATE_LIVE
+ assert cache._state == TreeCache.STATE_STARTED
+ assert cache._root._state == TreeNode.STATE_LIVE
def test_start_started(self) -> None:
- self.make_cache()
- with pytest.raises(KazooException):
- self.cache.start()
+ with self.make_cache() as cache:
+ with pytest.raises(KazooException):
+ cache.start()
def test_start_closed(self) -> None:
- self.make_cache()
- self.cache.close()
- with pytest.raises(KazooException):
- self.cache.start()
+ with self.make_cache() as cache:
+ cache.close()
+ with pytest.raises(KazooException):
+ cache.start()
def test_close(self) -> None:
- assert self.count_tree_node() == 0
+ with self.make_cache() as cache:
+ self.wait_cache(since=TreeEvent.INITIALIZED)
+ self.client.create(self.path + "/foo/bar/baz", makepath=True)
+ for _ in range(3):
+ self.wait_cache(TreeEvent.NODE_ADDED)
+
+ # setup stub watchers which are outside of tree cache
+ stub_data_watcher = Mock(spec=lambda event: None)
+ stub_child_watcher = Mock(spec=lambda event: None)
+ self.client.get(self.path + "/foo", stub_data_watcher)
+ self.client.get_children(self.path + "/foo", stub_child_watcher)
+
+ # watchers inside tree cache should be here
+ root_path = self.client.chroot + self.path
+ assert len(self.client._data_watchers[root_path + "/foo"]) == 2
+ assert len(self.client._data_watchers[root_path + "/foo/bar"]) == 1
+ assert (
+ len(self.client._data_watchers[root_path + "/foo/bar/baz"])
+ == 1
+ )
+ assert len(self.client._child_watchers[root_path + "/foo"]) == 2
+ assert (
+ len(self.client._child_watchers[root_path + "/foo/bar"]) == 1
+ )
+ assert (
+ len(self.client._child_watchers[root_path + "/foo/bar/baz"])
+ == 1
+ )
- self.make_cache()
- self.wait_cache(since=TreeEvent.INITIALIZED)
- self.client.create(self.path + "/foo/bar/baz", makepath=True)
- for _ in range(3):
- self.wait_cache(TreeEvent.NODE_ADDED)
+ cache.close()
+
+ # nothing should be published since tree closed
+ assert self._event_queue.empty()
+
+ # tree should be empty
+ assert cache._root._children == {}
+ assert cache._root._data is None
+ assert cache._state == TreeCache.STATE_CLOSED
+
+ # node state should not be changed
+ assert cache._root._state != TreeNode.STATE_DEAD
+
+ # watchers should be reset
+ assert len(self.client._data_watchers[root_path + "/foo"]) == 1
+ assert len(self.client._data_watchers[root_path + "/foo/bar"]) == 0
+ assert (
+ len(self.client._data_watchers[root_path + "/foo/bar/baz"])
+ == 0
+ )
+ assert len(self.client._child_watchers[root_path + "/foo"]) == 1
+ assert (
+ len(self.client._child_watchers[root_path + "/foo/bar"]) == 0
+ )
+ assert (
+ len(self.client._child_watchers[root_path + "/foo/bar/baz"])
+ == 0
+ )
- # setup stub watchers which are outside of tree cache
- stub_data_watcher = Mock(spec=lambda event: None)
- stub_child_watcher = Mock(spec=lambda event: None)
- self.client.get(self.path + "/foo", stub_data_watcher)
- self.client.get_children(self.path + "/foo", stub_child_watcher)
-
- # watchers inside tree cache should be here
- root_path = self.client.chroot + self.path
- assert len(self.client._data_watchers[root_path + "/foo"]) == 2
- assert len(self.client._data_watchers[root_path + "/foo/bar"]) == 1
- assert len(self.client._data_watchers[root_path + "/foo/bar/baz"]) == 1
- assert len(self.client._child_watchers[root_path + "/foo"]) == 2
- assert len(self.client._child_watchers[root_path + "/foo/bar"]) == 1
- assert (
- len(self.client._child_watchers[root_path + "/foo/bar/baz"]) == 1
- )
-
- self.cache.close()
-
- # nothing should be published since tree closed
- assert self._event_queue.empty()
-
- # tree should be empty
- assert self.cache._root._children == {}
- assert self.cache._root._data is None
- assert self.cache._state == TreeCache.STATE_CLOSED
-
- # node state should not be changed
- assert self.cache._root._state != TreeNode.STATE_DEAD
-
- # watchers should be reset
- assert len(self.client._data_watchers[root_path + "/foo"]) == 1
- assert len(self.client._data_watchers[root_path + "/foo/bar"]) == 0
- assert len(self.client._data_watchers[root_path + "/foo/bar/baz"]) == 0
- assert len(self.client._child_watchers[root_path + "/foo"]) == 1
- assert len(self.client._child_watchers[root_path + "/foo/bar"]) == 0
- assert (
- len(self.client._child_watchers[root_path + "/foo/bar/baz"]) == 0
- )
-
- # outside watchers should not be deleted
- assert (
- list(self.client._data_watchers[root_path + "/foo"])[0]
- == stub_data_watcher
- )
- assert (
- list(self.client._child_watchers[root_path + "/foo"])[0]
- == stub_child_watcher
- )
-
- # FIXME This looks pointless at best.
- self._cache = None
+ # outside watchers should not be deleted
+ assert (
+ list(self.client._data_watchers[root_path + "/foo"])[0]
+ == stub_data_watcher
+ )
+ assert (
+ list(self.client._child_watchers[root_path + "/foo"])[0]
+ == stub_child_watcher
+ )
# should not be any leaked memory (tree node) here
+ cache = None # type: ignore
assert self.count_tree_node() == 0
def test_delete_operation(self) -> None:
- self.make_cache()
- self.wait_cache(since=TreeEvent.INITIALIZED)
-
- assert self.count_tree_node() == 1
+ with self.make_cache() as cache:
+ self.wait_cache(since=TreeEvent.INITIALIZED)
- self.client.create(self.path + "/foo/bar/baz", makepath=True)
- for _ in range(3):
- self.wait_cache(TreeEvent.NODE_ADDED)
+ self.client.create(self.path + "/foo/bar/baz", makepath=True)
+ for _ in range(3):
+ self.wait_cache(TreeEvent.NODE_ADDED)
- self.client.delete(self.path + "/foo", recursive=True)
- for _ in range(3):
- self.wait_cache(TreeEvent.NODE_REMOVED)
+ self.client.delete(self.path + "/foo", recursive=True)
+ for _ in range(3):
+ self.wait_cache(TreeEvent.NODE_REMOVED)
- # tree should be empty
- assert self.cache._root._children == {}
+ # tree should be empty
+ assert cache._root._children == {}
- # watchers should be reset
- root_path = self.client.chroot + self.path
- assert self.client._data_watchers[root_path + "/foo"] == set()
- assert self.client._data_watchers[root_path + "/foo/bar"] == set()
- assert self.client._data_watchers[root_path + "/foo/bar/baz"] == set()
- assert self.client._child_watchers[root_path + "/foo"] == set()
- assert self.client._child_watchers[root_path + "/foo/bar"] == set()
- assert self.client._child_watchers[root_path + "/foo/bar/baz"] == set()
+ # watchers should be reset
+ root_path = self.client.chroot + self.path
+ assert self.client._data_watchers[root_path + "/foo"] == set()
+ assert self.client._data_watchers[root_path + "/foo/bar"] == set()
+ assert (
+ self.client._data_watchers[root_path + "/foo/bar/baz"] == set()
+ )
+ assert self.client._child_watchers[root_path + "/foo"] == set()
+ assert self.client._child_watchers[root_path + "/foo/bar"] == set()
+ assert (
+ self.client._child_watchers[root_path + "/foo/bar/baz"]
+ == set()
+ )
- # should not be any leaked memory (tree node) here
- assert self.count_tree_node() == 1
+ # This should be the only tree left
+ assert self.count_tree_node() == 1
def test_children_operation(self) -> None:
- self.make_cache()
- self.wait_cache(since=TreeEvent.INITIALIZED)
-
- self.client.create(self.path + "/test_children", b"test_children_1")
- event = self.wait_cache(TreeEvent.NODE_ADDED)
- assert event is not None
- assert event.event_type == TreeEvent.NODE_ADDED
- assert event.event_data.path == self.path + "/test_children"
- assert event.event_data.data == b"test_children_1"
- assert event.event_data.stat.version == 0
-
- self.client.set(self.path + "/test_children", b"test_children_2")
- event = self.wait_cache(TreeEvent.NODE_UPDATED)
- assert event is not None
- assert event.event_type == TreeEvent.NODE_UPDATED
- assert event.event_data.path == self.path + "/test_children"
- assert event.event_data.data == b"test_children_2"
- assert event.event_data.stat.version == 1
-
- self.client.delete(self.path + "/test_children")
- event = self.wait_cache(TreeEvent.NODE_REMOVED)
- assert event is not None
- assert event.event_type == TreeEvent.NODE_REMOVED
- assert event.event_data.path == self.path + "/test_children"
- assert event.event_data.data == b"test_children_2"
- assert event.event_data.stat.version == 1
-
- def test_subtree_operation(self) -> None:
- self.make_cache()
- self.wait_cache(since=TreeEvent.INITIALIZED)
+ with self.make_cache():
+ self.wait_cache(since=TreeEvent.INITIALIZED)
- self.client.create(self.path + "/foo/bar/baz", makepath=True)
- for relative_path in ("/foo", "/foo/bar", "/foo/bar/baz"):
+ self.client.create(
+ self.path + "/test_children", b"test_children_1"
+ )
event = self.wait_cache(TreeEvent.NODE_ADDED)
assert event is not None
assert event.event_type == TreeEvent.NODE_ADDED
- assert event.event_data.path == self.path + relative_path
- assert event.event_data.data == b""
+ assert event.event_data.path == self.path + "/test_children"
+ assert event.event_data.data == b"test_children_1"
assert event.event_data.stat.version == 0
- self.client.delete(self.path + "/foo", recursive=True)
- for relative_path in ("/foo/bar/baz", "/foo/bar", "/foo"):
+ self.client.set(self.path + "/test_children", b"test_children_2")
+ event = self.wait_cache(TreeEvent.NODE_UPDATED)
+ assert event is not None
+ assert event.event_type == TreeEvent.NODE_UPDATED
+ assert event.event_data.path == self.path + "/test_children"
+ assert event.event_data.data == b"test_children_2"
+ assert event.event_data.stat.version == 1
+
+ self.client.delete(self.path + "/test_children")
event = self.wait_cache(TreeEvent.NODE_REMOVED)
assert event is not None
assert event.event_type == TreeEvent.NODE_REMOVED
- assert event.event_data.path == self.path + relative_path
+ assert event.event_data.path == self.path + "/test_children"
+ assert event.event_data.data == b"test_children_2"
+ assert event.event_data.stat.version == 1
+
+ def test_subtree_operation(self) -> None:
+ with self.make_cache():
+ self.wait_cache(since=TreeEvent.INITIALIZED)
+
+ self.client.create(self.path + "/foo/bar/baz", makepath=True)
+ for relative_path in ("/foo", "/foo/bar", "/foo/bar/baz"):
+ event = self.wait_cache(TreeEvent.NODE_ADDED)
+ assert event is not None
+ assert event.event_type == TreeEvent.NODE_ADDED
+ assert event.event_data.path == self.path + relative_path
+ assert event.event_data.data == b""
+ assert event.event_data.stat.version == 0
+
+ self.client.delete(self.path + "/foo", recursive=True)
+ for relative_path in ("/foo/bar/baz", "/foo/bar", "/foo"):
+ event = self.wait_cache(TreeEvent.NODE_REMOVED)
+ assert event is not None
+ assert event.event_type == TreeEvent.NODE_REMOVED
+ assert event.event_data.path == self.path + relative_path
def test_get_data(self) -> None:
- cache = self.make_cache()
- self.wait_cache(since=TreeEvent.INITIALIZED)
- self.client.create(self.path + "/foo/bar/baz", b"@", makepath=True)
- self.wait_cache(TreeEvent.NODE_ADDED)
- self.wait_cache(TreeEvent.NODE_ADDED)
- self.wait_cache(TreeEvent.NODE_ADDED)
-
- with patch.object(cache, "_client"): # disable any remote operation
- node = cache.get_data(self.path)
- assert node is not None
- assert node.data == b""
- assert node.stat.version == 0
-
- node = cache.get_data(self.path + "foo")
- assert node is not None
- assert node.data == b""
- assert node.stat.version == 0
-
- node = cache.get_data(self.path + "foo/bar")
- assert node is not None
- assert node.data == b""
- assert node.stat.version == 0
-
- node = cache.get_data(self.path + "foo/bar/baz")
- assert node is not None
- assert node.data == b"@"
- assert node.stat.version == 0
+ with self.make_cache() as cache:
+ self.wait_cache(since=TreeEvent.INITIALIZED)
+ self.client.create(self.path + "/foo/bar/baz", b"@", makepath=True)
+ self.wait_cache(TreeEvent.NODE_ADDED)
+ self.wait_cache(TreeEvent.NODE_ADDED)
+ self.wait_cache(TreeEvent.NODE_ADDED)
+
+ # disable any remote operations
+ with patch.object(cache, "_client"):
+ node = cache.get_data(self.path)
+ assert node is not None
+ assert node.data == b""
+ assert node.stat.version == 0
+
+ node = cache.get_data(self.path + "foo")
+ assert node is not None
+ assert node.data == b""
+ assert node.stat.version == 0
+
+ node = cache.get_data(self.path + "foo/bar")
+ assert node is not None
+ assert node.data == b""
+ assert node.stat.version == 0
+
+ node = cache.get_data(self.path + "foo/bar/baz")
+ assert node is not None
+ assert node.data == b"@"
+ assert node.stat.version == 0
def test_get_children(self) -> None:
- cache = self.make_cache()
- self.wait_cache(since=TreeEvent.INITIALIZED)
- self.client.create(self.path + "/foo/bar/baz", b"@", makepath=True)
- self.wait_cache(TreeEvent.NODE_ADDED)
- self.wait_cache(TreeEvent.NODE_ADDED)
- self.wait_cache(TreeEvent.NODE_ADDED)
-
- with patch.object(cache, "_client"): # disable any remote operation
- assert (
- cache.get_children(self.path + "/foo/bar/baz") == frozenset()
- )
- assert cache.get_children(self.path + "/foo/bar") == frozenset(
- ["baz"]
- )
- assert cache.get_children(self.path + "/foo") == frozenset(["bar"])
- assert cache.get_children(self.path) == frozenset(["foo"])
+ with self.make_cache() as cache:
+ self.wait_cache(since=TreeEvent.INITIALIZED)
+ self.client.create(self.path + "/foo/bar/baz", b"@", makepath=True)
+ self.wait_cache(TreeEvent.NODE_ADDED)
+ self.wait_cache(TreeEvent.NODE_ADDED)
+ self.wait_cache(TreeEvent.NODE_ADDED)
+
+ # Disable any remote operations
+ with patch.object(cache, "_client"):
+ assert (
+ cache.get_children(self.path + "/foo/bar/baz")
+ == frozenset()
+ )
+ assert cache.get_children(self.path + "/foo/bar") == frozenset(
+ ["baz"]
+ )
+ assert cache.get_children(self.path + "/foo") == frozenset(
+ ["bar"]
+ )
+ assert cache.get_children(self.path) == frozenset(["foo"])
def test_get_data_out_of_tree(self) -> None:
- self.make_cache()
- self.wait_cache(since=TreeEvent.INITIALIZED)
- with pytest.raises(ValueError):
- self.cache.get_data("/out_of_tree")
+ with self.make_cache() as cache:
+ self.wait_cache(since=TreeEvent.INITIALIZED)
+ with pytest.raises(ValueError):
+ cache.get_data("/out_of_tree")
def test_get_children_out_of_tree(self) -> None:
- self.make_cache()
- self.wait_cache(since=TreeEvent.INITIALIZED)
- with pytest.raises(ValueError):
- self.cache.get_children("/out_of_tree")
+ with self.make_cache() as cache:
+ self.wait_cache(since=TreeEvent.INITIALIZED)
+ with pytest.raises(ValueError):
+ cache.get_children("/out_of_tree")
def test_get_data_no_node(self) -> None:
- cache = self.make_cache()
- self.wait_cache(since=TreeEvent.INITIALIZED)
+ with self.make_cache() as cache:
+ self.wait_cache(since=TreeEvent.INITIALIZED)
- with patch.object(cache, "_client"): # disable any remote operation
- assert cache.get_data(self.path + "/non_exists") is None
+ with patch.object(cache, "_client"):
+ assert cache.get_data(self.path + "/non_exists") is None
def test_get_children_no_node(self) -> None:
- cache = self.make_cache()
- self.wait_cache(since=TreeEvent.INITIALIZED)
+ with self.make_cache() as cache:
+ self.wait_cache(since=TreeEvent.INITIALIZED)
- with patch.object(cache, "_client"): # disable any remote operation
- assert cache.get_children(self.path + "/non_exists") is None
+ with patch.object(cache, "_client"):
+ assert cache.get_children(self.path + "/non_exists") is None
def test_session_reconnected(self) -> None:
- self.make_cache()
- self.wait_cache(since=TreeEvent.INITIALIZED)
-
- self.client.create(self.path + "/foo")
- event = self.wait_cache(TreeEvent.NODE_ADDED)
- assert event is not None
- assert event.event_data.path == self.path + "/foo"
-
- with self.spy_client("get_async") as get_data:
- with self.spy_client("get_children_async") as get_children:
- # session suspended
- self.lose_connection(self.client.handler.event_object)
- self.wait_cache(TreeEvent.CONNECTION_SUSPENDED)
-
- # There are a serial refreshing operation here. But NODE_ADDED
- # events will not be raised because the zxid of nodes are the
- # same during reconnecting.
-
- # connection restore
- self.wait_cache(TreeEvent.CONNECTION_RECONNECTED)
-
- # wait for outstanding operations
- while self.cache._outstanding_ops > 0:
- self.client.handler.sleep_func(0.1)
-
- # inspect in-memory nodes
- _node_root = self.cache._root
- _node_foo = self.cache._root._children["foo"]
-
- # make sure that all nodes are refreshed
- get_data.assert_has_calls(
- [
- call(self.path, watch=_node_root._process_watch),
- call(
- self.path + "/foo", watch=_node_foo._process_watch
- ),
- ],
- any_order=True,
- )
- get_children.assert_has_calls(
- [
- call(self.path, watch=_node_root._process_watch),
- call(
- self.path + "/foo", watch=_node_foo._process_watch
- ),
- ],
- any_order=True,
- )
+ with self.make_cache() as cache:
+ self.wait_cache(since=TreeEvent.INITIALIZED)
+
+ self.client.create(self.path + "/foo")
+ event = self.wait_cache(TreeEvent.NODE_ADDED)
+ assert event is not None
+ assert event.event_data.path == self.path + "/foo"
+
+ with self.spy_client("get_async") as get_data:
+ with self.spy_client("get_children_async") as get_children:
+ # session suspended
+ self.lose_connection(self.client.handler.event_object)
+ self.wait_cache(TreeEvent.CONNECTION_SUSPENDED)
+
+ # There are a serial refreshing operation here. But
+ # NODE_ADDED events will not be raised because the zxid of
+ # nodes are the same during reconnecting.
+
+ # connection restore
+ self.wait_cache(TreeEvent.CONNECTION_RECONNECTED)
+
+ # wait for outstanding operations
+ while cache._outstanding_ops > 0:
+ self.client.handler.sleep_func(0.1)
+
+ # inspect in-memory nodes
+ _node_root = cache._root
+ _node_foo = cache._root._children["foo"]
+
+ # make sure that all nodes are refreshed
+ get_data.assert_has_calls(
+ [
+ call(self.path, watch=_node_root._process_watch),
+ call(
+ self.path + "/foo",
+ watch=_node_foo._process_watch,
+ ),
+ ],
+ any_order=True,
+ )
+ get_children.assert_has_calls(
+ [
+ call(self.path, watch=_node_root._process_watch),
+ call(
+ self.path + "/foo",
+ watch=_node_foo._process_watch,
+ ),
+ ],
+ any_order=True,
+ )
def test_root_recreated(self) -> None:
- self.make_cache()
- self.wait_cache(since=TreeEvent.INITIALIZED)
-
- # remove root node
- self.client.delete(self.path)
- event = self.wait_cache(TreeEvent.NODE_REMOVED)
- assert event is not None
- assert event.event_type == TreeEvent.NODE_REMOVED
- assert event.event_data.data == b""
- assert event.event_data.path == self.path
- assert event.event_data.stat.version == 0
-
- # re-create root node
- self.client.ensure_path(self.path)
- event = self.wait_cache(TreeEvent.NODE_ADDED)
- assert event is not None
- assert event.event_type == TreeEvent.NODE_ADDED
- assert event.event_data.data == b""
- assert event.event_data.path == self.path
- assert event.event_data.stat.version == 0
-
- assert self.cache._outstanding_ops >= 0, (
- "unexpected outstanding ops %r" % self.cache._outstanding_ops
- )
+ with self.make_cache() as cache:
+ self.wait_cache(since=TreeEvent.INITIALIZED)
+
+ # remove root node
+ self.client.delete(self.path)
+ event = self.wait_cache(TreeEvent.NODE_REMOVED)
+ assert event is not None
+ assert event.event_type == TreeEvent.NODE_REMOVED
+ assert event.event_data.data == b""
+ assert event.event_data.path == self.path
+ assert event.event_data.stat.version == 0
+
+ # re-create root node
+ self.client.ensure_path(self.path)
+ event = self.wait_cache(TreeEvent.NODE_ADDED)
+ assert event is not None
+ assert event.event_type == TreeEvent.NODE_ADDED
+ assert event.event_data.data == b""
+ assert event.event_data.path == self.path
+ assert event.event_data.stat.version == 0
+
+ assert cache._outstanding_ops >= 0, (
+ "unexpected outstanding ops %r" % cache._outstanding_ops
+ )
def test_exception_handler(self) -> None:
error_value = FakeException()
@@ -468,30 +482,30 @@ def test_exception_handler(self) -> None:
with patch.object(TreeNode, "on_deleted") as on_deleted:
on_deleted.side_effect = [error_value]
- self.make_cache()
- self.cache.listen_fault(error_handler)
+ with self.make_cache() as cache:
+ cache.listen_fault(error_handler)
- self.cache.close()
- error_handler.assert_called_once_with(error_value)
+ cache.close()
+ error_handler.assert_called_once_with(error_value)
def test_exception_suppressed(self) -> None:
- self.make_cache()
- self.wait_cache(since=TreeEvent.INITIALIZED)
+ with self.make_cache() as cache:
+ self.wait_cache(since=TreeEvent.INITIALIZED)
- # stoke up ConnectionClosedError
- self.client.stop()
- self.client.close()
- self.client.handler.start() # keep the async completion
- self.wait_cache(since=TreeEvent.CONNECTION_LOST)
+ # stoke up ConnectionClosedError
+ self.client.stop()
+ self.client.close()
+ self.client.handler.start() # keep the async completion
+ self.wait_cache(since=TreeEvent.CONNECTION_LOST)
- with patch.object(TreeNode, "on_created") as on_created:
- self.cache._root._call_client("exists", "/")
- self.cache._root._call_client("get", "/")
- self.cache._root._call_client("get_children", "/")
+ with patch.object(TreeNode, "on_created") as on_created:
+ cache._root._call_client("exists", "/")
+ cache._root._call_client("get", "/")
+ cache._root._call_client("get_children", "/")
- self.wait_cache(since=TreeEvent.INITIALIZED)
- on_created.assert_not_called()
- assert self.cache._outstanding_ops == 0
+ self.wait_cache(since=TreeEvent.INITIALIZED)
+ on_created.assert_not_called()
+ assert cache._outstanding_ops == 0
class FakeException(Exception):
diff --git a/kazoo/tests/test_client.py b/kazoo/tests/test_client.py
index c0263c1ae..76be9242e 100644
--- a/kazoo/tests/test_client.py
+++ b/kazoo/tests/test_client.py
@@ -1,1456 +1,1483 @@
-from __future__ import annotations
-
-import os
-import socket
-import tempfile
-import threading
-import time
-import uuid
-import unittest
-
-from typing import Any, TYPE_CHECKING
-from unittest.mock import Mock, MagicMock, patch
-
-import pytest
-
-
-from kazoo.client import KazooClient
-from kazoo.exceptions import (
- AuthFailedError,
- BadArgumentsError,
- BadVersionError,
- ConfigurationError,
- ConnectionClosedError,
- ConnectionLoss,
- InvalidACLError,
- NoAuthError,
- NoChildrenForEphemeralsError,
- NoNodeError,
- NodeExistsError,
- RolledBackError,
- SessionExpiredError,
- KazooException,
-)
-from kazoo.protocol.states import KazooState, KeeperState, WatchedEvent
-from kazoo.handlers.threading import (
- SequentialThreadingHandler,
- KazooTimeoutError,
-)
-from kazoo.protocol.connection import _CONNECTION_DROP
-from kazoo.retry import KazooRetry
-from kazoo.security import (
- make_digest_acl_credential,
- CREATOR_ALL_ACL,
- make_digest_acl,
- ACL,
- OPEN_ACL_UNSAFE,
-)
-
-from kazoo.testing import KazooTestCase
-
-if TYPE_CHECKING:
- from kazoo.testing.common import ManagedZooKeeper
- from kazoo.interfaces import IAsyncResult
-
-
-class TestClientTransitions(KazooTestCase):
- @staticmethod
- def make_event() -> threading.Event:
- return threading.Event()
-
- def test_connection_and_disconnection(self) -> None:
- states = []
- rc = threading.Event()
-
- @self.client.add_listener
- def listener(state: KazooState) -> None:
- states.append(state)
- if state == KazooState.CONNECTED:
- rc.set()
-
- self.client.stop()
- assert states == [KazooState.LOST]
- states.pop()
-
- self.client.start()
- rc.wait(2)
- assert states == [KazooState.CONNECTED]
- rc.clear()
- states.pop()
- self.expire_session(self.make_event)
- rc.wait(2)
-
- req_states = [KazooState.LOST, KazooState.CONNECTED]
- assert states == req_states
-
-
-class TestClientConstructor(unittest.TestCase):
- def _makeOne(self, *args: Any, **kw: Any) -> KazooClient:
- return KazooClient(*args, **kw)
-
- def test_invalid_handler(self) -> None:
- with pytest.raises(ConfigurationError):
- self._makeOne(handler=SequentialThreadingHandler)
-
- def test_chroot(self) -> None:
- assert self._makeOne(hosts="127.0.0.1:2181/").chroot == ""
- assert self._makeOne(hosts="127.0.0.1:2181/a").chroot == "/a"
- assert self._makeOne(hosts="127.0.0.1/a").chroot == "/a"
- assert self._makeOne(hosts="127.0.0.1/a/b").chroot == "/a/b"
- assert (
- self._makeOne(hosts="127.0.0.1:2181,127.0.0.1:2182/a/b").chroot
- == "/a/b"
- )
-
- def test_connection_timeout(self) -> None:
- client = self._makeOne(hosts="127.0.0.1:9")
- assert client.handler.timeout_exception is KazooTimeoutError
-
- with pytest.raises(KazooTimeoutError):
- client.start(0.1)
-
- def test_ordered_host_selection(self) -> None:
- client = self._makeOne(
- hosts="127.0.0.1:9,127.0.0.2:9/a", randomize_hosts=False
- )
- hosts = [h for h in client.hosts]
- assert hosts == [("127.0.0.1", 9), ("127.0.0.2", 9)]
-
- def test_invalid_hostname(self) -> None:
- client = self._makeOne(hosts="nosuchhost/a")
- timeout = client.handler.timeout_exception
- with pytest.raises(timeout):
- client.start(0.1)
-
- def test_another_invalid_hostname(self) -> None:
- with pytest.raises(ValueError):
- self._makeOne(hosts="/nosuchhost/a")
-
- def test_retry_options_dict(self) -> None:
- client = self._makeOne(
- command_retry=dict(max_tries=99), connection_retry=dict(delay=99)
- )
- assert type(client._conn_retry) is KazooRetry
- assert type(client._retry) is KazooRetry
- assert client._retry.max_tries == 99
- assert client._conn_retry.delay == 99
-
-
-class TestAuthentication(KazooTestCase):
- def _makeAuth(self, *args: Any, **kwargs: Any) -> ACL:
- return make_digest_acl(*args, **kwargs)
-
- def test_auth(self) -> None:
- username = uuid.uuid4().hex
- password = uuid.uuid4().hex
-
- digest_auth = "%s:%s" % (username, password)
- acl = self._makeAuth(username, password, all=True)
-
- client = self._get_client()
- client.start()
- client.add_auth("digest", digest_auth)
- client.default_acl = (acl,)
-
- try:
- client.create("/1")
- client.create("/1/2")
- client.ensure_path("/1/2/3")
-
- eve = self._get_client()
-
- eve.start()
-
- with pytest.raises(NoAuthError):
- eve.get("/1/2")
-
- # try again with the wrong auth token
- eve.add_auth("digest", "badbad:bad")
-
- with pytest.raises(NoAuthError):
- eve.get("/1/2")
-
- finally:
- # Ensure we remove the ACL protected nodes
- client.delete("/1", recursive=True)
- eve.stop()
- eve.close()
-
- def test_connect_auth(self) -> None:
-
- username = uuid.uuid4().hex
- password = uuid.uuid4().hex
-
- digest_auth = "%s:%s" % (username, password)
- acl = self._makeAuth(username, password, all=True)
-
- client = self._get_client(auth_data=[("digest", digest_auth)])
- client.start()
- try:
- client.create("/1", acl=(acl,))
- # Give ZK a chance to copy data to other ensemble nodes.
- # Follower reads are sequentially consistent, but may briefly lag
- # the leader until the commit is applied locally; sync flushes
- # the channel between client's connected server and the leader.
- self.client.sync("/1")
-
- with pytest.raises(NoAuthError):
- self.client.get("/1")
-
- finally:
- client.delete("/1")
- client.stop()
- client.close()
-
- def test_unicode_auth(self) -> None:
- username = r"xe4/\hm"
- password = r"/\xe4hm"
- digest_auth = "%s:%s" % (username, password)
- acl = self._makeAuth(username, password, all=True)
-
- client = self._get_client()
- client.start()
- client.add_auth("digest", digest_auth)
- client.default_acl = (acl,)
-
- try:
- client.create("/1")
- client.ensure_path("/1/2/3")
-
- eve = self._get_client()
- eve.start()
-
- with pytest.raises(NoAuthError):
- eve.get("/1/2")
-
- # try again with the wrong auth token
- eve.add_auth("digest", "badbad:bad")
-
- with pytest.raises(NoAuthError):
- eve.get("/1/2")
-
- finally:
- # Ensure we remove the ACL protected nodes
- client.delete("/1", recursive=True)
- eve.stop()
- eve.close()
-
- def test_invalid_auth(self) -> None:
- client = self._get_client()
- client.start()
-
- with pytest.raises(TypeError):
- client.add_auth(
- "digest",
- ("user", "pass"), # type: ignore[arg-type]
- )
-
- with pytest.raises(TypeError):
- client.add_auth(None, ("user", "pass")) # type: ignore[arg-type]
-
- def test_async_auth(self) -> None:
- client = self._get_client()
- client.start()
- username = uuid.uuid4().hex
- password = uuid.uuid4().hex
- digest_auth = "%s:%s" % (username, password)
- result = client.add_auth_async("digest", digest_auth)
- assert result.get() is True
-
- def test_async_auth_failure(self) -> None:
- client = self._get_client()
- client.start()
- username = uuid.uuid4().hex
- password = uuid.uuid4().hex
- digest_auth = "%s:%s" % (username, password)
-
- with pytest.raises(AuthFailedError):
- client.add_auth("unknown-scheme", digest_auth)
-
- def test_add_auth_on_reconnect(self) -> None:
- client = self._get_client()
- client.start()
- client.add_auth("digest", "jsmith:jsmith")
-
- ev_lost = client.handler.event_object()
- ev_connected = client.handler.event_object()
-
- def listener(state: KazooState) -> None:
- if state in (KazooState.SUSPENDED, KazooState.LOST):
- ev_lost.set()
- elif state == KazooState.CONNECTED and ev_lost.is_set():
- ev_connected.set()
-
- client.add_listener(listener)
- assert client._connection._socket is not None
- client._connection._socket.shutdown(socket.SHUT_RDWR)
-
- ev_connected.wait(15)
- assert ev_connected.is_set()
- assert ("digest", "jsmith:jsmith") in client.auth_data
-
-
-class TestConnection(KazooTestCase):
- @staticmethod
- def make_event() -> threading.Event:
- return threading.Event()
-
- @staticmethod
- def make_condition() -> threading.Condition:
- return threading.Condition()
-
- def test_chroot_warning(self) -> None:
- k = self._get_nonchroot_client()
- k.chroot = "abba"
- try:
- with patch("warnings.warn") as mock_func:
- k.start()
- assert mock_func.called
- finally:
- k.stop()
-
- def test_session_expire(self) -> None:
-
- cv = self.make_event()
-
- def watch_events(event: KazooState) -> None:
- if event == KazooState.LOST:
- cv.set()
-
- self.client.add_listener(watch_events)
- self.expire_session(self.make_event)
- cv.wait(3)
- assert cv.is_set()
-
- def test_bad_session_expire(self) -> None:
-
- cv = self.make_event()
- ab = self.make_event()
-
- def watch_events(event: KazooState) -> None:
- if event == KazooState.LOST:
- ab.set()
- raise Exception("oops")
-
- self.client.add_listener(watch_events)
- self.expire_session(self.make_event)
- ab.wait(5.0)
- assert ab.is_set()
- cv.wait(0.5)
- assert not cv.is_set()
-
- def test_state_listener(self) -> None:
-
- states = []
- condition = self.make_condition()
-
- def listener(state: KazooState) -> None:
- with condition:
- states.append(state)
- condition.notify_all()
-
- self.client.stop()
- assert self.client.state == KazooState.LOST
- self.client.add_listener(listener)
- self.client.start(5)
-
- with condition:
- if not states:
- condition.wait(5)
-
- assert len(states) == 1
- assert states[0] == KazooState.CONNECTED
-
- def test_invalid_listener(self) -> None:
- with pytest.raises(ConfigurationError):
- self.client.add_listener(15) # type: ignore[arg-type]
-
- def test_listener_only_called_on_real_state_change(self) -> None:
-
- assert self.client.state == KazooState.CONNECTED
- called = [False]
- condition = self.make_event()
-
- def listener(state: KazooState) -> None:
- called[0] = True
- condition.set()
-
- self.client.add_listener(listener)
- self.client._make_state_change(KazooState.CONNECTED)
- condition.wait(3)
- assert called[0] is False
-
- def test_no_connection(self) -> None:
- client = self.client
- client.stop()
- assert client.connected is False
- assert client.client_id is None
-
- with pytest.raises(ConnectionClosedError):
- client.exists("/")
-
- def test_close_connecting_connection(self) -> None:
- client = self.client
- client.stop()
- ev = self.make_event()
-
- def close_on_connecting(state: KazooState) -> None:
- if state in (KazooState.CONNECTED, KazooState.LOST):
- ev.set()
-
- client.add_listener(close_on_connecting)
- client.start()
-
- # Wait until we connect
- ev.wait(5)
- ev.clear()
- self.client._call(_CONNECTION_DROP, client.handler.async_result())
-
- client.stop()
-
- # ...and then wait until the connection is lost
- ev.wait(5)
-
- with pytest.raises(ConnectionClosedError):
- self.client.create("/foobar")
-
- def test_double_start(self) -> None:
- assert self.client.connected is True
- self.client.start()
- assert self.client.connected is True
-
- def test_double_stop(self) -> None:
- self.client.stop()
- assert self.client.connected is False
- self.client.stop()
- assert self.client.connected is False
-
- def test_restart(self) -> None:
- assert self.client.connected is True
- self.client.restart()
- assert self.client.connected is True
-
- def test_closed(self) -> None:
- client = self.client
- client.stop()
-
- write_sock = client._connection._write_sock
-
- # close the connection to free the socket
- client.close()
- assert client._connection._write_sock is None
-
- # sneak in and patch client to simulate race between a thread
- # calling stop(); close() and one running a command
- oldstate = client._state
- client._state = KeeperState.CONNECTED
- client._connection._write_sock = write_sock
-
- try:
- # simulate call made after write socket is closed
- with pytest.raises(ConnectionClosedError):
- client.exists("/")
-
- # simulate call made after write socket is set to None
- client._connection._write_sock = None
-
- with pytest.raises(ConnectionClosedError):
- client.exists("/")
-
- finally:
- # reset for teardown
- client._state = oldstate
- client._connection._write_sock = None
-
- def test_watch_trigger_expire(self) -> None:
- client = self.client
- cv = self.make_event()
-
- client.create("/test", b"")
-
- def test_watch(event: WatchedEvent) -> None:
- cv.set()
-
- client.get("/test/", watch=test_watch)
- self.expire_session(self.make_event)
-
- cv.wait(3)
- assert cv.is_set()
-
-
-class TestClient(KazooTestCase):
- def _makeOne(self, *args: Any) -> SequentialThreadingHandler:
- return SequentialThreadingHandler(*args)
-
- def test_server_version_retries_fail(self) -> None:
-
- client = self.client
- side_effects = [
- "",
- "zookeeper.version=",
- "zookeeper.version=1.",
- "zookeeper.ver",
- ]
- client.command = MagicMock() # type: ignore[method-assign]
- client.command.side_effect = side_effects
- with pytest.raises(KazooException):
- client.server_version(retries=len(side_effects) - 1)
-
- def test_server_version_retries_eventually_ok(self) -> None:
- client = self.client
- actual_version = "zookeeper.version=1.2"
- side_effects = []
- for i in range(0, len(actual_version) + 1):
- side_effects.append(actual_version[0:i])
- client.command = MagicMock() # type: ignore[method-assign]
- client.command.side_effect = side_effects
- assert client.server_version(retries=len(side_effects) - 1) == (1, 2)
-
- def test_client_id(self) -> None:
- client_id = self.client.client_id
- assert type(client_id) is tuple
- # make sure password is of correct length
- assert len(client_id[1]) == 16
-
- def test_connected(self) -> None:
- client = self.client
- assert client.connected
-
- def test_create(self) -> None:
- client = self.client
- path = client.create("/1")
- assert path == "/1"
- assert client.exists("/1")
-
- def test_create_on_broken_connection(self) -> None:
- client = self.client
- client.start()
-
- client._state = KeeperState.EXPIRED_SESSION
- with pytest.raises(SessionExpiredError):
- client.create("/closedpath", b"bar")
-
- client._state = KeeperState.AUTH_FAILED
- with pytest.raises(AuthFailedError):
- client.create("/closedpath", b"bar")
-
- client.stop()
- client.close()
-
- with pytest.raises(ConnectionClosedError):
- client.create("/closedpath", b"bar")
-
- def test_create_null_data(self) -> None:
- client = self.client
- client.create("/nulldata", None)
- value, _ = client.get("/nulldata")
- assert value is None
-
- def test_create_empty_string(self) -> None:
- client = self.client
- client.create("/empty", b"")
- value, _ = client.get("/empty")
- assert value == b""
-
- def test_create_unicode_path(self) -> None:
- client = self.client
- path = client.create("/ascii")
- assert path == "/ascii"
- path = client.create("/\xe4hm")
- assert path == "/\xe4hm"
-
- def test_create_async_returns_unchrooted_path(self) -> None:
- client = self.client
- path = client.create_async("/1").get()
- assert path == "/1"
-
- def test_create_invalid_path(self) -> None:
- client = self.client
- with pytest.raises(TypeError):
- client.create(("a",)) # type: ignore[call-overload]
- with pytest.raises(ValueError):
- client.create(".")
- with pytest.raises(ValueError):
- client.create("/a/../b")
- with pytest.raises(BadArgumentsError):
- client.create("/b\x00")
- with pytest.raises(BadArgumentsError):
- client.create("/b\x1e")
-
- def test_create_invalid_arguments(self) -> None:
- single_acl = OPEN_ACL_UNSAFE[0]
- client = self.client
- with pytest.raises(TypeError):
- client.create("a", acl="all") # type: ignore[arg-type]
- with pytest.raises(TypeError):
- client.create("a", acl=single_acl) # type: ignore[arg-type]
- with pytest.raises(TypeError):
- client.create("a", value=["a"]) # type: ignore[call-overload]
- with pytest.raises(TypeError):
- client.create("a", ephemeral="yes") # type: ignore[call-overload]
- with pytest.raises(TypeError):
- client.create("a", sequence="yes") # type: ignore[call-overload]
- with pytest.raises(TypeError):
- client.create("a", makepath="yes") # type: ignore[call-overload]
-
- def test_create_value(self) -> None:
- client = self.client
- client.create("/1", b"bytes")
- data, stat = client.get("/1")
- assert data == b"bytes"
-
- def test_create_unicode_value(self) -> None:
- client = self.client
- with pytest.raises(TypeError):
- client.create("/1", "\xe4hm") # type: ignore[call-overload]
-
- def test_create_large_value(self) -> None:
- client = self.client
- kb_512 = b"a" * (512 * 1024)
- client.create("/1", kb_512)
- assert client.exists("/1")
- mb_2 = b"a" * (2 * 1024 * 1024)
- with pytest.raises(ConnectionLoss):
- client.create("/2", mb_2)
-
- def test_create_acl_duplicate(self) -> None:
- single_acl = OPEN_ACL_UNSAFE[0]
- client = self.client
- client.create("/1", acl=[single_acl, single_acl])
- acls, stat = client.get_acls("/1")
- assert len(acls) == 1
-
- def test_create_acl_empty_list(self) -> None:
- client = self.client
- client.create("/1", acl=[])
- acls, stat = client.get_acls("/1")
- assert acls == OPEN_ACL_UNSAFE
-
- def test_version_no_connection(self) -> None:
- self.client.stop()
- with pytest.raises(ConnectionLoss):
- self.client.server_version()
-
- def test_create_ephemeral(self) -> None:
- client = self.client
- client.create("/1", b"ephemeral", ephemeral=True)
- assert client.client_id is not None
- data, stat = client.get("/1")
- assert data == b"ephemeral"
- assert stat.ephemeralOwner == client.client_id[0]
-
- def test_create_no_ephemeral(self) -> None:
- client = self.client
- client.create("/1", b"val1")
- data, stat = client.get("/1")
- assert not stat.ephemeralOwner
-
- def test_create_ephemeral_no_children(self) -> None:
- client = self.client
- client.create("/1", b"ephemeral", ephemeral=True)
- with pytest.raises(NoChildrenForEphemeralsError):
- client.create("/1/2", b"val1")
- with pytest.raises(NoChildrenForEphemeralsError):
- client.create("/1/2", b"val1", ephemeral=True)
-
- def test_create_sequence(self) -> None:
- client = self.client
- client.create("/folder")
- path = client.create("/folder/a", b"sequence", sequence=True)
- assert path == "/folder/a0000000000"
- path2 = client.create("/folder/a", b"sequence", sequence=True)
- assert path2 == "/folder/a0000000001"
- path3 = client.create("/folder/", b"sequence", sequence=True)
- assert path3 == "/folder/0000000002"
-
- def test_create_ephemeral_sequence(self) -> None:
- basepath = "/" + uuid.uuid4().hex
- realpath = self.client.create(
- basepath, b"sandwich", sequence=True, ephemeral=True
- )
- assert basepath != realpath and realpath.startswith(basepath)
- data, stat = self.client.get(realpath)
- assert data == b"sandwich"
-
- def test_create_makepath(self) -> None:
- self.client.create("/1/2", b"val1", makepath=True)
- data, stat = self.client.get("/1/2")
- assert data == b"val1"
-
- self.client.create("/1/2/3/4/5", b"val2", makepath=True)
- data, stat = self.client.get("/1/2/3/4/5")
- assert data == b"val2"
-
- with pytest.raises(NodeExistsError):
- self.client.create("/1/2/3/4/5", b"val2", makepath=True)
-
- def test_create_makepath_incompatible_acls(self) -> None:
- credential = make_digest_acl_credential("username", "password")
- alt_client = KazooClient(
- self.cluster[0].address + self.client.chroot,
- max_retries=5,
- auth_data=[("digest", credential)],
- handler=self._makeOne(),
- )
- alt_client.start()
- alt_client.create("/1/2", b"val2", makepath=True, acl=CREATOR_ALL_ACL)
-
- try:
- with pytest.raises(NoAuthError):
- self.client.create("/1/2/3/4/5", b"val2", makepath=True)
-
- finally:
- alt_client.delete("/", recursive=True)
- alt_client.stop()
-
- def test_create_no_makepath(self) -> None:
- with pytest.raises(NoNodeError):
- self.client.create("/1/2", b"val1")
- with pytest.raises(NoNodeError):
- self.client.create("/1/2", b"val1", makepath=False)
-
- self.client.create("/1/2", b"val1", makepath=True)
- with pytest.raises(NoNodeError):
- self.client.create("/1/2/3/4", b"val1", makepath=False)
-
- def test_create_exists(self) -> None:
- client = self.client
- path = client.create("/1")
- with pytest.raises(NodeExistsError):
- client.create(path)
-
- def test_create_stat(self) -> None:
- client = self.client
- path, stat1 = client.create("/1", b"bytes", include_data=True)
- data, stat2 = client.get("/1")
- assert data == b"bytes"
- assert stat1 == stat2
-
- def test_create_get_set(self) -> None:
- nodepath = "/" + uuid.uuid4().hex
-
- self.client.create(nodepath, b"sandwich", ephemeral=True)
-
- data, stat = self.client.get(nodepath)
- assert data == b"sandwich"
-
- newstat = self.client.set(nodepath, b"hats", stat.version)
- assert newstat
- assert newstat.version > stat.version
-
- # Some other checks of the ZnodeStat object we got
- assert newstat.acl_version == stat.acl_version
- assert newstat.created == stat.ctime / 1000.0
- assert newstat.last_modified == newstat.mtime / 1000.0
- assert newstat.owner_session_id == stat.ephemeralOwner
- assert newstat.creation_transaction_id == stat.czxid
- assert newstat.last_modified_transaction_id == newstat.mzxid
- assert newstat.data_length == newstat.dataLength
- assert newstat.children_count == stat.numChildren
- assert newstat.children_version == stat.cversion
-
- def test_get_invalid_arguments(self) -> None:
- client = self.client
- with pytest.raises(TypeError):
- client.get(("a", "b")) # type: ignore[arg-type]
- with pytest.raises(TypeError):
- client.get("a", watch=True) # type: ignore[arg-type]
-
- def test_bad_argument(self) -> None:
- client = self.client
- client.ensure_path("/1")
- with pytest.raises(TypeError):
- self.client.set("/1", 1) # type: ignore[arg-type]
-
- def test_ensure_path(self) -> None:
- client = self.client
- client.ensure_path("/1/2")
- assert client.exists("/1/2")
-
- client.ensure_path("/1/2/3/4")
- assert client.exists("/1/2/3/4")
-
- def test_sync(self) -> None:
- client = self.client
- assert client.sync("/") == "/"
- # Albeit surprising, you can sync anything, even what does not exist.
- assert client.sync("/not_there") == "/not_there"
-
- def test_exists(self) -> None:
- nodepath = "/" + uuid.uuid4().hex
-
- exists = self.client.exists(nodepath)
- assert exists is None
-
- self.client.create(nodepath, b"sandwich", ephemeral=True)
- exists = self.client.exists(nodepath)
- assert exists
- assert isinstance(exists.version, int)
-
- multi_node_nonexistent = "/" + uuid.uuid4().hex + "/hats"
- exists = self.client.exists(multi_node_nonexistent)
- assert exists is None
-
- def test_exists_invalid_arguments(self) -> None:
- client = self.client
- with pytest.raises(TypeError):
- client.exists(("a", "b")) # type: ignore[arg-type]
- with pytest.raises(TypeError):
- client.exists("a", watch=True) # type: ignore[arg-type]
-
- def test_exists_watch(self) -> None:
- nodepath = "/" + uuid.uuid4().hex
- event = self.client.handler.event_object()
-
- def w(watch_event: WatchedEvent) -> None:
- assert watch_event.path == nodepath
- event.set()
-
- exists = self.client.exists(nodepath, watch=w)
- assert exists is None
-
- self.client.create(nodepath, ephemeral=True)
-
- event.wait(1)
- assert event.is_set() is True
-
- def test_exists_watcher_exception(self) -> None:
- nodepath = "/" + uuid.uuid4().hex
- event = self.client.handler.event_object()
-
- # if the watcher throws an exception, all we can really do is log it
- def w(watch_event: WatchedEvent) -> None:
- assert watch_event.path == nodepath
- event.set()
-
- raise Exception("test exception in callback")
-
- exists = self.client.exists(nodepath, watch=w)
- assert exists is None
-
- self.client.create(nodepath, ephemeral=True)
-
- event.wait(1)
- assert event.is_set() is True
-
- def test_create_delete(self) -> None:
- nodepath = "/" + uuid.uuid4().hex
-
- self.client.create(nodepath, b"zzz")
-
- self.client.delete(nodepath)
-
- exists = self.client.exists(nodepath)
- assert exists is None
-
- def test_get_acls(self) -> None:
- user = "user"
- passw = "pass"
- acl = make_digest_acl(user, passw, all=True)
- client = self.client
- try:
- client.create("/a", acl=[acl])
- client.add_auth("digest", "{}:{}".format(user, passw))
- assert acl in client.get_acls("/a")[0]
- finally:
- client.delete("/a")
-
- def test_get_acls_invalid_arguments(self) -> None:
- client = self.client
- with pytest.raises(TypeError):
- client.get_acls(("a", "b")) # type: ignore[arg-type]
-
- def test_set_acls(self) -> None:
- user = "user"
- passw = "pass"
- acl = make_digest_acl(user, passw, all=True)
- client = self.client
- client.create("/a")
- try:
- client.set_acls("/a", [acl])
- client.add_auth("digest", "{}:{}".format(user, passw))
- assert acl in client.get_acls("/a")[0]
- finally:
- client.delete("/a")
-
- def test_set_acls_empty(self) -> None:
- client = self.client
- client.create("/a")
- with pytest.raises(InvalidACLError):
- client.set_acls("/a", [])
-
- def test_set_acls_no_node(self) -> None:
- client = self.client
- with pytest.raises(NoNodeError):
- client.set_acls("/a", OPEN_ACL_UNSAFE)
-
- def test_set_acls_invalid_arguments(self) -> None:
- single_acl = OPEN_ACL_UNSAFE[0]
- client = self.client
- with pytest.raises(TypeError):
- client.set_acls(("a", "b"), ()) # type: ignore[arg-type]
- with pytest.raises(TypeError):
- client.set_acls("a", single_acl) # type: ignore[arg-type]
- with pytest.raises(TypeError):
- client.set_acls("a", "all") # type: ignore[arg-type]
- with pytest.raises(TypeError):
- client.set_acls("a", [single_acl], "V1") # type: ignore[arg-type]
-
- def test_set(self) -> None:
- client = self.client
- client.create("a", b"first")
- stat = client.set("a", b"second")
- data, stat2 = client.get("a")
- assert data == b"second"
- assert stat == stat2
-
- def test_set_null_data(self) -> None:
- client = self.client
- client.create("/nulldata", b"not none")
- client.set("/nulldata", None)
- value, _ = client.get("/nulldata")
- assert value is None
-
- def test_set_empty_string(self) -> None:
- client = self.client
- client.create("/empty", b"not empty")
- client.set("/empty", b"")
- value, _ = client.get("/empty")
- assert value == b""
-
- def test_set_invalid_arguments(self) -> None:
- client = self.client
- client.create("a", b"first")
- with pytest.raises(TypeError):
- client.set(("a", "b"), b"value") # type: ignore[arg-type]
- with pytest.raises(TypeError):
- client.set("a", ["v", "w"]) # type: ignore[arg-type]
- with pytest.raises(TypeError):
- client.set("a", b"value", "V1") # type: ignore[arg-type]
-
- def test_delete(self) -> None:
- client = self.client
- client.ensure_path("/a/b")
- assert "b" in client.get_children("a")
- client.delete("/a/b")
- assert "b" not in client.get_children("a")
-
- def test_delete_recursive(self) -> None:
- client = self.client
- client.ensure_path("/a/b/c")
- client.ensure_path("/a/b/d")
- client.delete("/a/b", recursive=True)
- client.delete("/a/b/c", recursive=True)
- assert "b" not in client.get_children("a")
-
- def test_delete_invalid_arguments(self) -> None:
- client = self.client
- client.ensure_path("/a/b")
- with pytest.raises(TypeError):
- client.delete("/a/b", recursive="all") # type: ignore[arg-type]
- with pytest.raises(TypeError):
- client.delete(("a", "b")) # type: ignore[arg-type]
- with pytest.raises(TypeError):
- client.delete("/a/b", version="V1") # type: ignore[arg-type]
-
- def test_get_children(self) -> None:
- client = self.client
- client.ensure_path("/a/b/c")
- client.ensure_path("/a/b/d")
- assert client.get_children("/a") == ["b"]
- assert set(client.get_children("/a/b")) == set(["c", "d"])
- assert client.get_children("/a/b/c") == []
-
- def test_get_children2(self) -> None:
- client = self.client
- client.ensure_path("/a/b")
- children, stat = client.get_children("/a", include_data=True)
- value, stat2 = client.get("/a")
- assert children == ["b"]
- assert stat2.version == stat.version
-
- def test_get_children2_many_nodes(self) -> None:
- client = self.client
- client.ensure_path("/a/b")
- client.ensure_path("/a/c")
- client.ensure_path("/a/d")
- children, stat = client.get_children("/a", include_data=True)
- value, stat2 = client.get("/a")
- assert set(children) == set(["b", "c", "d"])
- assert stat2.version == stat.version
-
- def test_get_children_no_node(self) -> None:
- client = self.client
- with pytest.raises(NoNodeError):
- client.get_children("/none")
- with pytest.raises(NoNodeError):
- client.get_children("/none", include_data=True)
-
- def test_get_children_invalid_path(self) -> None:
- client = self.client
- with pytest.raises(ValueError):
- client.get_children("../a")
-
- def test_get_children_invalid_arguments(self) -> None:
- client = self.client
- with pytest.raises(TypeError):
- client.get_children(("a", "b")) # type: ignore[call-overload]
- with pytest.raises(TypeError):
- client.get_children("a", watch=True) # type: ignore[call-overload]
- with pytest.raises(TypeError):
- client.get_children( # type: ignore[call-overload]
- "a", include_data="yes"
- )
-
- def test_invalid_auth(self) -> None:
- client = self.client
- client.stop()
- client._state = KeeperState.AUTH_FAILED
-
- with pytest.raises(AuthFailedError):
- client.get("/")
-
- def test_client_state(self) -> None:
- assert self.client.client_state == KeeperState.CONNECTED
-
- def test_update_host_list(self) -> None:
- hosts = self.cluster[0].address
- # create a client with only one server in its list
- client = KazooClient(hosts=hosts)
- client.start()
-
- # try to change the chroot, not currently allowed
- with pytest.raises(ConfigurationError):
- client.set_hosts(hosts + "/new_chroot")
-
- # grow the cluster to 3
- client.set_hosts(self.servers)
-
- # shut down the first host
- try:
- ev_connected = client.handler.event_object()
-
- def listener(state: KazooState) -> None:
- if state == KazooState.CONNECTED:
- ev_connected.set()
-
- client.add_listener(listener)
-
- self.cluster[0].stop()
- ev_connected.wait(60)
- assert ev_connected.is_set(), (
- f"Failover timed out after 60s: ev_connected not set. "
- f"client.state={client.state}, "
- f"client.client_state={client.client_state}, "
- f"client.connected={client.connected}, "
- f"hosts={client.hosts}"
- )
- assert client.client_state == KeeperState.CONNECTED
- finally:
- self.cluster[0].run()
-
- # utility for test_request_queuing*
- def _make_request_queuing_client(
- self,
- ) -> tuple[KazooClient, ManagedZooKeeper]:
-
- server = self.cluster[0]
- handler = self._makeOne()
- # create a client with only one server in its list, and
- # infinite retries
- client = KazooClient(
- hosts=server.address + self.client.chroot,
- handler=handler,
- connection_retry=dict(
- max_tries=-1,
- delay=0.1,
- backoff=1,
- max_jitter=0.0,
- sleep_func=handler.sleep_func,
- ),
- )
-
- return client, server
-
- # utility for test_request_queuing*
- def _request_queuing_common(
- self,
- client: KazooClient,
- server: ManagedZooKeeper,
- path: str,
- expire_session: bool,
- ) -> IAsyncResult:
- ev_suspended = client.handler.event_object()
- ev_connected = client.handler.event_object()
-
- def listener(state: KazooState) -> None:
- if state == KazooState.SUSPENDED:
- ev_suspended.set()
- elif state == KazooState.CONNECTED:
- ev_connected.set()
-
- client.add_listener(listener)
-
- # wait for the client to connect
- client.start()
-
- try:
- # force the client to suspend
- server.stop()
-
- ev_suspended.wait(5)
- assert ev_suspended.is_set()
- ev_connected.clear()
-
- # submit a request, expecting it to be queued
- result = client.create_async(path)
- assert len(client._queue) != 0
- assert result.ready() is False
- assert client.state == KazooState.SUSPENDED
-
- # optionally cause a SessionExpiredError to occur by
- # mangling the first byte of the session password.
- if expire_session:
- b0 = b"\x00"
- if client._session_passwd[0] == 0:
- b0 = b"\xff"
- client._session_passwd = b0 + client._session_passwd[1:]
- finally:
- server.run()
-
- # wait for the client to reconnect (either with a recovered
- # session, or with a new one if expire_session was set)
- ev_connected.wait(5)
- assert ev_connected.is_set()
-
- return result
-
- def test_request_queuing_session_recovered(self) -> None:
- path = "/" + uuid.uuid4().hex
- client, server = self._make_request_queuing_client()
-
- try:
- result = self._request_queuing_common(
- client=client, server=server, path=path, expire_session=False
- )
-
- assert result.get() == path
- assert client.exists(path) is not None
- finally:
- client.stop()
-
- def test_request_queuing_session_expired(self) -> None:
- path = "/" + uuid.uuid4().hex
- client, server = self._make_request_queuing_client()
-
- try:
- result = self._request_queuing_common(
- client=client, server=server, path=path, expire_session=True
- )
-
- assert len(client._queue) == 0
- with pytest.raises(SessionExpiredError):
- result.get()
- finally:
- client.stop()
-
-
-class TestSSLClient(KazooTestCase):
- def setUp(self) -> None:
- ssl_path = tempfile.mkdtemp()
- key_path = os.path.join(ssl_path, "key.pem")
- cert_path = os.path.join(ssl_path, "cert.pem")
- cacert_path = os.path.join(ssl_path, "cacert.pem")
- with open(key_path, "wb") as key_file:
- key_file.write(
- self.cluster.get_ssl_client_configuration()["client_key"]
- )
- with open(cert_path, "wb") as cert_file:
- cert_file.write(
- self.cluster.get_ssl_client_configuration()["client_cert"]
- )
- with open(cacert_path, "wb") as cacert_file:
- cacert_file.write(
- self.cluster.get_ssl_client_configuration()["ca_cert"]
- )
- self.setup_zookeeper(
- use_ssl=True, keyfile=key_path, certfile=cert_path, ca=cacert_path
- )
-
- def test_create(self) -> None:
- client = self.client
- path = client.create("/1")
- assert path == "/1"
- assert client.exists("/1")
-
-
-dummy_dict = {
- "aversion": 1,
- "ctime": 0,
- "cversion": 1,
- "czxid": 110,
- "dataLength": 1,
- "ephemeralOwner": "ben",
- "mtime": 1,
- "mzxid": 1,
- "numChildren": 0,
- "pzxid": 1,
- "version": 1,
-}
-
-
-class TestClientTransactions(KazooTestCase):
- def setUp(self) -> None:
- KazooTestCase.setUp(self)
-
- def test_basic_create(self) -> None:
- t = self.client.transaction()
- t.create("/freddy")
- t.create("/fred", ephemeral=True)
- t.create("/smith", sequence=True)
- results = t.commit()
- assert len(results) == 3
- assert results[0] == "/freddy"
- assert results[2].startswith("/smith0") is True
-
- def test_bad_creates(self) -> None:
- args_list = [
- (True,),
- ("/smith", 0),
- ("/smith", b"", "bleh"),
- ("/smith", b"", None, "fred"),
- ("/smith", b"", None, True, "fred"),
- ]
-
- for args in args_list:
- with pytest.raises(TypeError):
- t = self.client.transaction()
- t.create(*args) # type: ignore[arg-type]
-
- def test_default_acl(self) -> None:
- username = uuid.uuid4().hex
- password = uuid.uuid4().hex
-
- digest_auth = "%s:%s" % (username, password)
- acl = make_digest_acl(username, password, all=True)
-
- self.client.add_auth("digest", digest_auth)
- self.client.default_acl = (acl,)
-
- t = self.client.transaction()
- t.create("/freddy")
- results = t.commit()
- assert results[0] == "/freddy"
-
- def test_basic_delete(self) -> None:
- self.client.create("/fred")
- t = self.client.transaction()
- t.delete("/fred")
- results = t.commit()
- assert results[0] is True
-
- def test_bad_deletes(self) -> None:
- args_list = [
- (True,),
- ("/smith", "woops"),
- ]
-
- for args in args_list:
- with pytest.raises(TypeError):
- t = self.client.transaction()
- t.delete(*args) # type: ignore[arg-type]
-
- def test_set(self) -> None:
- self.client.create("/fred", b"01")
- t = self.client.transaction()
- t.set_data("/fred", b"oops")
- t.commit()
- res = self.client.get("/fred")
- assert res[0] == b"oops"
-
- def test_bad_sets(self) -> None:
- args_list = [(42, 52), ("/smith", False), ("/smith", b"", "oops")]
-
- for args in args_list:
- with pytest.raises(TypeError):
- t = self.client.transaction()
- t.set_data(*args) # type: ignore[arg-type]
-
- def test_check(self) -> None:
- self.client.create("/fred")
- version = self.client.get("/fred")[1].version
- t = self.client.transaction()
- t.check("/fred", version)
- t.create("/blah")
- results = t.commit()
- assert results[0] is True
- assert results[1] == "/blah"
-
- def test_bad_checks(self) -> None:
- args_list = [(42, 52), ("/smith", "oops")]
-
- for args in args_list:
- with pytest.raises(TypeError):
- t = self.client.transaction()
- t.check(*args) # type: ignore[arg-type]
-
- def test_bad_transaction(self) -> None:
- t = self.client.transaction()
- t.create("/fred")
- t.delete("/smith")
- results = t.commit()
- assert results[0].__class__ == RolledBackError
- assert results[1].__class__ == NoNodeError
-
- def test_bad_commit(self) -> None:
- t = self.client.transaction()
- t.committed = True
-
- with pytest.raises(ValueError):
- t.commit()
-
- def test_bad_context(self) -> None:
- with pytest.raises(TypeError):
- with self.client.transaction() as t:
- t.check(4232) # type: ignore[arg-type,call-arg]
-
- def test_context(self) -> None:
- with self.client.transaction() as t:
- t.create("/smith", b"32")
- assert self.client.get("/smith")[0] == b"32"
-
-
-class TestSessionCallbacks(unittest.TestCase):
- def test_session_callback_states(self) -> None:
- client = KazooClient()
- client._live.set()
-
- client._session_callback(KeeperState.CONNECTED)
-
- # Now with stopped
- client._stopped.set()
- client._session_callback(KeeperState.CONNECTED)
-
- # Test several state transitions
- client._stopped.clear()
- client.start_async = ( # type: ignore[method-assign]
- lambda: threading.Event()
- )
- client._session_callback(KeeperState.CONNECTED)
- assert client.state == KazooState.CONNECTED
-
- client._session_callback(KeeperState.AUTH_FAILED)
- # FIXME mypy seems to be under the impression that the state can't
- # change as a result of the above call, even though it can.
- assert (
- client.state == KazooState.LOST # type: ignore[comparison-overlap]
- )
-
- client._session_callback(-250) # type: ignore[unreachable]
- assert client.state == KazooState.SUSPENDED
-
-
-class TestCallbacks(KazooTestCase):
- def test_async_result_callbacks_are_always_called(self) -> None:
- # create a callback object
- callback_mock = Mock()
-
- # simulate waiting for a response
- async_result = self.client.handler.async_result()
- async_result.rawlink(callback_mock)
-
- # begin the procedure to stop the client
- self.client.stop()
-
- # the response has just been received;
- # this should be on another thread,
- # simultaneously with the stop procedure
- async_result.set_exception(
- Exception("Anything that throws an exception")
- )
-
- # with the fix the callback should be called
- assert callback_mock.call_count > 0
-
-
-class TestNonChrootClient(KazooTestCase):
- def test_create(self) -> None:
- client = self._get_nonchroot_client()
- assert client.chroot == ""
- client.start()
- node = uuid.uuid4().hex
- path = client.create(node, ephemeral=True)
- client.delete(path)
- client.stop()
-
- def test_unchroot(self) -> None:
- client = self._get_nonchroot_client()
- client.chroot = "/a"
- # Unchroot'ing the chroot path should return "/"
- assert client.unchroot("/a") == "/"
- assert client.unchroot("/a/b") == "/b"
- assert client.unchroot("/b/c") == "/b/c"
-
-
-class TestReconfig(KazooTestCase):
- def setUp(self) -> None:
- KazooTestCase.setUp(self)
-
- def test_no_super_auth(self) -> None:
- with pytest.raises(NoAuthError):
- self.client.reconfig(
- joining="server.999=0.0.0.0:1234:2345:observer;3456",
- leaving=None,
- new_members=None,
- )
-
- def test_add_remove_observer(self) -> None:
- def free_sock_port() -> tuple[socket.socket, int]:
- s = socket.socket()
- s.bind(("", 0))
- return s, s.getsockname()[1]
-
- username = "super"
- password = "test"
- digest_auth = "%s:%s" % (username, password)
- client = self._get_client(auth_data=[("digest", digest_auth)])
- client.start()
-
- # get ports for election, zab and client endpoints. we need to use
- # ports for which we'd immediately get a RST upon connect(); otherwise
- # the cluster could crash if it gets a SocketTimeoutException:
- # https://issues.apache.org/jira/browse/ZOOKEEPER-2202
- s1, port1 = free_sock_port()
- s2, port2 = free_sock_port()
- s3, port3 = free_sock_port()
-
- joining = "server.100=0.0.0.0:%d:%d:observer;0.0.0.0:%d" % (
- port1,
- port2,
- port3,
- )
- data, _ = client.reconfig(
- joining=joining, leaving=None, new_members=None
- )
- assert joining.encode("utf8") in data
-
- data, _ = client.reconfig(
- joining=None, leaving="100", new_members=None
- )
- assert joining.encode("utf8") not in data
-
- # try to add it again, but a config number in the future
- curver = int(data.decode().split("\n")[-1].split("=")[1], base=16)
- with pytest.raises(BadVersionError):
- self.client.reconfig(
- joining=joining,
- leaving=None,
- new_members=None,
- from_config=curver + 1,
- )
-
- def test_bad_input(self) -> None:
- with pytest.raises(BadArgumentsError):
- self.client.reconfig(
- joining="some thing", leaving=None, new_members=None
- )
+from __future__ import annotations
+
+import os
+import socket
+import tempfile
+import threading
+import uuid
+import unittest
+
+from typing import Any, TYPE_CHECKING
+from unittest.mock import Mock, MagicMock, patch
+
+import pytest
+
+
+from kazoo.client import KazooClient
+from kazoo.exceptions import (
+ AuthFailedError,
+ BadArgumentsError,
+ BadVersionError,
+ ConfigurationError,
+ ConnectionClosedError,
+ ConnectionLoss,
+ InvalidACLError,
+ NoAuthError,
+ NoChildrenForEphemeralsError,
+ NoNodeError,
+ NodeExistsError,
+ RolledBackError,
+ SessionExpiredError,
+ KazooException,
+)
+from kazoo.protocol.states import KazooState, KeeperState, WatchedEvent
+from kazoo.handlers.threading import (
+ SequentialThreadingHandler,
+ KazooTimeoutError,
+)
+from kazoo.protocol.connection import _CONNECTION_DROP
+from kazoo.retry import KazooRetry
+from kazoo.security import (
+ make_digest_acl_credential,
+ CREATOR_ALL_ACL,
+ make_digest_acl,
+ OPEN_ACL_UNSAFE,
+)
+
+from kazoo.testing import KazooTestCase
+
+if TYPE_CHECKING:
+ from kazoo.testing.common import ManagedZooKeeper
+ from kazoo.interfaces import IAsyncResult
+
+
+class TestClientTransitions(KazooTestCase):
+ @staticmethod
+ def make_event() -> threading.Event:
+ return threading.Event()
+
+ def test_connection_and_disconnection(self) -> None:
+ states = []
+ rc = threading.Event()
+
+ @self.client.add_listener
+ def listener(state: KazooState) -> None:
+ states.append(state)
+ if state == KazooState.CONNECTED:
+ rc.set()
+
+ self.client.stop()
+ assert states == [KazooState.LOST]
+ states.pop()
+
+ self.client.start()
+ rc.wait(2)
+ assert states == [KazooState.CONNECTED]
+ rc.clear()
+ states.pop()
+ self.expire_session(self.make_event)
+ rc.wait(2)
+
+ req_states = [KazooState.LOST, KazooState.CONNECTED]
+ assert states == req_states
+
+
+class TestClientConstructor(unittest.TestCase):
+ _makeOne = KazooClient
+
+ def _old_makeOne(self, *args: Any, **kw: Any) -> KazooClient:
+ # This is a hack to so that test_invalid_handler doesn't generate a
+ # mypy error.
+ return KazooClient(*args, **kw)
+
+ def test_invalid_handler(self) -> None:
+ with pytest.raises(ConfigurationError):
+ self._old_makeOne(handler=SequentialThreadingHandler)
+
+ def test_chroot(self) -> None:
+ assert self._makeOne(hosts="127.0.0.1:2181/").chroot == ""
+ assert self._makeOne(hosts="127.0.0.1:2181/a").chroot == "/a"
+ assert self._makeOne(hosts="127.0.0.1/a").chroot == "/a"
+ assert self._makeOne(hosts="127.0.0.1/a/b").chroot == "/a/b"
+ assert (
+ self._makeOne(hosts="127.0.0.1:2181,127.0.0.1:2182/a/b").chroot
+ == "/a/b"
+ )
+
+ def test_connection_timeout(self) -> None:
+ client = self._makeOne(hosts="127.0.0.1:9")
+ assert client.handler.timeout_exception is KazooTimeoutError
+
+ with pytest.raises(KazooTimeoutError):
+ client.start(0.1)
+
+ def test_ordered_host_selection(self) -> None:
+ client = self._makeOne(
+ hosts="127.0.0.1:9,127.0.0.2:9/a", randomize_hosts=False
+ )
+ hosts = [h for h in client.hosts]
+ assert hosts == [("127.0.0.1", 9), ("127.0.0.2", 9)]
+
+ def test_invalid_hostname(self) -> None:
+ client = self._makeOne(hosts="nosuchhost/a")
+ timeout = client.handler.timeout_exception
+ with pytest.raises(timeout):
+ client.start(0.1)
+
+ def test_another_invalid_hostname(self) -> None:
+ with pytest.raises(ValueError):
+ self._makeOne(hosts="/nosuchhost/a")
+
+ def test_retry_options_dict(self) -> None:
+ # Deprecated API, but still supported for backwards compatibility
+ client = self._makeOne(
+ command_retry=dict(max_tries=99), connection_retry=dict(delay=99)
+ )
+ assert type(client._conn_retry) is KazooRetry
+ assert type(client._retry) is KazooRetry
+ assert client._retry.max_tries == 99
+ assert client._conn_retry.delay == 99
+
+ def test_retry_options_one_retry(self) -> None:
+ # Deprecated API, but still supported for backwards compatibility
+ client = self._makeOne(
+ command_retry=KazooRetry(max_tries=98),
+ connection_retry=dict(delay=97),
+ )
+ assert type(client._conn_retry) is KazooRetry
+ assert type(client._retry) is KazooRetry
+ assert client._retry.max_tries == 98
+ assert client._conn_retry.delay == 97
+
+ def test_retry_options_other_retry(self) -> None:
+ # Deprecated API, but still supported for backwards compatibility
+ client = self._makeOne(
+ command_retry=dict(max_tries=88),
+ connection_retry=KazooRetry(delay=87),
+ )
+ assert type(client._conn_retry) is KazooRetry
+ assert type(client._retry) is KazooRetry
+ assert client._retry.max_tries == 88
+ assert client._conn_retry.delay == 87
+
+ def test_retry_options_both_retry(self) -> None:
+ client = self._makeOne(
+ command_retry=KazooRetry(max_tries=96),
+ connection_retry=KazooRetry(delay=95),
+ )
+ assert type(client._conn_retry) is KazooRetry
+ assert type(client._retry) is KazooRetry
+ assert client._retry.max_tries == 96
+ assert client._conn_retry.delay == 95
+
+
+class TestAuthentication(KazooTestCase):
+ def test_auth(self) -> None:
+ username = uuid.uuid4().hex
+ password = uuid.uuid4().hex
+
+ digest_auth = "%s:%s" % (username, password)
+ acl = make_digest_acl(username, password, all=True)
+
+ client = self._get_client()
+ client.start()
+ client.add_auth("digest", digest_auth)
+ client.default_acl = (acl,)
+
+ try:
+ client.create("/1")
+ client.create("/1/2")
+ client.ensure_path("/1/2/3")
+
+ eve = self._get_client()
+
+ eve.start()
+
+ with pytest.raises(NoAuthError):
+ eve.get("/1/2")
+
+ # try again with the wrong auth token
+ eve.add_auth("digest", "badbad:bad")
+
+ with pytest.raises(NoAuthError):
+ eve.get("/1/2")
+
+ finally:
+ # Ensure we remove the ACL protected nodes
+ client.delete("/1", recursive=True)
+ eve.stop()
+ eve.close()
+
+ def test_connect_auth(self) -> None:
+ username = uuid.uuid4().hex
+ password = uuid.uuid4().hex
+
+ digest_auth = "%s:%s" % (username, password)
+ acl = make_digest_acl(username, password, all=True)
+
+ client = self._get_client(auth_data=[("digest", digest_auth)])
+ client.start()
+ try:
+ client.create("/1", acl=(acl,))
+ # Give ZK a chance to copy data to other ensemble nodes.
+ # Follower reads are sequentially consistent, but may briefly lag
+ # the leader until the commit is applied locally; sync flushes
+ # the channel between client's connected server and the leader.
+ self.client.sync("/1")
+
+ with pytest.raises(NoAuthError):
+ self.client.get("/1")
+
+ finally:
+ client.delete("/1")
+ client.stop()
+ client.close()
+
+ def test_unicode_auth(self) -> None:
+ username = r"xe4/\hm"
+ password = r"/\xe4hm"
+ digest_auth = "%s:%s" % (username, password)
+ acl = make_digest_acl(username, password, all=True)
+
+ client = self._get_client()
+ client.start()
+ client.add_auth("digest", digest_auth)
+ client.default_acl = (acl,)
+
+ try:
+ client.create("/1")
+ client.ensure_path("/1/2/3")
+
+ eve = self._get_client()
+ eve.start()
+
+ with pytest.raises(NoAuthError):
+ eve.get("/1/2")
+
+ # try again with the wrong auth token
+ eve.add_auth("digest", "badbad:bad")
+
+ with pytest.raises(NoAuthError):
+ eve.get("/1/2")
+
+ finally:
+ # Ensure we remove the ACL protected nodes
+ client.delete("/1", recursive=True)
+ eve.stop()
+ eve.close()
+
+ def test_invalid_auth(self) -> None:
+ # Fixes deprecated warning for add_auth() with a tuple instead of a
+ # string
+ client = self._get_client()
+ client.start()
+
+ with pytest.raises(TypeError):
+ client.add_auth(
+ "digest",
+ ("user", "pass"), # type: ignore[arg-type]
+ )
+
+ with pytest.raises(TypeError):
+ client.add_auth(None, ("user", "pass")) # type: ignore[arg-type]
+
+ def test_async_auth(self) -> None:
+ client = self._get_client()
+ client.start()
+ username = uuid.uuid4().hex
+ password = uuid.uuid4().hex
+ digest_auth = "%s:%s" % (username, password)
+ result = client.add_auth_async("digest", digest_auth)
+ assert result.get() is True
+
+ def test_async_auth_failure(self) -> None:
+ client = self._get_client()
+ client.start()
+ username = uuid.uuid4().hex
+ password = uuid.uuid4().hex
+ digest_auth = "%s:%s" % (username, password)
+
+ with pytest.raises(AuthFailedError):
+ client.add_auth("unknown-scheme", digest_auth)
+
+ def test_add_auth_on_reconnect(self) -> None:
+ client = self._get_client()
+ client.start()
+ client.add_auth("digest", "jsmith:jsmith")
+
+ ev_lost = client.handler.event_object()
+ ev_connected = client.handler.event_object()
+
+ def listener(state: KazooState) -> None:
+ if state in (KazooState.SUSPENDED, KazooState.LOST):
+ ev_lost.set()
+ elif state == KazooState.CONNECTED and ev_lost.is_set():
+ ev_connected.set()
+
+ client.add_listener(listener)
+ assert client._connection._socket is not None
+ client._connection._socket.shutdown(socket.SHUT_RDWR)
+
+ ev_connected.wait(15)
+ assert ev_connected.is_set()
+ assert ("digest", "jsmith:jsmith") in client.auth_data
+
+
+class TestConnection(KazooTestCase):
+ @staticmethod
+ def make_event() -> threading.Event:
+ return threading.Event()
+
+ @staticmethod
+ def make_condition() -> threading.Condition:
+ return threading.Condition()
+
+ def test_chroot_warning(self) -> None:
+ k = self._get_nonchroot_client()
+ k.chroot = "abba"
+ try:
+ with patch("warnings.warn") as mock_func:
+ k.start()
+ assert mock_func.called
+ finally:
+ k.stop()
+
+ def test_session_expire(self) -> None:
+
+ cv = self.make_event()
+
+ def watch_events(event: KazooState) -> None:
+ if event == KazooState.LOST:
+ cv.set()
+
+ self.client.add_listener(watch_events)
+ self.expire_session(self.make_event)
+ cv.wait(3)
+ assert cv.is_set()
+
+ def test_bad_session_expire(self) -> None:
+
+ cv = self.make_event()
+ ab = self.make_event()
+
+ def watch_events(event: KazooState) -> None:
+ if event == KazooState.LOST:
+ ab.set()
+ raise Exception("oops")
+
+ self.client.add_listener(watch_events)
+ self.expire_session(self.make_event)
+ ab.wait(5.0)
+ assert ab.is_set()
+ cv.wait(0.5)
+ assert not cv.is_set()
+
+ def test_state_listener(self) -> None:
+ states = []
+ condition = self.make_condition()
+
+ def listener(state: KazooState) -> None:
+ with condition:
+ states.append(state)
+ condition.notify_all()
+
+ self.client.stop()
+ assert self.client.state == KazooState.LOST
+ self.client.add_listener(listener)
+ self.client.start(5)
+
+ with condition:
+ if not states:
+ condition.wait(5)
+
+ assert len(states) == 1
+ assert states[0] == KazooState.CONNECTED
+
+ def test_invalid_listener(self) -> None:
+ with pytest.raises(ConfigurationError):
+ self.client.add_listener(15) # type: ignore[arg-type]
+
+ def test_listener_only_called_on_real_state_change(self) -> None:
+ assert self.client.state == KazooState.CONNECTED
+ called = [False]
+ condition = self.make_event()
+
+ def listener(state: KazooState) -> None:
+ called[0] = True
+ condition.set()
+
+ self.client.add_listener(listener)
+ self.client._make_state_change(KazooState.CONNECTED)
+ condition.wait(3)
+ assert called[0] is False
+
+ def test_no_connection(self) -> None:
+ client = self.client
+ client.stop()
+ assert client.connected is False
+ assert client.client_id is None
+
+ with pytest.raises(ConnectionClosedError):
+ client.exists("/")
+
+ def test_close_connecting_connection(self) -> None:
+ client = self.client
+ client.stop()
+ ev = self.make_event()
+
+ def close_on_connecting(state: KazooState) -> None:
+ if state in (KazooState.CONNECTED, KazooState.LOST):
+ ev.set()
+
+ client.add_listener(close_on_connecting)
+ client.start()
+
+ # Wait until we connect
+ ev.wait(5)
+ ev.clear()
+ self.client._call(_CONNECTION_DROP, client.handler.async_result())
+
+ client.stop()
+
+ # ...and then wait until the connection is lost
+ ev.wait(5)
+
+ with pytest.raises(ConnectionClosedError):
+ self.client.create("/foobar")
+
+ def test_double_start(self) -> None:
+ assert self.client.connected is True
+ self.client.start()
+ assert self.client.connected is True
+
+ def test_double_stop(self) -> None:
+ self.client.stop()
+ assert self.client.connected is False
+ self.client.stop()
+ assert self.client.connected is False
+
+ def test_restart(self) -> None:
+ assert self.client.connected is True
+ self.client.restart()
+ assert self.client.connected is True
+
+ def test_closed(self) -> None:
+ client = self.client
+ client.stop()
+
+ write_sock = client._connection._write_sock
+
+ # close the connection to free the socket
+ client.close()
+ assert client._connection._write_sock is None
+
+ # sneak in and patch client to simulate race between a thread
+ # calling stop(); close() and one running a command
+ oldstate = client._state
+ client._state = KeeperState.CONNECTED
+ client._connection._write_sock = write_sock
+
+ try:
+ # simulate call made after write socket is closed
+ with pytest.raises(ConnectionClosedError):
+ client.exists("/")
+
+ # simulate call made after write socket is set to None
+ client._connection._write_sock = None
+
+ with pytest.raises(ConnectionClosedError):
+ client.exists("/")
+
+ finally:
+ # reset for teardown
+ client._state = oldstate
+ client._connection._write_sock = None
+
+ def test_watch_trigger_expire(self) -> None:
+ client = self.client
+ cv = self.make_event()
+
+ client.create("/test", b"")
+
+ def test_watch(event: WatchedEvent) -> None:
+ cv.set()
+
+ client.get("/test/", watch=test_watch)
+ self.expire_session(self.make_event)
+
+ cv.wait(3)
+ assert cv.is_set()
+
+
+class TestClient(KazooTestCase):
+ _makeOne = SequentialThreadingHandler
+
+ def test_server_version_retries_fail(self) -> None:
+
+ client = self.client
+ side_effects = [
+ "",
+ "zookeeper.version=",
+ "zookeeper.version=1.",
+ "zookeeper.ver",
+ ]
+ client.command = MagicMock() # type: ignore[method-assign]
+ client.command.side_effect = side_effects
+ with pytest.raises(KazooException):
+ client.server_version(retries=len(side_effects) - 1)
+
+ def test_server_version_retries_eventually_ok(self) -> None:
+ client = self.client
+ actual_version = "zookeeper.version=1.2"
+ side_effects = []
+ for i in range(0, len(actual_version) + 1):
+ side_effects.append(actual_version[0:i])
+ client.command = MagicMock() # type: ignore[method-assign]
+ client.command.side_effect = side_effects
+ assert client.server_version(retries=len(side_effects) - 1) == (1, 2)
+
+ def test_client_id(self) -> None:
+ client_id = self.client.client_id
+ assert type(client_id) is tuple
+ # make sure password is of correct length
+ assert len(client_id[1]) == 16
+
+ def test_connected(self) -> None:
+ client = self.client
+ assert client.connected
+
+ def test_create(self) -> None:
+ client = self.client
+ path = client.create("/1")
+ assert path == "/1"
+ assert client.exists("/1")
+
+ def test_create_on_broken_connection(self) -> None:
+ client = self.client
+ client.start()
+
+ client._state = KeeperState.EXPIRED_SESSION
+ with pytest.raises(SessionExpiredError):
+ client.create("/closedpath", b"bar")
+
+ client._state = KeeperState.AUTH_FAILED
+ with pytest.raises(AuthFailedError):
+ client.create("/closedpath", b"bar")
+
+ client.stop()
+ client.close()
+
+ with pytest.raises(ConnectionClosedError):
+ client.create("/closedpath", b"bar")
+
+ def test_create_null_data(self) -> None:
+ client = self.client
+ client.create("/nulldata", None)
+ value, _ = client.get("/nulldata")
+ assert value is None
+
+ def test_create_empty_string(self) -> None:
+ client = self.client
+ client.create("/empty", b"")
+ value, _ = client.get("/empty")
+ assert value == b""
+
+ def test_create_unicode_path(self) -> None:
+ client = self.client
+ path = client.create("/ascii")
+ assert path == "/ascii"
+ path = client.create("/\xe4hm")
+ assert path == "/\xe4hm"
+
+ def test_create_async_returns_unchrooted_path(self) -> None:
+ client = self.client
+ path = client.create_async("/1").get()
+ assert path == "/1"
+
+ def test_create_invalid_path(self) -> None:
+ client = self.client
+ with pytest.raises(TypeError):
+ client.create(("a",)) # type: ignore[call-overload]
+ with pytest.raises(ValueError):
+ client.create(".")
+ with pytest.raises(ValueError):
+ client.create("/a/../b")
+ with pytest.raises(BadArgumentsError):
+ client.create("/b\x00")
+ with pytest.raises(BadArgumentsError):
+ client.create("/b\x1e")
+
+ def test_create_invalid_arguments(self) -> None:
+ single_acl = OPEN_ACL_UNSAFE[0]
+ client = self.client
+ with pytest.raises(TypeError):
+ client.create("a", acl="all") # type: ignore[arg-type]
+ with pytest.raises(TypeError):
+ client.create("a", acl=single_acl) # type: ignore[arg-type]
+ with pytest.raises(TypeError):
+ client.create("a", value=["a"]) # type: ignore[call-overload]
+ with pytest.raises(TypeError):
+ client.create("a", ephemeral="yes") # type: ignore[call-overload]
+ with pytest.raises(TypeError):
+ client.create("a", sequence="yes") # type: ignore[call-overload]
+ with pytest.raises(TypeError):
+ client.create("a", makepath="yes") # type: ignore[call-overload]
+
+ def test_create_value(self) -> None:
+ client = self.client
+ client.create("/1", b"bytes")
+ data, stat = client.get("/1")
+ assert data == b"bytes"
+
+ def test_create_unicode_value(self) -> None:
+ client = self.client
+ with pytest.raises(TypeError):
+ client.create("/1", "\xe4hm") # type: ignore[call-overload]
+
+ def test_create_large_value(self) -> None:
+ client = self.client
+ kb_512 = b"a" * (512 * 1024)
+ client.create("/1", kb_512)
+ assert client.exists("/1")
+ mb_2 = b"a" * (2 * 1024 * 1024)
+ with pytest.raises(ConnectionLoss):
+ client.create("/2", mb_2)
+
+ def test_create_acl_duplicate(self) -> None:
+ single_acl = OPEN_ACL_UNSAFE[0]
+ client = self.client
+ client.create("/1", acl=[single_acl, single_acl])
+ acls, stat = client.get_acls("/1")
+ assert len(acls) == 1
+
+ def test_create_acl_empty_list(self) -> None:
+ client = self.client
+ client.create("/1", acl=[])
+ acls, stat = client.get_acls("/1")
+ assert acls == OPEN_ACL_UNSAFE
+
+ def test_version_no_connection(self) -> None:
+ self.client.stop()
+ with pytest.raises(ConnectionLoss):
+ self.client.server_version()
+
+ def test_create_ephemeral(self) -> None:
+ client = self.client
+ client.create("/1", b"ephemeral", ephemeral=True)
+ assert client.client_id is not None
+ data, stat = client.get("/1")
+ assert data == b"ephemeral"
+ assert stat.ephemeralOwner == client.client_id[0]
+
+ def test_create_no_ephemeral(self) -> None:
+ client = self.client
+ client.create("/1", b"val1")
+ data, stat = client.get("/1")
+ assert not stat.ephemeralOwner
+
+ def test_create_ephemeral_no_children(self) -> None:
+ client = self.client
+ client.create("/1", b"ephemeral", ephemeral=True)
+ with pytest.raises(NoChildrenForEphemeralsError):
+ client.create("/1/2", b"val1")
+ with pytest.raises(NoChildrenForEphemeralsError):
+ client.create("/1/2", b"val1", ephemeral=True)
+
+ def test_create_sequence(self) -> None:
+ client = self.client
+ client.create("/folder")
+ path = client.create("/folder/a", b"sequence", sequence=True)
+ assert path == "/folder/a0000000000"
+ path2 = client.create("/folder/a", b"sequence", sequence=True)
+ assert path2 == "/folder/a0000000001"
+ path3 = client.create("/folder/", b"sequence", sequence=True)
+ assert path3 == "/folder/0000000002"
+
+ def test_create_ephemeral_sequence(self) -> None:
+ basepath = "/" + uuid.uuid4().hex
+ realpath = self.client.create(
+ basepath, b"sandwich", sequence=True, ephemeral=True
+ )
+ assert basepath != realpath and realpath.startswith(basepath)
+ data, stat = self.client.get(realpath)
+ assert data == b"sandwich"
+
+ def test_create_makepath(self) -> None:
+ self.client.create("/1/2", b"val1", makepath=True)
+ data, stat = self.client.get("/1/2")
+ assert data == b"val1"
+
+ self.client.create("/1/2/3/4/5", b"val2", makepath=True)
+ data, stat = self.client.get("/1/2/3/4/5")
+ assert data == b"val2"
+
+ with pytest.raises(NodeExistsError):
+ self.client.create("/1/2/3/4/5", b"val2", makepath=True)
+
+ def test_create_makepath_incompatible_acls(self) -> None:
+ credential = make_digest_acl_credential("username", "password")
+ alt_client = KazooClient(
+ self.cluster[0].address + self.client.chroot,
+ max_retries=5,
+ auth_data=[("digest", credential)],
+ handler=self._makeOne(),
+ )
+ alt_client.start()
+ alt_client.create("/1/2", b"val2", makepath=True, acl=CREATOR_ALL_ACL)
+
+ try:
+ with pytest.raises(NoAuthError):
+ self.client.create("/1/2/3/4/5", b"val2", makepath=True)
+
+ finally:
+ alt_client.delete("/", recursive=True)
+ alt_client.stop()
+
+ def test_create_no_makepath(self) -> None:
+ with pytest.raises(NoNodeError):
+ self.client.create("/1/2", b"val1")
+ with pytest.raises(NoNodeError):
+ self.client.create("/1/2", b"val1", makepath=False)
+
+ self.client.create("/1/2", b"val1", makepath=True)
+ with pytest.raises(NoNodeError):
+ self.client.create("/1/2/3/4", b"val1", makepath=False)
+
+ def test_create_exists(self) -> None:
+ client = self.client
+ path = client.create("/1")
+ with pytest.raises(NodeExistsError):
+ client.create(path)
+
+ def test_create_stat(self) -> None:
+ client = self.client
+ path, stat1 = client.create("/1", b"bytes", include_data=True)
+ data, stat2 = client.get("/1")
+ assert data == b"bytes"
+ assert stat1 == stat2
+
+ def test_create_get_set(self) -> None:
+ nodepath = "/" + uuid.uuid4().hex
+
+ self.client.create(nodepath, b"sandwich", ephemeral=True)
+
+ data, stat = self.client.get(nodepath)
+ assert data == b"sandwich"
+
+ newstat = self.client.set(nodepath, b"hats", stat.version)
+ assert newstat
+ assert newstat.version > stat.version
+
+ # Some other checks of the ZnodeStat object we got
+ assert newstat.acl_version == stat.acl_version
+ assert newstat.created == stat.ctime / 1000.0
+ assert newstat.last_modified == newstat.mtime / 1000.0
+ assert newstat.owner_session_id == stat.ephemeralOwner
+ assert newstat.creation_transaction_id == stat.czxid
+ assert newstat.last_modified_transaction_id == newstat.mzxid
+ assert newstat.data_length == newstat.dataLength
+ assert newstat.children_count == stat.numChildren
+ assert newstat.children_version == stat.cversion
+
+ def test_get_invalid_arguments(self) -> None:
+ client = self.client
+ with pytest.raises(TypeError):
+ client.get(("a", "b")) # type: ignore[arg-type]
+ with pytest.raises(TypeError):
+ client.get("a", watch=True) # type: ignore[arg-type]
+
+ def test_bad_argument(self) -> None:
+ client = self.client
+ client.ensure_path("/1")
+ with pytest.raises(TypeError):
+ self.client.set("/1", 1) # type: ignore[arg-type]
+
+ def test_ensure_path(self) -> None:
+ client = self.client
+ client.ensure_path("/1/2")
+ assert client.exists("/1/2")
+
+ client.ensure_path("/1/2/3/4")
+ assert client.exists("/1/2/3/4")
+
+ def test_sync(self) -> None:
+ client = self.client
+ assert client.sync("/") == "/"
+ # Albeit surprising, you can sync anything, even what does not exist.
+ assert client.sync("/not_there") == "/not_there"
+
+ def test_exists(self) -> None:
+ nodepath = "/" + uuid.uuid4().hex
+
+ exists = self.client.exists(nodepath)
+ assert exists is None
+
+ self.client.create(nodepath, b"sandwich", ephemeral=True)
+ exists = self.client.exists(nodepath)
+ assert exists
+ assert isinstance(exists.version, int)
+
+ multi_node_nonexistent = "/" + uuid.uuid4().hex + "/hats"
+ exists = self.client.exists(multi_node_nonexistent)
+ assert exists is None
+
+ def test_exists_invalid_arguments(self) -> None:
+ client = self.client
+ with pytest.raises(TypeError):
+ client.exists(("a", "b")) # type: ignore[arg-type]
+ with pytest.raises(TypeError):
+ client.exists("a", watch=True) # type: ignore[arg-type]
+
+ def test_exists_watch(self) -> None:
+ nodepath = "/" + uuid.uuid4().hex
+ event = self.client.handler.event_object()
+
+ def w(watch_event: WatchedEvent) -> None:
+ assert watch_event.path == nodepath
+ event.set()
+
+ exists = self.client.exists(nodepath, watch=w)
+ assert exists is None
+
+ self.client.create(nodepath, ephemeral=True)
+
+ event.wait(1)
+ assert event.is_set() is True
+
+ def test_exists_watcher_exception(self) -> None:
+ nodepath = "/" + uuid.uuid4().hex
+ event = self.client.handler.event_object()
+
+ # if the watcher throws an exception, all we can really do is log it
+ def w(watch_event: WatchedEvent) -> None:
+ assert watch_event.path == nodepath
+ event.set()
+
+ raise Exception("test exception in callback")
+
+ exists = self.client.exists(nodepath, watch=w)
+ assert exists is None
+
+ self.client.create(nodepath, ephemeral=True)
+
+ event.wait(1)
+ assert event.is_set() is True
+
+ def test_create_delete(self) -> None:
+ nodepath = "/" + uuid.uuid4().hex
+
+ self.client.create(nodepath, b"zzz")
+
+ self.client.delete(nodepath)
+
+ exists = self.client.exists(nodepath)
+ assert exists is None
+
+ def test_get_acls(self) -> None:
+ user = "user"
+ passw = "pass"
+ acl = make_digest_acl(user, passw, all=True)
+ client = self.client
+ try:
+ client.create("/a", acl=[acl])
+ client.add_auth("digest", "{}:{}".format(user, passw))
+ assert acl in client.get_acls("/a")[0]
+ finally:
+ client.delete("/a")
+
+ def test_get_acls_invalid_arguments(self) -> None:
+ client = self.client
+ with pytest.raises(TypeError):
+ client.get_acls(("a", "b")) # type: ignore[arg-type]
+
+ def test_set_acls(self) -> None:
+ user = "user"
+ passw = "pass"
+ acl = make_digest_acl(user, passw, all=True)
+ client = self.client
+ client.create("/a")
+ try:
+ client.set_acls("/a", [acl])
+ client.add_auth("digest", "{}:{}".format(user, passw))
+ assert acl in client.get_acls("/a")[0]
+ finally:
+ client.delete("/a")
+
+ def test_set_acls_empty(self) -> None:
+ client = self.client
+ client.create("/a")
+ with pytest.raises(InvalidACLError):
+ client.set_acls("/a", [])
+
+ def test_set_acls_no_node(self) -> None:
+ client = self.client
+ with pytest.raises(NoNodeError):
+ client.set_acls("/a", OPEN_ACL_UNSAFE)
+
+ def test_set_acls_invalid_arguments(self) -> None:
+ single_acl = OPEN_ACL_UNSAFE[0]
+ client = self.client
+ with pytest.raises(TypeError):
+ client.set_acls(("a", "b"), ()) # type: ignore[arg-type]
+ with pytest.raises(TypeError):
+ client.set_acls("a", single_acl) # type: ignore[arg-type]
+ with pytest.raises(TypeError):
+ client.set_acls("a", "all") # type: ignore[arg-type]
+ with pytest.raises(TypeError):
+ client.set_acls("a", [single_acl], "V1") # type: ignore[arg-type]
+
+ def test_set(self) -> None:
+ client = self.client
+ client.create("a", b"first")
+ stat = client.set("a", b"second")
+ data, stat2 = client.get("a")
+ assert data == b"second"
+ assert stat == stat2
+
+ def test_set_null_data(self) -> None:
+ client = self.client
+ client.create("/nulldata", b"not none")
+ client.set("/nulldata", None)
+ value, _ = client.get("/nulldata")
+ assert value is None
+
+ def test_set_empty_string(self) -> None:
+ client = self.client
+ client.create("/empty", b"not empty")
+ client.set("/empty", b"")
+ value, _ = client.get("/empty")
+ assert value == b""
+
+ def test_set_invalid_arguments(self) -> None:
+ client = self.client
+ client.create("a", b"first")
+ with pytest.raises(TypeError):
+ client.set(("a", "b"), b"value") # type: ignore[arg-type]
+ with pytest.raises(TypeError):
+ client.set("a", ["v", "w"]) # type: ignore[arg-type]
+ with pytest.raises(TypeError):
+ client.set("a", b"value", "V1") # type: ignore[arg-type]
+
+ def test_delete(self) -> None:
+ client = self.client
+ client.ensure_path("/a/b")
+ assert "b" in client.get_children("a")
+ client.delete("/a/b")
+ assert "b" not in client.get_children("a")
+
+ def test_delete_recursive(self) -> None:
+ client = self.client
+ client.ensure_path("/a/b/c")
+ client.ensure_path("/a/b/d")
+ client.delete("/a/b", recursive=True)
+ client.delete("/a/b/c", recursive=True)
+ assert "b" not in client.get_children("a")
+
+ def test_delete_invalid_arguments(self) -> None:
+ client = self.client
+ client.ensure_path("/a/b")
+ with pytest.raises(TypeError):
+ client.delete("/a/b", recursive="all") # type: ignore[arg-type]
+ with pytest.raises(TypeError):
+ client.delete(("a", "b")) # type: ignore[arg-type]
+ with pytest.raises(TypeError):
+ client.delete("/a/b", version="V1") # type: ignore[arg-type]
+
+ def test_get_children(self) -> None:
+ client = self.client
+ client.ensure_path("/a/b/c")
+ client.ensure_path("/a/b/d")
+ assert client.get_children("/a") == ["b"]
+ assert set(client.get_children("/a/b")) == set(["c", "d"])
+ assert client.get_children("/a/b/c") == []
+
+ def test_get_children2(self) -> None:
+ client = self.client
+ client.ensure_path("/a/b")
+ children, stat = client.get_children("/a", include_data=True)
+ value, stat2 = client.get("/a")
+ assert children == ["b"]
+ assert stat2.version == stat.version
+
+ def test_get_children2_many_nodes(self) -> None:
+ client = self.client
+ client.ensure_path("/a/b")
+ client.ensure_path("/a/c")
+ client.ensure_path("/a/d")
+ children, stat = client.get_children("/a", include_data=True)
+ value, stat2 = client.get("/a")
+ assert set(children) == set(["b", "c", "d"])
+ assert stat2.version == stat.version
+
+ def test_get_children_no_node(self) -> None:
+ client = self.client
+ with pytest.raises(NoNodeError):
+ client.get_children("/none")
+ with pytest.raises(NoNodeError):
+ client.get_children("/none", include_data=True)
+
+ def test_get_children_invalid_path(self) -> None:
+ client = self.client
+ with pytest.raises(ValueError):
+ client.get_children("../a")
+
+ def test_get_children_invalid_arguments(self) -> None:
+ client = self.client
+ with pytest.raises(TypeError):
+ client.get_children(("a", "b")) # type: ignore[call-overload]
+ with pytest.raises(TypeError):
+ client.get_children("a", watch=True) # type: ignore[call-overload]
+ with pytest.raises(TypeError):
+ client.get_children( # type: ignore[call-overload]
+ "a", include_data="yes"
+ )
+
+ def test_invalid_auth(self) -> None:
+ client = self.client
+ client.stop()
+ client._state = KeeperState.AUTH_FAILED
+
+ with pytest.raises(AuthFailedError):
+ client.get("/")
+
+ def test_client_state(self) -> None:
+ assert self.client.client_state == KeeperState.CONNECTED
+
+ def test_update_host_list(self) -> None:
+ hosts = self.cluster[0].address
+ # create a client with only one server in its list
+ client = KazooClient(hosts=hosts)
+ client.start()
+
+ # try to change the chroot, not currently allowed
+ with pytest.raises(ConfigurationError):
+ client.set_hosts(hosts + "/new_chroot")
+
+ # grow the cluster to 3
+ client.set_hosts(self.servers)
+
+ # shut down the first host
+ try:
+ ev_connected = client.handler.event_object()
+
+ def listener(state: KazooState) -> None:
+ if state == KazooState.CONNECTED:
+ ev_connected.set()
+
+ client.add_listener(listener)
+
+ self.cluster[0].stop()
+ ev_connected.wait(60)
+ assert ev_connected.is_set(), (
+ f"Failover timed out after 60s: ev_connected not set. "
+ f"client.state={client.state}, "
+ f"client.client_state={client.client_state}, "
+ f"client.connected={client.connected}, "
+ f"hosts={client.hosts}"
+ )
+ assert client.client_state == KeeperState.CONNECTED
+ finally:
+ self.cluster[0].run()
+
+ # utility for test_request_queuing*
+ def _make_request_queuing_client(
+ self,
+ ) -> tuple[KazooClient, ManagedZooKeeper]:
+
+ server = self.cluster[0]
+ handler = self._makeOne()
+ # create a client with only one server in its list, and
+ # infinite retries
+ client = KazooClient(
+ hosts=server.address + self.client.chroot,
+ handler=handler,
+ connection_retry=dict(
+ max_tries=-1,
+ delay=0.1,
+ backoff=1,
+ max_jitter=0.0,
+ sleep_func=handler.sleep_func,
+ ),
+ )
+
+ return client, server
+
+ # utility for test_request_queuing*
+ def _request_queuing_common(
+ self,
+ client: KazooClient,
+ server: ManagedZooKeeper,
+ path: str,
+ expire_session: bool,
+ ) -> IAsyncResult:
+ ev_suspended = client.handler.event_object()
+ ev_connected = client.handler.event_object()
+
+ def listener(state: KazooState) -> None:
+ if state == KazooState.SUSPENDED:
+ ev_suspended.set()
+ elif state == KazooState.CONNECTED:
+ ev_connected.set()
+
+ client.add_listener(listener)
+
+ # wait for the client to connect
+ client.start()
+
+ try:
+ # force the client to suspend
+ server.stop()
+
+ ev_suspended.wait(5)
+ assert ev_suspended.is_set()
+ ev_connected.clear()
+
+ # submit a request, expecting it to be queued
+ result = client.create_async(path)
+ assert len(client._queue) != 0
+ assert result.ready() is False
+ assert client.state == KazooState.SUSPENDED
+
+ # optionally cause a SessionExpiredError to occur by
+ # mangling the first byte of the session password.
+ if expire_session:
+ b0 = b"\x00"
+ if client._session_passwd[0] == 0:
+ b0 = b"\xff"
+ client._session_passwd = b0 + client._session_passwd[1:]
+ finally:
+ server.run()
+
+ # wait for the client to reconnect (either with a recovered
+ # session, or with a new one if expire_session was set)
+ ev_connected.wait(5)
+ assert ev_connected.is_set()
+
+ return result
+
+ def test_request_queuing_session_recovered(self) -> None:
+ path = "/" + uuid.uuid4().hex
+ client, server = self._make_request_queuing_client()
+
+ try:
+ result = self._request_queuing_common(
+ client=client, server=server, path=path, expire_session=False
+ )
+
+ assert result.get() == path
+ assert client.exists(path) is not None
+ finally:
+ client.stop()
+
+ def test_request_queuing_session_expired(self) -> None:
+ path = "/" + uuid.uuid4().hex
+ client, server = self._make_request_queuing_client()
+
+ try:
+ result = self._request_queuing_common(
+ client=client, server=server, path=path, expire_session=True
+ )
+
+ assert len(client._queue) == 0
+ with pytest.raises(SessionExpiredError):
+ result.get()
+ finally:
+ client.stop()
+
+
+class TestSSLClient(KazooTestCase):
+ def setUp(self) -> None:
+ ssl_path = tempfile.mkdtemp()
+ key_path = os.path.join(ssl_path, "key.pem")
+ cert_path = os.path.join(ssl_path, "cert.pem")
+ cacert_path = os.path.join(ssl_path, "cacert.pem")
+ with open(key_path, "wb") as key_file:
+ key_file.write(
+ self.cluster.get_ssl_client_configuration()["client_key"]
+ )
+ with open(cert_path, "wb") as cert_file:
+ cert_file.write(
+ self.cluster.get_ssl_client_configuration()["client_cert"]
+ )
+ with open(cacert_path, "wb") as cacert_file:
+ cacert_file.write(
+ self.cluster.get_ssl_client_configuration()["ca_cert"]
+ )
+ self.setup_zookeeper(
+ use_ssl=True, keyfile=key_path, certfile=cert_path, ca=cacert_path
+ )
+
+ def test_create(self) -> None:
+ client = self.client
+ path = client.create("/1")
+ assert path == "/1"
+ assert client.exists("/1")
+
+
+dummy_dict = {
+ "aversion": 1,
+ "ctime": 0,
+ "cversion": 1,
+ "czxid": 110,
+ "dataLength": 1,
+ "ephemeralOwner": "ben",
+ "mtime": 1,
+ "mzxid": 1,
+ "numChildren": 0,
+ "pzxid": 1,
+ "version": 1,
+}
+
+
+class TestClientTransactions(KazooTestCase):
+ def setUp(self) -> None:
+ KazooTestCase.setUp(self)
+
+ def test_basic_create(self) -> None:
+ t = self.client.transaction()
+ t.create("/freddy")
+ t.create("/fred", ephemeral=True)
+ t.create("/smith", sequence=True)
+ results = t.commit()
+ assert len(results) == 3
+ assert results[0] == "/freddy"
+ assert results[2].startswith("/smith0") is True
+
+ def test_bad_creates(self) -> None:
+ args_list = [
+ (True,),
+ ("/smith", 0),
+ ("/smith", b"", "bleh"),
+ ("/smith", b"", None, "fred"),
+ ("/smith", b"", None, True, "fred"),
+ ]
+
+ for args in args_list:
+ with pytest.raises(TypeError):
+ t = self.client.transaction()
+ t.create(*args) # type: ignore[arg-type]
+
+ def test_default_acl(self) -> None:
+ username = uuid.uuid4().hex
+ password = uuid.uuid4().hex
+
+ digest_auth = "%s:%s" % (username, password)
+ acl = make_digest_acl(username, password, all=True)
+
+ self.client.add_auth("digest", digest_auth)
+ self.client.default_acl = (acl,)
+
+ t = self.client.transaction()
+ t.create("/freddy")
+ results = t.commit()
+ assert results[0] == "/freddy"
+
+ def test_basic_delete(self) -> None:
+ self.client.create("/fred")
+ t = self.client.transaction()
+ t.delete("/fred")
+ results = t.commit()
+ assert results[0] is True
+
+ def test_bad_deletes(self) -> None:
+ args_list = [(True,), ("/smith", "woops")]
+
+ for args in args_list:
+ with pytest.raises(TypeError):
+ t = self.client.transaction()
+ t.delete(*args) # type: ignore[arg-type]
+
+ def test_set(self) -> None:
+ self.client.create("/fred", b"01")
+ t = self.client.transaction()
+ t.set_data("/fred", b"oops")
+ t.commit()
+ res = self.client.get("/fred")
+ assert res[0] == b"oops"
+
+ def test_bad_sets(self) -> None:
+ args_list = [(42, 52), ("/smith", False), ("/smith", b"", "oops")]
+
+ for args in args_list:
+ with pytest.raises(TypeError):
+ t = self.client.transaction()
+ t.set_data(*args) # type: ignore[arg-type]
+
+ def test_check(self) -> None:
+ self.client.create("/fred")
+ version = self.client.get("/fred")[1].version
+ t = self.client.transaction()
+ t.check("/fred", version)
+ t.create("/blah")
+ results = t.commit()
+ assert results[0] is True
+ assert results[1] == "/blah"
+
+ def test_bad_checks(self) -> None:
+ args_list = [(42, 52), ("/smith", "oops")]
+
+ for args in args_list:
+ with pytest.raises(TypeError):
+ t = self.client.transaction()
+ t.check(*args) # type: ignore[arg-type]
+
+ def test_bad_transaction(self) -> None:
+ t = self.client.transaction()
+ t.create("/fred")
+ t.delete("/smith")
+ results = t.commit()
+ assert results[0].__class__ == RolledBackError
+ assert results[1].__class__ == NoNodeError
+
+ def test_bad_commit(self) -> None:
+ t = self.client.transaction()
+ t.committed = True
+
+ with pytest.raises(ValueError):
+ t.commit()
+
+ def test_bad_context(self) -> None:
+ with pytest.raises(TypeError):
+ with self.client.transaction() as t:
+ t.check(4232) # type: ignore[arg-type,call-arg]
+
+ def test_context(self) -> None:
+ with self.client.transaction() as t:
+ t.create("/smith", b"32")
+ assert self.client.get("/smith")[0] == b"32"
+
+
+class TestSessionCallbacks(unittest.TestCase):
+ def test_session_callback_states(self) -> None:
+ client = KazooClient()
+ client._live.set()
+
+ client._session_callback(KeeperState.CONNECTED)
+
+ # Now with stopped
+ client._stopped.set()
+ client._session_callback(KeeperState.CONNECTED)
+
+ # Test several state transitions
+ client._stopped.clear()
+ client.start_async = ( # type: ignore[method-assign]
+ lambda: threading.Event()
+ )
+ client._session_callback(KeeperState.CONNECTED)
+ assert client.state == KazooState.CONNECTED
+
+ client._session_callback(KeeperState.AUTH_FAILED)
+ # FIXME mypy seems to be under the impression that the state can't
+ # change as a result of the above call, even though it can.
+ assert (
+ client.state == KazooState.LOST # type: ignore[comparison-overlap]
+ )
+
+ client._session_callback(-250) # type: ignore[unreachable]
+ assert client.state == KazooState.SUSPENDED
+
+
+class TestCallbacks(KazooTestCase):
+ def test_async_result_callbacks_are_always_called(self) -> None:
+ # create a callback object
+ callback_mock = Mock()
+
+ # simulate waiting for a response
+ async_result = self.client.handler.async_result()
+ async_result.rawlink(callback_mock)
+
+ # begin the procedure to stop the client
+ self.client.stop()
+
+ # the response has just been received;
+ # this should be on another thread,
+ # simultaneously with the stop procedure
+ async_result.set_exception(
+ Exception("Anything that throws an exception")
+ )
+
+ # with the fix the callback should be called
+ assert callback_mock.call_count > 0
+
+
+class TestNonChrootClient(KazooTestCase):
+ def test_create(self) -> None:
+ client = self._get_nonchroot_client()
+ assert client.chroot == ""
+ client.start()
+ node = uuid.uuid4().hex
+ path = client.create(node, ephemeral=True)
+ client.delete(path)
+ client.stop()
+
+ def test_unchroot(self) -> None:
+ client = self._get_nonchroot_client()
+ client.chroot = "/a"
+ # Unchroot'ing the chroot path should return "/"
+ assert client.unchroot("/a") == "/"
+ assert client.unchroot("/a/b") == "/b"
+ assert client.unchroot("/b/c") == "/b/c"
+
+
+class TestReconfig(KazooTestCase):
+ def setUp(self) -> None:
+ KazooTestCase.setUp(self)
+
+ def test_no_super_auth(self) -> None:
+ with pytest.raises(NoAuthError):
+ self.client.reconfig(
+ joining="server.999=0.0.0.0:1234:2345:observer;3456",
+ leaving=None,
+ new_members=None,
+ )
+
+ def test_add_remove_observer(self) -> None:
+ def free_sock_port() -> tuple[socket.socket, int]:
+ s = socket.socket()
+ s.bind(("", 0))
+ return s, s.getsockname()[1]
+
+ username = "super"
+ password = "test"
+ digest_auth = "%s:%s" % (username, password)
+ client = self._get_client(auth_data=[("digest", digest_auth)])
+ client.start()
+
+ # get ports for election, zab and client endpoints. we need to use
+ # ports for which we'd immediately get a RST upon connect(); otherwise
+ # the cluster could crash if it gets a SocketTimeoutException:
+ # https://issues.apache.org/jira/browse/ZOOKEEPER-2202
+ s1, port1 = free_sock_port()
+ s2, port2 = free_sock_port()
+ s3, port3 = free_sock_port()
+
+ joining = "server.100=0.0.0.0:%d:%d:observer;0.0.0.0:%d" % (
+ port1,
+ port2,
+ port3,
+ )
+ data, _ = client.reconfig(
+ joining=joining, leaving=None, new_members=None
+ )
+ assert joining.encode("utf8") in data
+
+ data, _ = client.reconfig(
+ joining=None, leaving="100", new_members=None
+ )
+ assert joining.encode("utf8") not in data
+
+ # try to add it again, but a config number in the future
+ curver = int(data.decode().split("\n")[-1].split("=")[1], base=16)
+ with pytest.raises(BadVersionError):
+ self.client.reconfig(
+ joining=joining,
+ leaving=None,
+ new_members=None,
+ from_config=curver + 1,
+ )
+
+ def test_bad_input(self) -> None:
+ with pytest.raises(BadArgumentsError):
+ self.client.reconfig(
+ joining="some thing", leaving=None, new_members=None
+ )
diff --git a/kazoo/tests/test_connection.py b/kazoo/tests/test_connection.py
index 7f9021fcc..1f5d6c8be 100644
--- a/kazoo/tests/test_connection.py
+++ b/kazoo/tests/test_connection.py
@@ -14,11 +14,7 @@
import pytest
from kazoo.exceptions import ConnectionLoss, NotReadOnlyCallError
-from kazoo.protocol.serialization import (
- Connect,
- int_struct,
- write_string,
-)
+from kazoo.protocol.serialization import Connect, int_struct, write_string
from kazoo.protocol.states import KazooState, KeeperState
from kazoo.protocol.connection import _CONNECTION_DROP
from kazoo.testing import KazooTestCase
@@ -400,11 +396,14 @@ def log_exception(*args: Any) -> None:
args, exc_info = error_stack[-1]
assert args == ("Unhandled exception in connection loop",)
- assert exc_info[0] == RuntimeError
+ assert exc_info[0] is RuntimeError
self.client.handler.sleep_func(0.2)
assert self.connection_routine is not None
- assert not self.connection_routine.is_alive()
+ # This vvvv doesn't work for greenlet handlers as the greenlet doesn't
+ # implement is_alive...
+ if (alive := getattr(self.connection_routine, "is_alive")) is not None:
+ assert not alive()
assert len(self._zk_loop_errors) == 1
assert self._zk_loop_errors[0] == exc_info[1]
diff --git a/kazoo/tests/test_eventlet_handler.py b/kazoo/tests/test_eventlet_handler.py
index e410496a3..058fc173e 100644
--- a/kazoo/tests/test_eventlet_handler.py
+++ b/kazoo/tests/test_eventlet_handler.py
@@ -1,29 +1,45 @@
from __future__ import annotations
import contextlib
+import importlib
import unittest
-from typing import Generator, Literal, TYPE_CHECKING
+from typing import Any, Generator, Literal, TYPE_CHECKING
import pytest
from kazoo.handlers.utils import create_tcp_socket
from kazoo.handlers import utils
+from kazoo.interfaces import IAsyncResult
from kazoo.protocol import states as kazoo_states
+from kazoo.testing.async_result_common import (
+ AsyncResultCommon,
+ ThreadlikeContext,
+)
if TYPE_CHECKING:
from kazoo.handlers.eventlet import SequentialEventletHandler
-try:
+if importlib.util.find_spec("eventlet") is None:
+ # The double test here stops complaints about imports not being at the top
+ # of the file.
+ pytest.importorskip("eventlet", reason="eventlet not available")
+else:
from eventlet.green import socket
+ import eventlet
from kazoo.handlers import eventlet as eventlet_handler
-except ImportError:
- pytestmark = pytest.mark.skip(reason="eventlet not available")
+
+
+class EventletContext(ThreadlikeContext):
+ def __enter__(self) -> None:
+ # Spawn the eventlet and then immediately sleep to hand over control
+ eventlet.spawn(self._target) # type:ignore[no-untyped-call]
+ eventlet.sleep(0) # type:ignore[no-untyped-call]
@contextlib.contextmanager
def start_stop_one(
- handler: SequentialEventletHandler = None, # type: ignore[assignment]
+ handler: SequentialEventletHandler | None = None,
) -> Generator[SequentialEventletHandler]:
if not handler:
handler = eventlet_handler.SequentialEventletHandler()
@@ -34,6 +50,18 @@ def start_stop_one(
handler.stop()
+# Doesn't work with eventlet, so commented out for now.
+class TestEventletAsyncResult(AsyncResultCommon, unittest.TestCase):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super().__init__(
+ eventlet_handler.SequentialEventletHandler,
+ lambda handler: eventlet_handler.AsyncResult(handler),
+ thread_context_factory=EventletContext,
+ *args,
+ **kwargs,
+ )
+
+
class TestEventletHandler(unittest.TestCase):
def test_started(self) -> None:
with start_stop_one() as handler:
@@ -65,10 +93,10 @@ def cb() -> None:
assert len(captures) == 1
def test_async_link(self) -> None:
- captures: list[SequentialEventletHandler] = []
+ captures: list[IAsyncResult] = []
- def cb(handler: SequentialEventletHandler) -> None:
- captures.append(handler)
+ def cb(async_result: IAsyncResult) -> None:
+ captures.append(async_result)
with start_stop_one() as handler:
r = handler.async_result()
@@ -85,14 +113,14 @@ def test_timeout_raising(self) -> None:
raise handler.timeout_exception("This is a timeout")
def test_async_ok(self) -> None:
- captures: list[Literal[1] | SequentialEventletHandler] = []
+ captures: list[Literal[1] | IAsyncResult] = []
def delayed() -> Literal[1]:
captures.append(1)
return 1
- def after_delayed(handler: SequentialEventletHandler) -> None:
- captures.append(handler)
+ def after_delayed(async_result: IAsyncResult) -> None:
+ captures.append(async_result)
with start_stop_one() as handler:
r = handler.async_result()
diff --git a/kazoo/tests/test_gevent_handler.py b/kazoo/tests/test_gevent_handler.py
index eda2505e4..2d0c7c66d 100644
--- a/kazoo/tests/test_gevent_handler.py
+++ b/kazoo/tests/test_gevent_handler.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import importlib.util
import unittest
import sys
@@ -8,25 +9,56 @@
from kazoo.exceptions import NoNodeError
from kazoo.handlers.utils import create_tcp_socket
+from kazoo.interfaces import IAsyncResult
from kazoo.protocol.states import Callback, KazooState, ZnodeStat
from kazoo.testing import KazooTestCase
-
-try:
- import gevent # noqa: F401
+from kazoo.testing.async_result_common import (
+ AsyncResultCommon,
+ ThreadlikeContext,
+)
+
+if importlib.util.find_spec("gevent") is None:
+ # The double test here stops complaints about imports not being at the top
+ # of the file.
+ pytest.importorskip("gevent", reason="gevent not available")
+else:
+ import gevent
from gevent.event import Event
from gevent.queue import Empty
from gevent import socket
from kazoo.handlers.gevent import AsyncResult, SequentialGeventHandler
-except ImportError:
- pytestmark = pytest.mark.skip(reason="gevent not available")
+
+
+class GeventContext(ThreadlikeContext):
+ def __enter__(self) -> None:
+ # Spawn the geventlet and then immediately sleep to hand over control
+ gevent.spawn(self._target)
+ gevent.sleep(0)
+
+
+@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
+class TestGeventAsyncResult(AsyncResultCommon, unittest.TestCase):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super().__init__(
+ SequentialGeventHandler,
+ lambda handler: AsyncResult(),
+ thread_context_factory=GeventContext,
+ *args,
+ **kwargs,
+ )
+
+ # We override this because the wait behaviour for AsyncResult is different
+ # for gevent and everything else even though IASyncResult is meant to be
+ # based on the gevent AsyncResult
+ def test_wait(self, *, is_green: bool = False) -> None:
+ super().test_wait(is_green=True)
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
class TestGeventHandler(unittest.TestCase):
- def _makeOne(self, *args: Any) -> SequentialGeventHandler:
- return SequentialGeventHandler(*args)
+ _makeOne = SequentialGeventHandler
- def _getAsync(self) -> Type[AsyncResult[Any]]:
+ def _getAsync(self) -> Type[IAsyncResult]:
return AsyncResult
def _getEvent(self) -> Type[Event]:
@@ -43,12 +75,6 @@ def test_matching_async(self) -> None:
async_handler = self._getAsync()
assert isinstance(h.async_result(), async_handler)
- def test_exception_raising(self) -> None:
- h = self._makeOne()
-
- with pytest.raises(h.timeout_exception):
- raise h.timeout_exception("This is a timeout")
-
def test_exception_in_queue(self) -> None:
h = self._makeOne()
h.start()
@@ -78,12 +104,11 @@ def func() -> None:
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
class TestBasicGeventClient(KazooTestCase):
+ _makeOne = SequentialGeventHandler
+
def setUp(self) -> None:
KazooTestCase.setUp(self)
- def _makeOne(self, *args: Any) -> SequentialGeventHandler:
- return SequentialGeventHandler(*args)
-
def _getEvent(self) -> Type[Event]:
return Event
diff --git a/kazoo/tests/test_hosts.py b/kazoo/tests/test_hosts.py
index 80517d5d7..6cb603725 100644
--- a/kazoo/tests/test_hosts.py
+++ b/kazoo/tests/test_hosts.py
@@ -16,7 +16,7 @@ def test_ipv4(self) -> None:
("192.168.1.2", 2181),
("132.254.111.10", 2181),
]
- assert chroot is None
+ assert chroot == ""
hosts, chroot = collect_hosts(
["127.0.0.1:2181", "192.168.1.2:2181", "132.254.111.10:2181"]
@@ -26,26 +26,26 @@ def test_ipv4(self) -> None:
("192.168.1.2", 2181),
("132.254.111.10", 2181),
]
- assert chroot is None
+ assert chroot == ""
def test_ipv6(self) -> None:
hosts, chroot = collect_hosts("[fe80::200:5aee:feaa:20a2]:2181")
assert hosts == [("fe80::200:5aee:feaa:20a2", 2181)]
- assert chroot is None
+ assert chroot == ""
hosts, chroot = collect_hosts(["[fe80::200:5aee:feaa:20a2]:2181"])
assert hosts == [("fe80::200:5aee:feaa:20a2", 2181)]
- assert chroot is None
+ assert chroot == ""
def test_hosts_list(self) -> None:
hosts, chroot = collect_hosts("zk01:2181, zk02:2181, zk03:2181")
expected1 = [("zk01", 2181), ("zk02", 2181), ("zk03", 2181)]
assert hosts == expected1
- assert chroot is None
+ assert chroot == ""
hosts, chroot = collect_hosts(["zk01:2181", "zk02:2181", "zk03:2181"])
assert hosts == expected1
- assert chroot is None
+ assert chroot == ""
expected2 = "/test"
hosts, chroot = collect_hosts("zk01:2181, zk02:2181, zk03:2181/test")
diff --git a/kazoo/tests/test_lock.py b/kazoo/tests/test_lock.py
index d03c2e193..a91a48fc6 100644
--- a/kazoo/tests/test_lock.py
+++ b/kazoo/tests/test_lock.py
@@ -341,7 +341,7 @@ def _acquire() -> None:
attempts.append(int(lock.acquire(blocking=False)))
threads = []
- for _i in range(0, self.thread_count):
+ for _ in range(0, self.thread_count):
t = self.make_thread(target=_acquire)
threads.append(t)
t.start()
@@ -374,7 +374,7 @@ def _acquire() -> None:
differences.append(end_count - starting_count)
threads = []
- for _i in range(0, self.thread_count):
+ for _ in range(0, self.thread_count):
t = self.make_thread(target=_acquire)
threads.append(t)
t.start()
diff --git a/kazoo/tests/test_retry.py b/kazoo/tests/test_retry.py
index 9e67c6270..05ffa6edf 100644
--- a/kazoo/tests/test_retry.py
+++ b/kazoo/tests/test_retry.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import Any
+from functools import partial
from unittest import mock
import pytest
@@ -8,19 +8,7 @@
from kazoo import exceptions as ke
from kazoo import retry as kr
-
-def _make_retry(*args: Any, **kwargs: Any) -> kr.KazooRetry:
- """Return a KazooRetry instance with a dummy sleep function."""
-
- def _sleep_func(_time: float) -> None:
- pass
-
- # FIXME better way of doing this? Use TypedDict perhaps?
- return kr.KazooRetry(
- *args,
- sleep_func=_sleep_func,
- **kwargs, # type: ignore[misc]
- )
+_make_retry = partial(kr.KazooRetry, sleep_func=lambda _time: None)
def _make_try_func(times: int = 1) -> mock.Mock:
@@ -28,7 +16,7 @@ def _make_try_func(times: int = 1) -> mock.Mock:
returning None.
"""
callmock = mock.Mock(
- side_effect=[kr.ForceRetryError("Failed!")] * times + [None],
+ side_effect=[kr.ForceRetryError("Failed!")] * times + [None]
)
return callmock
diff --git a/kazoo/tests/test_sasl.py b/kazoo/tests/test_sasl.py
index 411777053..d04b77451 100644
--- a/kazoo/tests/test_sasl.py
+++ b/kazoo/tests/test_sasl.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import importlib.util
import os
import subprocess
import time
@@ -7,17 +8,12 @@
import pytest
from kazoo.testing import KazooTestHarness
-from kazoo.exceptions import (
- AuthFailedError,
- NoAuthError,
-)
+from kazoo.exceptions import AuthFailedError, NoAuthError
class TestLegacySASLDigestAuthentication(KazooTestHarness):
def setUp(self) -> None:
- try:
- import puresasl # NOQA
- except ImportError:
+ if importlib.util.find_spec("puresasl") is None:
pytest.skip("PureSASL not available.")
os.environ["ZOOKEEPER_JAAS_AUTH"] = "digest"
@@ -58,9 +54,7 @@ def test_invalid_sasl_auth(self) -> None:
class TestSASLDigestAuthentication(KazooTestHarness):
def setUp(self) -> None:
- try:
- import puresasl # NOQA
- except ImportError:
+ if importlib.util.find_spec("puresasl") is None:
pytest.skip("PureSASL not available.")
os.environ["ZOOKEEPER_JAAS_AUTH"] = "digest"
@@ -112,13 +106,9 @@ def test_invalid_sasl_auth(self) -> None:
class TestSASLGSSAPIAuthentication(KazooTestHarness):
def setUp(self) -> None:
# puresasl isn't available under windows, so we can't do this test.
- try:
- import puresasl
- except ImportError:
+ if importlib.util.find_spec("puresasl") is None:
pytest.skip("PureSASL not available.")
- try:
- import kerberos # type: ignore[import-not-found,import-untyped]
- except ImportError:
+ if importlib.util.find_spec("kerberos") is None:
pytest.skip("Kerberos support not available.")
if not os.environ.get("KRB5_TEST_ENV"):
pytest.skip("Test Kerberos environ not setup.")
diff --git a/kazoo/tests/test_selectors_select.py b/kazoo/tests/test_selectors_select.py
index 0d9e77da5..84abb3bbb 100644
--- a/kazoo/tests/test_selectors_select.py
+++ b/kazoo/tests/test_selectors_select.py
@@ -10,7 +10,7 @@
import sys
import unittest
-from typing import cast, TYPE_CHECKING
+from typing import TYPE_CHECKING
from kazoo.handlers.utils import selector_select
@@ -24,17 +24,8 @@
(sys.platform[:3] == "win"), "can't easily test on this system"
)
class SelectTestCase(unittest.TestCase):
- class Nope:
- pass
-
- class Almost:
- def fileno(self) -> str:
- return "fileno"
-
def test_error_conditions(self) -> None:
self.assertRaises(TypeError, select, 1, 2, 3)
- self.assertRaises(TypeError, select, [self.Nope()], [], [])
- self.assertRaises(TypeError, select, [self.Almost()], [], [])
self.assertRaises(TypeError, select, [], [], [], "not a number")
self.assertRaises(ValueError, select, [], [], [], -1)
@@ -59,23 +50,14 @@ def test_returned_list_identity(self) -> None:
def test_select(self) -> None:
cmd = "for i in 0 1 2 3 4 5 6 7 8 9; do echo testing...; sleep 1; done"
with subprocess.Popen(
- cmd,
- shell=True,
- stdout=subprocess.PIPE,
- text=True,
+ cmd, shell=True, stdout=subprocess.PIPE, text=True
) as process:
assert process.stdout is not None
for tout in (0, 1, 2, 4, 8, 16) + (None,) * 10:
- rfd, wfd, xfd = select(
- [cast("HasFileNo", process.stdout)], [], [], tout
- )
+ rfd, wfd, xfd = select([process.stdout], [], [], tout)
if (rfd, wfd, xfd) == ([], [], []):
continue
- if (rfd, wfd, xfd) == (
- [cast("HasFileNo", process.stdout)],
- [],
- [],
- ):
+ if (rfd, wfd, xfd) == ([process.stdout], [], []):
line = process.stdout.readline()
if not line:
break
@@ -101,7 +83,3 @@ def fileno(self) -> int:
finally:
s1.close()
s2.close()
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/kazoo/tests/test_serialization.py b/kazoo/tests/test_serialization.py
index ff339c093..cc643ddb8 100644
--- a/kazoo/tests/test_serialization.py
+++ b/kazoo/tests/test_serialization.py
@@ -209,8 +209,7 @@ def test_serialize(self) -> None:
def test_deserialize(self) -> None:
result_stat = serialization.SetData.deserialize(
- _stat_struct.pack(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11),
- 0,
+ _stat_struct.pack(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11), 0
)
assert result_stat == _make_stat()
@@ -253,8 +252,7 @@ def test_serialize(self) -> None:
def test_deserialize(self) -> None:
result_stat = serialization.SetACL.deserialize(
- _stat_struct.pack(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11),
- 0,
+ _stat_struct.pack(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11), 0
)
assert result_stat == _make_stat()
@@ -269,8 +267,7 @@ def test_serialize(self) -> None:
def test_deserialize(self) -> None:
children = serialization.GetChildren.deserialize(
- _int_struct.pack(2) + _write_string("a") + _write_string("b"),
- 0,
+ _int_struct.pack(2) + _write_string("a") + _write_string("b"), 0
)
assert children == ["a", "b"]
diff --git a/kazoo/tests/test_threading_handler.py b/kazoo/tests/test_threading_handler.py
index f4ac5d434..fb6ec1923 100644
--- a/kazoo/tests/test_threading_handler.py
+++ b/kazoo/tests/test_threading_handler.py
@@ -4,13 +4,46 @@
import threading
import unittest
-from typing import Any, Type
-from unittest.mock import Mock
+from typing import Any, Callable, Type
import pytest
from kazoo.handlers.threading import AsyncResult, SequentialThreadingHandler
from kazoo.handlers.utils import create_tcp_socket
+from kazoo.testing.async_result_common import (
+ AsyncResultCommon,
+ ThreadlikeContext,
+)
+
+
+class ThreadContext(ThreadlikeContext):
+ def __init__(self, target: Callable[[], None]):
+ super().__init__(target)
+ self._thread = threading.Thread(target=self._target)
+
+ def __enter__(self) -> None:
+ self._thread.start()
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: object | None,
+ ) -> None:
+ self._thread.join()
+
+
+class TestThreadingAsyncResult(AsyncResultCommon, unittest.TestCase):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super().__init__(
+ SequentialThreadingHandler,
+ lambda handler: AsyncResult(handler),
+ thread_context_factory=ThreadContext,
+ # threading.Condition,
+ # SequentialThreadingHandler.timeout_exception,
+ *args,
+ **kwargs,
+ )
class TestThreadingHandler(unittest.TestCase):
@@ -23,9 +56,7 @@ def _getAsync(self) -> Type[AsyncResult]:
def test_proper_threading(self) -> None:
h = self._makeOne()
h.start()
- # In Python 3.3 _Event is gone, before Event is function
- event_class = getattr(threading, "_Event", threading.Event)
- assert isinstance(h.event_object(), event_class)
+ assert isinstance(h.event_object(), threading.Event)
def test_matching_async(self) -> None:
h = self._makeOne()
@@ -69,326 +100,3 @@ def test_huge_file_descriptor(self) -> None:
h.stop()
for sock in socks:
sock.close()
-
-
-class TestThreadingAsync(unittest.TestCase):
- def _makeOne(self, *args: Any) -> AsyncResult:
- return AsyncResult(*args)
-
- def _makeHandler(self) -> SequentialThreadingHandler:
- return SequentialThreadingHandler()
-
- def test_ready(self) -> None:
- mock_handler = Mock()
- async_result = self._makeOne(mock_handler)
-
- assert async_result.ready() is False
- async_result.set("val")
- assert async_result.ready() is True
- assert async_result.successful() is True
- assert async_result.exception is None
-
- def test_callback_queued(self) -> None:
- mock_handler = Mock()
- mock_handler.completion_queue = Mock()
- async_result = self._makeOne(mock_handler)
-
- async_result.rawlink(lambda a: a)
- async_result.set("val")
-
- assert mock_handler.completion_queue.put.called
-
- def test_set_exception(self) -> None:
- mock_handler = Mock()
- mock_handler.completion_queue = Mock()
- async_result = self._makeOne(mock_handler)
- async_result.rawlink(lambda a: a)
- async_result.set_exception(ImportError("Error occurred"))
-
- assert isinstance(async_result.exception, ImportError)
- assert mock_handler.completion_queue.put.called
-
- def test_get_wait_while_setting(self) -> None:
- mock_handler = Mock()
- async_result = self._makeOne(mock_handler)
-
- lst = []
- bv = threading.Event()
- cv = threading.Event()
-
- def wait_for_val() -> None:
- bv.set()
- val = async_result.get()
- lst.append(val)
- cv.set()
-
- th = threading.Thread(target=wait_for_val)
- th.start()
- bv.wait()
-
- async_result.set("fred")
- cv.wait()
- assert lst == ["fred"]
- th.join()
-
- def test_get_with_nowait(self) -> None:
- mock_handler = Mock()
- async_result = self._makeOne(mock_handler)
- timeout = self._makeHandler().timeout_exception
-
- with pytest.raises(timeout):
- async_result.get(block=False)
-
- with pytest.raises(timeout):
- async_result.get_nowait()
-
- def test_get_with_exception(self) -> None:
- mock_handler = Mock()
- async_result = self._makeOne(mock_handler)
-
- lst = []
- bv = threading.Event()
- cv = threading.Event()
-
- def wait_for_val() -> None:
- bv.set()
- try:
- val = async_result.get()
- except ImportError:
- lst.append("oops")
- else:
- lst.append(val)
- cv.set()
-
- th = threading.Thread(target=wait_for_val)
- th.start()
- bv.wait()
-
- async_result.set_exception(ImportError())
- cv.wait()
- assert lst == ["oops"]
- th.join()
-
- def test_wait(self) -> None:
- mock_handler = Mock()
- async_result = self._makeOne(mock_handler)
-
- lst: list[bool | str] = []
- bv = threading.Event()
- cv = threading.Event()
-
- def wait_for_val() -> None:
- bv.set()
- try:
- val = async_result.wait(10)
- except ImportError:
- lst.append("oops")
- else:
- lst.append(val)
- cv.set()
-
- th = threading.Thread(target=wait_for_val)
- th.start()
- bv.wait(10)
-
- async_result.set("fred")
- cv.wait(15)
- assert lst == [True]
- th.join()
-
- def test_wait_race(self) -> None:
- """Test that there is no race condition in `IAsyncResult.wait()`.
-
- Guards against the reappearance of:
- https://github.com/python-zk/kazoo/issues/485
- """
- mock_handler = Mock()
- async_result = self._makeOne(mock_handler)
-
- async_result.set("immediate")
-
- cv = threading.Event()
-
- def wait_for_val() -> None:
- # NB: should not sleep
- async_result.wait(20)
- cv.set()
-
- th = threading.Thread(target=wait_for_val)
- th.daemon = True
- th.start()
-
- # if the wait() didn't sleep (correctly), cv will be set quickly
- # if it did sleep, the cv will not be set yet and this will timeout
- cv.wait(10)
- assert cv.is_set() is True
- th.join()
-
- def test_set_before_wait(self) -> None:
- mock_handler = Mock()
- async_result = self._makeOne(mock_handler)
-
- lst = []
- cv = threading.Event()
- async_result.set("fred")
-
- def wait_for_val() -> None:
- val = async_result.get()
- lst.append(val)
- cv.set()
-
- th = threading.Thread(target=wait_for_val)
- th.start()
- cv.wait()
- assert lst == ["fred"]
- th.join()
-
- def test_set_exc_before_wait(self) -> None:
- mock_handler = Mock()
- async_result = self._makeOne(mock_handler)
-
- lst = []
- cv = threading.Event()
- async_result.set_exception(ImportError())
-
- def wait_for_val() -> None:
- try:
- val = async_result.get()
- except ImportError:
- lst.append("ooops")
- else:
- lst.append(val)
- cv.set()
-
- th = threading.Thread(target=wait_for_val)
- th.start()
- cv.wait()
- assert lst == ["ooops"]
- th.join()
-
- def test_linkage(self) -> None:
- mock_handler = Mock()
- async_result = self._makeOne(mock_handler)
- cv = threading.Event()
-
- lst = []
-
- def add_on() -> None:
- lst.append(True)
-
- def wait_for_val() -> None:
- async_result.get()
- cv.set()
-
- th = threading.Thread(target=wait_for_val)
- th.start()
-
- async_result.rawlink(add_on)
- async_result.set(b"fred")
- assert mock_handler.completion_queue.put.called
- async_result.unlink(add_on)
- cv.wait()
- assert async_result.value == b"fred"
- th.join()
-
- def test_linkage_not_ready(self) -> None:
- mock_handler = Mock()
- async_result = self._makeOne(mock_handler)
-
- lst = []
-
- def add_on() -> None:
- lst.append(True)
-
- async_result.set("fred")
- assert not mock_handler.completion_queue.called
- async_result.rawlink(add_on)
- assert mock_handler.completion_queue.put.called
-
- def test_link_and_unlink(self) -> None:
- mock_handler = Mock()
- async_result = self._makeOne(mock_handler)
-
- lst = []
-
- def add_on() -> None:
- lst.append(True)
-
- async_result.rawlink(add_on)
- assert not mock_handler.completion_queue.put.called
- async_result.unlink(add_on)
- async_result.set("fred")
- assert not mock_handler.completion_queue.put.called
-
- def test_captured_exception(self) -> None:
- from kazoo.handlers.utils import capture_exceptions
-
- mock_handler = Mock()
- async_result = self._makeOne(mock_handler)
-
- @capture_exceptions(async_result)
- def exceptional_function() -> float:
- return 1 / 0
-
- exceptional_function()
-
- with pytest.raises(ZeroDivisionError):
- async_result.get()
-
- def test_no_capture_exceptions(self) -> None:
- from kazoo.handlers.utils import capture_exceptions
-
- mock_handler = Mock()
- async_result = self._makeOne(mock_handler)
-
- lst = []
-
- def add_on() -> None:
- lst.append(True)
-
- async_result.rawlink(add_on)
-
- @capture_exceptions(async_result)
- def regular_function() -> bool:
- return True
-
- regular_function()
-
- assert not mock_handler.completion_queue.put.called
-
- def test_wraps(self) -> None:
- from kazoo.handlers.utils import wrap
-
- mock_handler = Mock()
- async_result = self._makeOne(mock_handler)
-
- lst = []
-
- def add_on(result: AsyncResult) -> None:
- lst.append(result.get())
-
- async_result.rawlink(add_on)
-
- @wrap(async_result)
- def regular_function() -> str:
- return "hello"
-
- assert regular_function() == "hello"
- assert mock_handler.completion_queue.put.called
- assert async_result.get() == "hello"
-
- def test_multiple_callbacks(self) -> None:
- mockback1 = Mock(name="mockback1")
- mockback2 = Mock(name="mockback2")
- handler = self._makeHandler()
- handler.start()
-
- async_result = self._makeOne(handler)
- async_result.rawlink(mockback1)
- async_result.rawlink(mockback2)
- async_result.set("howdy")
- async_result.wait()
- handler.stop()
-
- mockback2.assert_called_once_with(async_result)
- mockback1.assert_called_once_with(async_result)
diff --git a/pyproject.toml b/pyproject.toml
index 9455bf328..a81a5440f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -57,11 +57,14 @@ test-extras = [
"pyjks==20.0.0",
"unittest-parametrize==1.8.0",
]
+coverage = [
+ "coverage==7.10.7"
+]
pytest = [
"pytest==8.4.2",
]
test = [
- "coverage==7.10.7",
+ {include-group = "coverage"},
{include-group = "pytest"},
"pytest-cov==7.1.0",
"pytest-timeout==2.4.0",
@@ -126,7 +129,8 @@ line-ending = "auto"
[tool.pytest.ini_options]
addopts = "-ra -v --color=yes"
-log_cli = true
+# log_cli = true
+log_cli = false
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
log_cli_format = "%(asctime)s %(levelname)s %(message)s"
log_cli_level = "INFO"
diff --git a/run_failure.py b/run_failure.py
deleted file mode 100644
index 7bc1dac2d..000000000
--- a/run_failure.py
+++ /dev/null
@@ -1,28 +0,0 @@
-import os
-import sys
-
-
-def test(arg):
- return os.system("bin/pytest -v %s" % arg)
-
-
-def main(args):
- if not args:
- print(
- "Run as bin/python run_failure.py , for example: \n"
- "bin/python run_failure.py "
- "kazoo.tests.test_watchers:KazooChildrenWatcherTests"
- )
- return
- arg = args[0]
- i = 0
- while 1:
- i += 1
- print("Run number: %s" % i)
- ret = test(arg)
- if ret != 0:
- break
-
-
-if __name__ == "__main__":
- main(sys.argv[1:])
diff --git a/tox.ini b/tox.ini
index 88995960e..8b01121bf 100644
--- a/tox.ini
+++ b/tox.ini
@@ -31,14 +31,22 @@ allowlist_externals =
{toxinidir}/ensure-zookeeper-env.sh
{toxinidir}/init_krb5.sh
bash
+ echo
commands =
+ echo Current environment: {env:TOX_ENV_NAME}
bash \
sasl: {toxinidir}/init_krb5.sh {envtmpdir}/kerberos \
{toxinidir}/ensure-zookeeper-env.sh \
- pytest {posargs: -ra -v --cov-report=xml --cov=kazoo kazoo/tests}
+ pytest {posargs: -ra -v --cov-append --cov-report=xml --cov=kazoo kazoo/tests}
[testenv:build]
+[testenv:clean_coverage]
+dependency_groups =
+ coverage
+skip_install = true
+commands = coverage erase
+
[testenv:docs]
dependency_groups =
test-extras