Skip to content

feat(testing): overhaul test harness with docker compose and restructure kazoo.testing package - #802

Open
ceache wants to merge 10 commits into
python-zk:masterfrom
ceache:feature/docker-compose-tests
Open

feat(testing): overhaul test harness with docker compose and restructure kazoo.testing package#802
ceache wants to merge 10 commits into
python-zk:masterfrom
ceache:feature/docker-compose-tests

Conversation

@ceache

@ceache ceache commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #

Why is this needed?

The legacy test harness (kazoo.testing.harness) relied on local JVM/ZooKeeper binary installations, custom environment setup scripts (ensure-zookeeper-env.sh), and hardcoded port configurations. This created multiple maintenance and developer experience challenges:

  1. Environment friction & matrix testing: Running tests across different ZooKeeper versions (3.7 through 3.9) and authentication schemes (Plain, Digest, SASL Digest-MD5, SASL GSSAPI/Kerberos, mutual TLS) required manual daemon management and external scripts that were fragile across Linux, macOS, and Windows.
  2. Slow feedback loops: Offline unit tests were mixed with integration tests, requiring a live cluster setup even for simple logic and parsing tests.
  3. Flakiness and protocol edge cases: Long-standing race conditions and unhandled protocol scenarios (such as SASL disconnection retry handling and TLS SNI formatting in four-letter word commands) hindered reliable CI runs.

Proposed Changes

This PR introduces a modular, containerized test harness powered by Docker Compose and pytest fixtures, separates offline unit tests, and hardens client/protocol reliability:

  • Docker Compose Test Harness (kazoo.testing):

    • Replaced legacy harness.py with modular packages: kazoo.testing.common (axis resolution, mount handling, ensemble state) and kazoo.testing.fixtures (pytest fixtures).
    • Composable Docker Compose overlays:
      • Base 3-node ZooKeeper cluster (zoo1, zoo2, zoo3).
      • Authentication overlays: Digest (superDigest), SASL Digest (DIGEST-MD5), SASL GSSAPI (with embedded MIT Kerberos KDC sidecar), and mutual TLS (with certgen and tls-secrets-agent sidecars).
      • Capture overlay: On-demand packet capture (tshark) generating .pcapng traces and TLS keylog material.
    • Native ephemeral host port publishing (2181 / 2281), eliminating static port math and port collisions.
    • Per-test chroot isolation (zkchroot) shared cleanly across zkclient and zksuperadmin_client.
    • Process environment isolation via docker_env snapshot and restoration on session teardown.
    • Standardized KAZOO_TESTING_ZK_* environment variables (with fallback to ZK_* in CLI option parsing).
    • Registered collection-time pytest markers (zk_version, zk_auth, zk_features) and CLI options (--zk-version, --zk-auth, --zk-features).
  • Offline Unit Test Separation (kazoo/tests/unit):

    • Separated offline tests from integration tests; pytest kazoo/tests/unit runs 192 tests in ~12 seconds with zero network or Docker dependencies.
    • Added unit test suite kazoo/tests/unit/test_testing.py providing comprehensive coverage for harness logic and fixtures.
  • Style & Python 3.12+ AST Compatibility:

    • Codebase reformatted with black==24.10.0 and commit registered in .git-blame-ignore-revs.
  • Client & Protocol Bug Fixes:

    • fix(client): Pass peer hostname instead of port as TLS SNI server name in KazooClient.command().
    • fix(protocol): Propagate ConnectionDropped during SASL handshake to enable cluster failover retry instead of stopping connection attempts.
    • fix(protocol): Defer CONNECTED state notification until SASL and initial auth data dispatch complete, and handle SASLException cleanly.
    • test(client): Eliminate flakiness in request queuing and reconnect tests by awaiting explicit server state and synchronization events.
    • test(integ): Flush replication via client1.sync("/1") in test_connect_auth to prevent replication lag assertions.
    • test(unit): Use socket.socketpair() in test_select_mutated to eliminate macOS pipe deadlock.

Does this PR introduce any breaking change?

  • The legacy kazoo.testing.harness.KazooTestCase test base class is replaced by standard pytest fixtures (zkensemble, zkclient, zksuperadmin_client, zkchroot).
  • Harness configuration environment variables now use the KAZOO_TESTING_ZK_* prefix (legacy ZK_* variable names remain supported as fallbacks in CLI resolution).

…ure kazoo.testing package

