From 6ae835fca86c6885fb204a65628db5b6bb7f299b Mon Sep 17 00:00:00 2001 From: Fanta Niakate Date: Wed, 29 Jul 2026 01:14:21 +0000 Subject: [PATCH 1/3] Add ReuseServerTestCase for class-scoped server reuse Instead of spawning a fresh valkey-server per test (~42ms), one server is started per class and FLUSHALL + CONFIG RESETSTAT (~5ms) run between tests to reset state. This gives ~5-7x speedup for test classes. Changes: - src/conftest.py: add class_port_tracker fixture (scope="class") - src/valkey_test_case.py: add ReuseServerTestCase class - tests/test_reuse_server.py: demo/test proving reuse and isolation Signed-off-by: Fanta Niakate --- src/conftest.py | 10 +++++++ src/valkey_test_case.py | 54 ++++++++++++++++++++++++++++++++++++++ tests/test_reuse_server.py | 39 +++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 tests/test_reuse_server.py diff --git a/src/conftest.py b/src/conftest.py index 37eda7e..76a3efc 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -108,3 +108,13 @@ def resource_port_tracker(request): """ with PortTracker(request.node.nodeid) as p: yield p + + +@pytest.fixture(scope="class") +def class_port_tracker(request): + """ + Create port tracker shared across all tests in a class. + Used by ReuseServerTestCase to maintain one server per class. + """ + with PortTracker(request.node.nodeid) as p: + yield p diff --git a/src/valkey_test_case.py b/src/valkey_test_case.py index 90a3470..d2cf2cc 100644 --- a/src/valkey_test_case.py +++ b/src/valkey_test_case.py @@ -728,3 +728,57 @@ def waitForReplicaOffsetToSyncUp(self, primary, replica): pinfo.get_primary_repl_offset(), timeout=TEST_MAX_WAIT_TIME_SECONDS, ) + + +class ReuseServerTestCase(ValkeyTestCaseBase): + """Test case that reuses a single server across all tests in the class. + + Instead of spawning a fresh server per test (~42ms each), one server is + started for the entire class and FLUSHALL + CONFIG RESETSTAT run between + tests (~5ms) to reset state. + """ + + server_path = "valkey-server" + + def _ensure_testdir(self): + if not os.path.isdir(self.testdir): + try: + os.mkdir(self.testdir) + except OSError: + assert os.path.isdir(self.testdir) + + @pytest.fixture(autouse=True, scope="class") + def class_server(self, class_port_tracker): + self.__class__.port_tracker = class_port_tracker + self.__class__.port = class_port_tracker.get_unused_port() + self.__class__._server_list = [] + self._ensure_testdir() + server = ValkeyServerHandle( + bind_ip=self.DEFAULT_BIND_IP, + port=self.__class__.port, + port_tracker=class_port_tracker, + cwd=self.testdir, + server_path=self.server_path, + ) + server.start(wait_for_ping=True, connect_client=True) + self.__class__._shared_server = server + self.__class__._shared_client = server.client + self.__class__._server_list.append(server) + yield + for s in self.__class__._server_list: + if s: + s.exit() + + @pytest.fixture(autouse=True) + def reset_between_tests(self, class_server): + yield + self._shared_client.flushall() + self._shared_client.execute_command("CONFIG", "RESETSTAT") + + @property + def server(self): + return self.__class__._shared_server + + @property + def client(self): + return self.__class__._shared_client diff --git a/tests/test_reuse_server.py b/tests/test_reuse_server.py new file mode 100644 index 0000000..bc715c5 --- /dev/null +++ b/tests/test_reuse_server.py @@ -0,0 +1,39 @@ +""" +Demonstrates ReuseServerTestCase usage. + +All tests in this class share ONE server. Between each test, FLUSHALL + CONFIG +RESETSTAT run automatically to give each test a clean slate without the cost of +restarting the server. +""" + +import pytest +from conftest import class_port_tracker, resource_port_tracker +from valkey_test_case import ReuseServerTestCase + + +class TestReuseServer(ReuseServerTestCase): + """Verifies that server reuse works and tests are isolated.""" + + def test_write_and_read(self): + """Basic write/read on the shared server.""" + self.client.set("greeting", "hello") + assert self.client.get("greeting") == b"hello" + + def test_isolation_from_previous(self): + """Proves FLUSHALL cleaned up the previous test's data.""" + result = self.client.get("greeting") + assert result is None, "Key from previous test should not exist" + + def test_server_still_alive(self): + """Proves the server survived across tests (no restart).""" + assert self.client.ping() is True + + def test_multiple_keys(self): + """Write multiple keys, verify they all exist within this test.""" + for i in range(10): + self.client.set(f"key:{i}", f"value:{i}") + assert self.client.dbsize() == 10 + + def test_previous_keys_gone(self): + """Proves the 10 keys from the previous test were flushed.""" + assert self.client.dbsize() == 0 From 88d0f96bbc9e3ba81cd5d5ceeb49eef255aca3c6 Mon Sep 17 00:00:00 2001 From: Fanta Niakate Date: Thu, 30 Jul 2026 17:12:35 +0000 Subject: [PATCH 2/3] Redesign ReuseServerTestCase for zero-friction module adoption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ReuseServerTestCase now inherits ValkeyTestCase and overrides create_server() to cache the server on first call. Modules only need to change their base class — no other code modifications required. Key changes: - Inherit ValkeyTestCase instead of ValkeyTestCaseBase so all fixtures (setup, port_tracker_fixture) work automatically - Override create_server() to return cached server on subsequent calls - Snapshot all configs on first creation and restore between tests to prevent config pollution across tests - FLUSHALL + CONFIG RESETSTAT between tests for data isolation - Bump pytest to 7.4.3 Tested against bloom (34/34 pass) and JSON (170/171 pass, 1 unrelated env var issue). JSON sees ~17x speedup (8s vs 138s). Signed-off-by: Fanta Niakate --- requirements.txt | 2 +- src/valkey_test_case.py | 104 ++++++++++++++++++++++++---------------- 2 files changed, 63 insertions(+), 43 deletions(-) diff --git a/requirements.txt b/requirements.txt index 349566f..4583452 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ valkey -pytest==6 +pytest==7.4.3 black pytest-order diff --git a/src/valkey_test_case.py b/src/valkey_test_case.py index d2cf2cc..2a90644 100644 --- a/src/valkey_test_case.py +++ b/src/valkey_test_case.py @@ -730,55 +730,75 @@ def waitForReplicaOffsetToSyncUp(self, primary, replica): ) -class ReuseServerTestCase(ValkeyTestCaseBase): +class ReuseServerTestCase(ValkeyTestCase): """Test case that reuses a single server across all tests in the class. - Instead of spawning a fresh server per test (~42ms each), one server is - started for the entire class and FLUSHALL + CONFIG RESETSTAT run between - tests (~5ms) to reset state. + Instead of spawning a fresh server per test, one server is started on the + first create_server() call and reused for all subsequent tests. FLUSHALL + + CONFIG RESETSTAT run between tests to reset state. + + Usage — just change your base class: + + class MyModuleTestCase(ReuseServerTestCase): + ... # keep your existing setup_test exactly as-is + + That's it. self.server, self.client, create_server() all work as before. """ - server_path = "valkey-server" + def create_server( + self, + testdir=None, + bind_ip=None, + port=None, + server_path=None, + args="", + skip_teardown=False, + conf_file=None, + external_server=False, + wait_for_ping=True, + connect_client=True, + ): + if hasattr(self.__class__, '_shared_server') and self.__class__._shared_server: + return self.__class__._shared_server, self.__class__._shared_client - def _ensure_testdir(self): - if not os.path.isdir(self.testdir): - try: - os.mkdir(self.testdir) - except OSError: - assert os.path.isdir(self.testdir) + if server_path is None: + server_path = self.server_path - @pytest.fixture(autouse=True, scope="class") - def class_server(self, class_port_tracker): - self.__class__.port_tracker = class_port_tracker - self.__class__.port = class_port_tracker.get_unused_port() - self.__class__._server_list = [] - self._ensure_testdir() - server = ValkeyServerHandle( - bind_ip=self.DEFAULT_BIND_IP, - port=self.__class__.port, - port_tracker=class_port_tracker, - cwd=self.testdir, - server_path=self.server_path, + server, client = super().create_server( + testdir=testdir, + bind_ip=bind_ip, + port=port, + server_path=server_path, + args=args, + skip_teardown=skip_teardown, + conf_file=conf_file, + external_server=external_server, + wait_for_ping=wait_for_ping, + connect_client=connect_client, ) - server.start(wait_for_ping=True, connect_client=True) self.__class__._shared_server = server - self.__class__._shared_client = server.client - self.__class__._server_list.append(server) - yield - for s in self.__class__._server_list: - if s: - s.exit() - - @pytest.fixture(autouse=True) - def reset_between_tests(self, class_server): - yield - self._shared_client.flushall() - self._shared_client.execute_command("CONFIG", "RESETSTAT") + self.__class__._shared_client = client + self.__class__._initial_config = client.config_get("*") + return server, client - @property - def server(self): - return self.__class__._shared_server + def teardown(self): + if hasattr(self.__class__, '_shared_server') and self.__class__._shared_server: + client = self.__class__._shared_client + client.flushall() + client.execute_command("CONFIG", "RESETSTAT") + if hasattr(self.__class__, '_initial_config'): + current = client.config_get("*") + for key, val in self.__class__._initial_config.items(): + if current.get(key) != val: + try: + client.config_set(key, val) + except Exception: + pass - @property - def client(self): - return self.__class__._shared_client + @pytest.fixture(autouse=True, scope="class") + def class_teardown(self, request): + yield + if hasattr(self.__class__, '_shared_server') and self.__class__._shared_server: + self.__class__._shared_server.exit() + self.__class__._shared_server = None + self.__class__._shared_client = None From c3685d44000046197298efe6c2aae575fd749815 Mon Sep 17 00:00:00 2001 From: Fanta Niakate Date: Thu, 30 Jul 2026 23:52:57 +0000 Subject: [PATCH 3/3] Address PR feedback: crash handling, testdir fallback, and setup_test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Handle server crash in teardown: if flushall/config reset fails, discard the shared server so next test creates a fresh one - Default testdir to self.testdir when not passed (same pattern as server_path) so None never reaches the parent - Add setup_test fixture to test_reuse_server.py to call create_server() matching the pattern modules use - Remove unused class_port_tracker import from test file - Revert pytest version bump — not needed for server reuse Signed-off-by: Fanta Niakate --- requirements.txt | 2 +- src/conftest.py | 10 ---------- src/valkey_test_case.py | 26 ++++++++++++++++---------- tests/test_reuse_server.py | 6 +++++- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4583452..349566f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ valkey -pytest==7.4.3 +pytest==6 black pytest-order diff --git a/src/conftest.py b/src/conftest.py index 76a3efc..37eda7e 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -108,13 +108,3 @@ def resource_port_tracker(request): """ with PortTracker(request.node.nodeid) as p: yield p - - -@pytest.fixture(scope="class") -def class_port_tracker(request): - """ - Create port tracker shared across all tests in a class. - Used by ReuseServerTestCase to maintain one server per class. - """ - with PortTracker(request.node.nodeid) as p: - yield p diff --git a/src/valkey_test_case.py b/src/valkey_test_case.py index 2a90644..a924582 100644 --- a/src/valkey_test_case.py +++ b/src/valkey_test_case.py @@ -761,6 +761,8 @@ def create_server( if hasattr(self.__class__, '_shared_server') and self.__class__._shared_server: return self.__class__._shared_server, self.__class__._shared_client + if testdir is None: + testdir = self.testdir if server_path is None: server_path = self.server_path @@ -784,16 +786,20 @@ def create_server( def teardown(self): if hasattr(self.__class__, '_shared_server') and self.__class__._shared_server: client = self.__class__._shared_client - client.flushall() - client.execute_command("CONFIG", "RESETSTAT") - if hasattr(self.__class__, '_initial_config'): - current = client.config_get("*") - for key, val in self.__class__._initial_config.items(): - if current.get(key) != val: - try: - client.config_set(key, val) - except Exception: - pass + try: + client.flushall() + client.execute_command("CONFIG", "RESETSTAT") + if hasattr(self.__class__, '_initial_config'): + current = client.config_get("*") + for key, val in self.__class__._initial_config.items(): + if current.get(key) != val: + try: + client.config_set(key, val) + except Exception: + pass + except Exception: + self.__class__._shared_server = None + self.__class__._shared_client = None @pytest.fixture(autouse=True, scope="class") def class_teardown(self, request): diff --git a/tests/test_reuse_server.py b/tests/test_reuse_server.py index bc715c5..f8f303e 100644 --- a/tests/test_reuse_server.py +++ b/tests/test_reuse_server.py @@ -7,13 +7,17 @@ """ import pytest -from conftest import class_port_tracker, resource_port_tracker +from conftest import resource_port_tracker from valkey_test_case import ReuseServerTestCase class TestReuseServer(ReuseServerTestCase): """Verifies that server reuse works and tests are isolated.""" + @pytest.fixture(autouse=True) + def setup_test(self, setup): + self.server, self.client = self.create_server(testdir=self.testdir) + def test_write_and_read(self): """Basic write/read on the shared server.""" self.client.set("greeting", "hello")