- Replace legacy ZookeeperCluster process driver with testcontainers 4.x using base+overlay compose layouts (docker-compose.base.yml).
- Add auth matrix overlays for digest, sasl-digest, tls, and sasl-gssapi with certgen and alpine KDC sidecars.
- Scaffold docker-compose.capture.yml feature overlay and tshark sidecar for pcapng network packet capture, splitting ensemble into netns-holder + service pairs.
- Emit TLS keylog files and context certificates from tls-secrets-agent for Wireshark/pcapng TLS decryption.
- Restructure kazoo.testing package into kazoo.testing.common (core logic, helpers) and kazoo.testing.fixtures (pytest plugins), retiring harness.py.
- Move compose overlays, JAAS configs, and Dockerfile resources into kazoo.testing package using importlib.resources.
- Migrate integration tests off KazooTestCase to zkclient/zkensemble pytest fixtures and retire legacy root compose files.
- Add registered pytest markers (zk_version, zk_auth, zk_features) with collection-time evaluation and skip reasons.
- Introduce ZK axis enums (backports.strenum / enum.StrEnum) and bump default ZK version to 3.9.5.
- Add contract & unit tests reaching 93%+ coverage on kazoo.testing pure helpers and subprocess functions.
- Optimize GitHub Actions workflows with test matrices, docker layer caching, and Windows WSL2 runner support.
KazooClient.command() used getpeername()[1], which is the remote port, as
the TLS hostname for SNI. Under TLS, ssl._encode_hostname rejects the
integer, so every server_version()/command() call failed (e.g. the tls
integ suite: test_queue._skip_unless_zk34, test_client server_version and
command tests). Use the peer host address instead.

Regression test: kazoo/tests/unit/test_client_command.py
Docs: CHANGES.md note
- Unskip and stabilize test_request_queuing_session_expired by synchronizing on async result resolution and eliminating state listener race conditions with queue assertions.
- Harden test_request_queuing_session_recovered to verify queued async creation and znode existence without race conditions.
- Replace fragile polling loop in test_add_auth_on_reconnect and fixed sleep in test_update_host_list with event-driven state listener synchronization.
- Increase bounded event wait timeout in test_bad_session_expire for test reliability under load.
- Record testing bug fixes in CHANGES.md under unreleased changes.
…nable failover retry

When _read_header hit ConnectionDropped during SASL handshake, it previously
raised AuthFailedError("Connection dropped in SASL"). In _connect_attempt,
AuthFailedError transitioned the client state to AUTH_FAILED and returned
STOP_CONNECTING, permanently halting the connection retry loop during cluster
failover instead of retrying available hosts. Propagating ConnectionDropped
allows the retry loop to continue to other cluster hosts.

Also add diagnostic error message to failover assertion in test_update_host_list.
…ASLException

Defer client._session_callback(KeeperState.CONNECTED) until after
_authenticate_with_sasl and initial auth_data packet dispatch complete.
Previously, the client reported CONNECTED before authentication finished,
allowing client operations to race auth processing and falsely signalling
readiness on servers enforcing authentication (enforce.auth.enabled=true).

Also catch SASLException alongside AuthFailedError in _connect_attempt
to cleanly transition client state to AUTH_FAILED.
…race

In test_connect_auth, client2 creates node /1 with ACLs restricting read
access. Follower reads in ZooKeeper are sequentially consistent, but can
briefly lag behind the leader until the commit is applied locally; if
client1 connects to a lagging follower, get("/1") could intermittently raise
NoNodeError instead of NoAuthError.

Calling client1.sync("/1") flushes the replication channel from the leader
to client1's connected follower before reading, eliminating transient
NoNodeError without polling loops.
…adlock

An unconnected TCP socket created with socket.socket(AF_INET, SOCK_STREAM)
is reported as writable in Linux epoll (TCP_CLOSE state), but BSD/XNU kqueue
on macOS requires an established connection (SS_ISCONNECTED) to signal
EVFILT_WRITE. Calling selector_select with timeout=None on an unconnected
socket caused a permanent deadlock on Darwin.

Using socket.socketpair() provides a connected stream socket pair supported
on Unix and Windows that is immediately writable on all platforms, eliminating
the macOS deadlock and removing the need for a platform skip marker while
preserving the list-mutation assertion.
@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.82759% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.27%. Comparing base (e894342) to head (8cb597a).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
kazoo/testing/common.py 97.26% 9 Missing ⚠️
kazoo/protocol/connection.py 42.85% 8 Missing ⚠️
kazoo/testing/fixtures.py 93.69% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #802      +/-   ##
==========================================
- Coverage   96.65%   92.27%   -4.38%     
==========================================
  Files          27       30       +3     
  Lines        3554     4259     +705     
==========================================
+ Hits         3435     3930     +495     
- Misses        119      329     +210     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ceache
ceache marked this pull request as ready for review September 10, 2026 07:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant